9 Commits

Author SHA1 Message Date
twotalesanimation
a66382661d Fixed some bugs 2025-12-13 19:25:47 +02:00
twotalesanimation
32e50ffc39 Commit since isp push 2025-12-13 14:33:23 +02:00
twotalesanimation
cce181e2d0 Add interactive Base 4 track map with Leaflet.js
- Created new track-map page with aerial image and SVG overlay
- Implemented custom rotated square markers with obstacle numbers
- Added admin edit mode for placing and repositioning markers
- Database migration for track_obstacles table
- Modal form for adding new obstacles (replaces browser alerts)
- Drag-to-reposition functionality with auto-save
- Color-coded markers (green/red/black/split) for difficulty levels
- Clickable popups showing obstacle details
- Added track-map to navigation menu and sitemap
- URL rewrite rule for clean /track-map URL
2025-12-12 12:00:20 +02:00
twotalesanimation
48ee7592b2 Reorganize event processors and update routing
- Move process_event.php from src/admin to src/processors
- Move toggle_event_published.php from src/admin to src/processors
- Move delete_event.php from src/admin to src/processors
- Update .htaccess rewrite rules to point event processors to correct location
- Keep admin_events.php and manage_events.php in admin (display pages only)
2025-12-11 08:55:24 +02:00
twotalesanimation
abb8eb23e5 Add updates modal to homepage with session-based display and Jan 1 2026 expiry 2025-12-08 11:47:01 +02:00
twotalesanimation
2acbeac7ca fixed gallery 2025-12-08 11:39:57 +02:00
twotalesanimation
5808788b9e Make blog cards clickable - wrap in anchor tags matching gallery pattern 2025-12-08 11:35:22 +02:00
twotalesanimation
bbc0aecbcb force update CSS2 2025-12-08 10:55:08 +02:00
twotalesanimation
752ea6e5e9 fix: correct CSS syntax error in .comments rule that was breaking footer and other component styles 2025-12-08 10:37:01 +02:00
86 changed files with 15289 additions and 1078 deletions

View File

@@ -70,6 +70,7 @@ RewriteRule ^instapage$ src/pages/events/instapage.php [L]
RewriteRule ^about$ src/pages/other/about.php [L] RewriteRule ^about$ src/pages/other/about.php [L]
RewriteRule ^contact$ src/pages/other/contact.php [L] RewriteRule ^contact$ src/pages/other/contact.php [L]
RewriteRule ^privacy_policy$ src/pages/other/privacy_policy.php [L] RewriteRule ^privacy_policy$ src/pages/other/privacy_policy.php [L]
RewriteRule ^track-map$ src/pages/track-map.php [L]
RewriteRule ^404$ src/pages/other/404.php [L] RewriteRule ^404$ src/pages/other/404.php [L]
RewriteRule ^account_settings$ src/pages/other/account_settings.php [L] RewriteRule ^account_settings$ src/pages/other/account_settings.php [L]
RewriteRule ^rescue_recovery$ src/pages/other/rescue_recovery.php [L] RewriteRule ^rescue_recovery$ src/pages/other/rescue_recovery.php [L]
@@ -122,11 +123,11 @@ RewriteRule ^upload_profile_picture$ src/processors/upload_profile_picture.php [
RewriteRule ^send_reset_link$ src/processors/send_reset_link.php [L] RewriteRule ^send_reset_link$ src/processors/send_reset_link.php [L]
RewriteRule ^logout$ src/processors/logout.php [L] RewriteRule ^logout$ src/processors/logout.php [L]
RewriteRule ^process_trip$ src/processors/process_trip.php [L] RewriteRule ^process_trip$ src/processors/process_trip.php [L]
RewriteRule ^process_event$ src/admin/process_event.php [L] RewriteRule ^process_event$ src/processors/process_event.php [L]
RewriteRule ^toggle_trip_published$ src/processors/toggle_trip_published.php [L] RewriteRule ^toggle_trip_published$ src/processors/toggle_trip_published.php [L]
RewriteRule ^toggle_event_published$ src/admin/toggle_event_published.php [L] RewriteRule ^toggle_event_published$ src/processors/toggle_event_published.php [L]
RewriteRule ^delete_trip$ src/processors/delete_trip.php [L] RewriteRule ^delete_trip$ src/processors/delete_trip.php [L]
RewriteRule ^delete_event$ src/admin/delete_event.php [L] RewriteRule ^delete_event$ src/processors/delete_event.php [L]
RewriteRule ^save_album$ src/processors/save_album.php [L] RewriteRule ^save_album$ src/processors/save_album.php [L]
RewriteRule ^update_album$ src/processors/update_album.php [L] RewriteRule ^update_album$ src/processors/update_album.php [L]
RewriteRule ^delete_album$ src/processors/delete_album.php [L] RewriteRule ^delete_album$ src/processors/delete_album.php [L]

4
.user.ini Normal file
View File

@@ -0,0 +1,4 @@
; memory_limit = 512M
upload_max_filesize = 64M
post_max_size = 64M
max_execution_time = 120

View File

@@ -1,3 +0,0 @@
<?php
// Redirector file - loads the actual page from src/pages/other/
require_once __DIR__ . '/src/pages/other/about.php';

View File

@@ -1,12 +1,6 @@
@charset "UTF-8"; @charset "UTF-8";
/*---------------------------------------------------------------------- /*----------------------------------------------------------------------
Template Name: Ravelo - Travel & Tour Booking HTML Template 4WDCSA.co.za CSS Stylesheet
Template URI: https://webtend.net/demo/html/ravelo/
Author: WebTend
Author URI: https://webtend.net/
Version: 1.0
Note: This is Main Style CSS File. */
/*---------------------------------------------------------------------- /*----------------------------------------------------------------------
CSS INDEX CSS INDEX
---------------------- ----------------------
@@ -7124,7 +7118,8 @@ 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; }

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 494 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 291 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 291 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 MiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 24 KiB

13126
assets/images/track-route2.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 2.6 MiB

66
assets/js/map.js Normal file
View File

@@ -0,0 +1,66 @@
/**
* TRACK MAP WITH LEAFLET.JS
*
* Basic Leaflet map test
*/
console.log('Track map script loaded2');
// Check if Leaflet is available
if (typeof L === 'undefined') {
console.error('Leaflet library not loaded!');
} else {
console.log('Leaflet library is available, version:', L.version);
}
document.addEventListener('DOMContentLoaded', () => {
console.log('DOM loaded, initializing map...');
const mapElement = document.getElementById('map');
console.log('Map element:', mapElement);
if (!mapElement) {
console.error('Map element not found!');
return;
}
console.log('Map element dimensions:', mapElement.offsetWidth, 'x', mapElement.offsetHeight);
try {
// Image dimensions: 2876 x 2035 pixels
const imageWidth = 2876;
const imageHeight = 2035;
// Create map with simple CRS (pixel coordinates)
// Note: Leaflet uses [y, x] format, so bounds are [[0, 0], [height, width]]
const bounds = [[0, 0], [imageHeight, imageWidth]];
const map = L.map('map', {
crs: L.CRS.Simple,
minZoom: -2,
maxZoom: 2,
center: [imageHeight / 2, imageWidth / 2],
zoom: -1
});
console.log('Map object created with CRS.Simple:', map);
// Add aerial image overlay
const imageUrl = '/assets/images/track-aerial.jpg';
L.imageOverlay(imageUrl, bounds).addTo(map);
console.log('Aerial image overlay added');
// Add SVG overlay
const svgUrl = '/assets/images/track-route.svg';
L.imageOverlay(svgUrl, bounds, {
opacity: 0.8,
interactive: false
}).addTo(map);
console.log('SVG route overlay added');
// Fit map to image bounds
map.fitBounds(bounds);
console.log('Map initialized successfully');
} catch (error) {
console.error('Error initializing map:', error);
}
});

View File

@@ -0,0 +1,25 @@
-- ============================================================================
-- MIGRATION: Create Track Obstacles Table
-- Date: 2025-12-12
-- Description: Create table to store 4x4 track obstacles with positioning and details
-- ============================================================================
CREATE TABLE IF NOT EXISTS track_obstacles (
obstacle_id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(100) NOT NULL COMMENT 'Obstacle name (e.g., "Rock Crawl", "Water Crossing")',
x_position INT NOT NULL COMMENT 'X pixel position on the track map',
y_position INT NOT NULL COMMENT 'Y pixel position on the track map',
difficulty VARCHAR(20) NOT NULL COMMENT 'Difficulty level (easy, medium, hard, extreme)',
description TEXT COMMENT 'Detailed description of the obstacle',
image_path VARCHAR(255) COMMENT 'Path to obstacle image (e.g., assets/images/obstacles/obstacle1.jpg)',
marker_color VARCHAR(20) NOT NULL COMMENT 'Marker color: red, green, black, or split (red-green)',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_name (name),
INDEX idx_difficulty (difficulty)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- ============================================================================
-- ROLLBACK INSTRUCTIONS (if needed)
-- ============================================================================
-- DROP TABLE IF EXISTS track_obstacles;

View File

@@ -258,6 +258,7 @@ if ($headerStyle === 'light') {
<ul class="navigation clearfix"> <ul class="navigation clearfix">
<li><a href="index">Home</a></li> <li><a href="index">Home</a></li>
<li><a href="about">About</a></li> <li><a href="about">About</a></li>
<li><a href="track-map">Track Map</a></li>
<li><a href="trips">Trips</a> <li><a href="trips">Trips</a>
<?php if ($headerStyle === 'dark'): ?> <?php if ($headerStyle === 'dark'): ?>
<ul> <ul>
@@ -319,7 +320,13 @@ 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> <?php
if (getUserMemberStatus($_SESSION['user_id'])) {
echo "<li><a href=\"user_blogs\">My Blog Posts</a></li>";
} else {
echo "<li><a href=\"membership\">My Blog Posts</a><i class='fal fa-lock'></i></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>

212
index.php
View File

@@ -4,6 +4,21 @@ $headerStyle = 'dark';
include_once($rootPath . '/header.php'); include_once($rootPath . '/header.php');
$indemnityPending = false; $indemnityPending = false;
// Set session flag for updates modal - only show once per session and before Jan 1, 2026
if (!isset($_SESSION['updates_modal_shown'])) {
$currentDate = new DateTime();
$endDate = new DateTime('2026-01-01');
if ($currentDate < $endDate) {
$_SESSION['updates_modal_shown'] = true;
$showUpdatesModal = true;
} else {
$showUpdatesModal = false;
}
} else {
$showUpdatesModal = false;
}
if (isset($_SESSION['user_id']) && isset($conn) && $conn !== null) { if (isset($_SESSION['user_id']) && isset($conn) && $conn !== null) {
$userId = $_SESSION['user_id']; $userId = $_SESSION['user_id'];
$stmt = $conn->prepare("SELECT user_id FROM membership_application WHERE user_id = ? AND accept_indemnity = 0 LIMIT 1"); $stmt = $conn->prepare("SELECT user_id FROM membership_application WHERE user_id = ? AND accept_indemnity = 0 LIMIT 1");
@@ -54,7 +69,7 @@ if (!empty($bannerImages)) {
<div style="padding-top: 50px; padding-bottom: 50px;"> <div style="padding-top: 50px; padding-bottom: 50px;">
<img style="width: 250px; margin-bottom: 20px;" src="assets/images/logos/weblogo2.png" alt="Logo"> <img style="width: 250px; margin-bottom: 20px;" src="assets/images/logos/weblogo2.png" alt="Logo">
<h1 class="hero-title" data-aos="flip-up" data-aos-delay="50" data-aos-duration="1500" data-aos-offset="50"> <h1 class="hero-title" data-aos="flip-up" data-aos-delay="50" data-aos-duration="1500" data-aos-offset="50">
Welcome to<br>the Four Wheel Drive Club<br>of Southern Africa Welcome to<br>the 4 Wheel Drive Club<br>of Southern Africa
</h1> </h1>
<a href="membership.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="membership.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="Become a Member">Become a Member</span> <span data-hover="Become a Member">Become a Member</span>
@@ -636,8 +651,203 @@ if (countUpcomingTrips() > 0) { ?>
updateCountdown(); // initial call updateCountdown(); // initial call
setInterval(updateCountdown, 1000); setInterval(updateCountdown, 1000);
// Show updates modal on page load
document.addEventListener('DOMContentLoaded', function() {
const modal = document.getElementById('updatesModal');
const closeBtn = document.querySelector('.updates-modal-close');
const showModal = <?php echo $showUpdatesModal ? 'true' : 'false'; ?>;
if (showModal) {
// Show modal after a short delay for better UX
setTimeout(function() {
modal.style.display = 'flex';
}, 500);
}
// Close modal when X is clicked
closeBtn.addEventListener('click', function() {
modal.style.display = 'none';
});
// Close modal when clicking outside the modal content
modal.addEventListener('click', function(event) {
if (event.target === modal) {
modal.style.display = 'none';
}
});
});
</script> </script>
<!-- Updates Modal -->
<div id="updatesModal" class="updates-modal">
<div class="updates-modal-content">
<span class="updates-modal-close">&times;</span>
<div class="updates-modal-header">
<h2>✨ What's New</h2>
</div>
<div class="updates-modal-body">
<div class="update-item">
<h3><i class="fas fa-images" style="margin-right: 10px; color: #e90000;"></i>Track Map</h3>
<p>Interactive map of the BASE4 4x4 Training Track.</p>
</div>
<div class="update-item">
<h3><i class="fas fa-images" style="margin-right: 10px; color: #e90000;"></i>Photo Gallery</h3>
<p>Explore and share memories from club events and trips. Members can now upload and view photos from past adventures.</p>
</div>
<div class="update-item">
<h3><i class="fas fa-map-location-dot" style="margin-right: 10px; color: #e90000;"></i>Campsites Directory</h3>
<p>Discover recommended campsites and accommodation options for your next adventure. Browse detailed information and member reviews.</p>
</div>
<div class="update-item">
<h3><i class="fas fa-users" style="margin-right: 10px; color: #e90000;"></i>Linked Membership</h3>
<p>Link a second user to your profile so both can book trips and receive member benefits together.</p>
</div>
<div class="update-item">
<h3><i class="fas fa-pen-fancy" style="margin-right: 10px; color: #e90000;"></i>Blog Posts</h3>
<p>Members can now post blogs, reviews, and trip reports to share experiences with the community.</p>
</div>
</div>
<div class="updates-modal-footer">
<button class="theme-btn style-two updates-modal-btn" onclick="document.getElementById('updatesModal').style.display='none'">
<span>Got It</span>
</button>
</div>
</div>
</div>
<style>
.updates-modal {
display: none;
position: fixed;
z-index: 9999;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.6);
align-items: center;
justify-content: center;
animation: fadeIn 0.3s ease-in-out;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.updates-modal-content {
background-color: white;
padding: 40px;
border-radius: 15px;
max-width: 500px;
width: 90%;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
animation: slideDown 0.3s ease-out;
position: relative;
}
@keyframes slideDown {
from {
transform: translateY(-50px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
.updates-modal-close {
position: absolute;
right: 20px;
top: 20px;
font-size: 28px;
font-weight: bold;
color: #999;
cursor: pointer;
transition: color 0.2s;
}
.updates-modal-close:hover {
color: #e90000;
}
.updates-modal-header {
margin-bottom: 30px;
text-align: center;
}
.updates-modal-header h2 {
color: #1c231f;
font-size: 28px;
margin: 0;
}
.updates-modal-body {
margin-bottom: 30px;
}
.update-item {
margin-bottom: 25px;
padding-bottom: 25px;
border-bottom: 1px solid #e0e0e0;
}
.update-item:last-child {
border-bottom: none;
margin-bottom: 0;
padding-bottom: 0;
}
.update-item h3 {
color: #1c231f;
font-size: 18px;
margin: 0 0 10px 0;
display: flex;
align-items: center;
}
.update-item p {
color: #666;
font-size: 14px;
line-height: 1.6;
margin: 0;
}
.updates-modal-footer {
text-align: center;
}
.updates-modal-btn {
padding: 10px 30px !important;
background-color: #e90000 !important;
color: white !important;
}
.updates-modal-btn:hover {
background-color: #c70000 !important;
}
@media (max-width: 600px) {
.updates-modal-content {
padding: 30px 20px;
}
.updates-modal-header h2 {
font-size: 24px;
}
.update-item h3 {
font-size: 16px;
}
}
</style>
</body> </body>
</html> </html>

View File

@@ -1,107 +0,0 @@
<?php
// Migration runner - creates membership linking tables if they don't exist
require_once __DIR__ . '/vendor/autoload.php';
require_once __DIR__ . '/src/config/env.php';
require_once __DIR__ . '/src/config/functions.php';
$conn = openDatabaseConnection();
if (!$conn) {
die("Database connection failed\n");
}
echo "Connected to database successfully.\n\n";
// Check if membership_links table exists
$checkTable = $conn->query("SHOW TABLES LIKE 'membership_links'");
if ($checkTable->num_rows > 0) {
echo "✓ membership_links table already exists\n";
} else {
echo "Creating membership_links table...\n";
$createLink = $conn->query("
CREATE TABLE IF NOT EXISTS `membership_links` (
`link_id` INT AUTO_INCREMENT PRIMARY KEY,
`primary_user_id` INT NOT NULL,
`secondary_user_id` INT NOT NULL,
`relationship` VARCHAR(50) NOT NULL DEFAULT 'spouse',
`linked_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT `fk_membership_links_primary` FOREIGN KEY (`primary_user_id`)
REFERENCES `users`(`user_id`) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT `fk_membership_links_secondary` FOREIGN KEY (`secondary_user_id`)
REFERENCES `users`(`user_id`) ON DELETE CASCADE ON UPDATE CASCADE,
INDEX `idx_primary_user` (`primary_user_id`),
INDEX `idx_secondary_user` (`secondary_user_id`),
UNIQUE KEY `unique_link` (`primary_user_id`, `secondary_user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
");
if ($createLink) {
echo "✓ membership_links table created successfully\n";
} else {
echo "✗ Error creating membership_links table: " . $conn->error . "\n";
}
}
// Check if membership_permissions table exists
$checkTable = $conn->query("SHOW TABLES LIKE 'membership_permissions'");
if ($checkTable->num_rows > 0) {
echo "✓ membership_permissions table already exists\n";
} else {
echo "Creating membership_permissions table...\n";
$createPerm = $conn->query("
CREATE TABLE IF NOT EXISTS `membership_permissions` (
`permission_id` INT AUTO_INCREMENT PRIMARY KEY,
`link_id` INT NOT NULL,
`permission_name` VARCHAR(100) NOT NULL,
`granted_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT `fk_membership_permissions_link` FOREIGN KEY (`link_id`)
REFERENCES `membership_links`(`link_id`) ON DELETE CASCADE ON UPDATE CASCADE,
INDEX `idx_link` (`link_id`),
UNIQUE KEY `unique_permission` (`link_id`, `permission_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
");
if ($createPerm) {
echo "✓ membership_permissions table created successfully\n";
} else {
echo "✗ Error creating membership_permissions table: " . $conn->error . "\n";
}
}
// Create or replace the view
echo "\nCreating linked_membership_users view...\n";
$createView = $conn->query("
CREATE OR REPLACE VIEW `linked_membership_users` AS
SELECT
primary_user_id,
secondary_user_id,
relationship,
linked_at
FROM membership_links
UNION ALL
SELECT
primary_user_id,
primary_user_id as secondary_user_id,
'primary' as relationship,
linked_at
FROM membership_links
");
if ($createView) {
echo "✓ View created successfully\n";
} else {
echo "✗ Error creating view: " . $conn->error . "\n";
}
$conn->close();
echo "\n✓ Migration completed successfully!\n";
?>

View File

@@ -1,53 +1,124 @@
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd"> <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd">
<!-- created with Free Online Sitemap Generator www.xml-sitemaps.com --> <!-- Updated: 2025-12-13 -->
<!-- Homepage -->
<url> <url>
<loc>https://4wdcsa.co.za/</loc> <loc>https://4wdcsa.co.za/</loc>
<lastmod>2025-04-10T11:24:41+00:00</lastmod> <lastmod>2025-12-13T00:00:00+00:00</lastmod>
<priority>1.00</priority> <priority>1.00</priority>
<changefreq>weekly</changefreq>
</url>
<!-- Main Pages -->
<url>
<loc>https://4wdcsa.co.za/about</loc>
<lastmod>2025-12-13T00:00:00+00:00</lastmod>
<priority>0.90</priority>
<changefreq>monthly</changefreq>
</url> </url>
<url> <url>
<loc>https://4wdcsa.co.za/index.php</loc> <loc>https://4wdcsa.co.za/contact</loc>
<lastmod>2025-04-10T11:24:41+00:00</lastmod> <lastmod>2025-12-13T00:00:00+00:00</lastmod>
<priority>0.90</priority>
<changefreq>monthly</changefreq>
</url>
<url>
<loc>https://4wdcsa.co.za/track-map</loc>
<lastmod>2025-12-13T00:00:00+00:00</lastmod>
<priority>0.85</priority>
<changefreq>weekly</changefreq>
</url>
<!-- Trips & Events -->
<url>
<loc>https://4wdcsa.co.za/trips</loc>
<lastmod>2025-12-13T00:00:00+00:00</lastmod>
<priority>0.95</priority>
<changefreq>weekly</changefreq>
</url>
<url>
<loc>https://4wdcsa.co.za/events</loc>
<lastmod>2025-12-13T00:00:00+00:00</lastmod>
<priority>0.95</priority>
<changefreq>weekly</changefreq>
</url>
<url>
<loc>https://4wdcsa.co.za/driver_training</loc>
<lastmod>2025-12-13T00:00:00+00:00</lastmod>
<priority>0.90</priority>
<changefreq>monthly</changefreq>
</url>
<!-- Blog & Gallery -->
<url>
<loc>https://4wdcsa.co.za/blog</loc>
<lastmod>2025-12-13T00:00:00+00:00</lastmod>
<priority>0.85</priority>
<changefreq>weekly</changefreq>
</url>
<url>
<loc>https://4wdcsa.co.za/gallery</loc>
<lastmod>2025-12-13T00:00:00+00:00</lastmod>
<priority>0.80</priority> <priority>0.80</priority>
<changefreq>weekly</changefreq>
</url>
<!-- Membership -->
<url>
<loc>https://4wdcsa.co.za/membership</loc>
<lastmod>2025-12-13T00:00:00+00:00</lastmod>
<priority>0.95</priority>
<changefreq>monthly</changefreq>
</url> </url>
<url> <url>
<loc>https://4wdcsa.co.za/about.php</loc> <loc>https://4wdcsa.co.za/membership_details</loc>
<lastmod>2025-04-10T11:24:41+00:00</lastmod> <lastmod>2025-12-13T00:00:00+00:00</lastmod>
<priority>0.85</priority>
<changefreq>monthly</changefreq>
</url>
<!-- Campsites -->
<url>
<loc>https://4wdcsa.co.za/campsites</loc>
<lastmod>2025-12-13T00:00:00+00:00</lastmod>
<priority>0.90</priority>
<changefreq>monthly</changefreq>
</url>
<!-- Special Pages -->
<url>
<loc>https://4wdcsa.co.za/rescue_recovery</loc>
<lastmod>2025-12-13T00:00:00+00:00</lastmod>
<priority>0.80</priority> <priority>0.80</priority>
<changefreq>monthly</changefreq>
</url> </url>
<url> <url>
<loc>https://4wdcsa.co.za/trips.php</loc> <loc>https://4wdcsa.co.za/bush_mechanics</loc>
<lastmod>2025-04-10T11:24:41+00:00</lastmod> <lastmod>2025-12-13T00:00:00+00:00</lastmod>
<priority>0.80</priority> <priority>0.80</priority>
<changefreq>monthly</changefreq>
</url>
<!-- Auth Pages (Lower Priority) -->
<url>
<loc>https://4wdcsa.co.za/login</loc>
<lastmod>2025-12-13T00:00:00+00:00</lastmod>
<priority>0.60</priority>
<changefreq>yearly</changefreq>
</url> </url>
<url> <url>
<loc>https://4wdcsa.co.za/events.php</loc> <loc>https://4wdcsa.co.za/register</loc>
<lastmod>2025-04-10T11:24:41+00:00</lastmod> <lastmod>2025-12-13T00:00:00+00:00</lastmod>
<priority>0.80</priority> <priority>0.60</priority>
<changefreq>yearly</changefreq>
</url> </url>
<!-- Legal -->
<url> <url>
<loc>https://4wdcsa.co.za/blog.php</loc> <loc>https://4wdcsa.co.za/privacy_policy</loc>
<lastmod>2025-04-10T11:24:41+00:00</lastmod> <lastmod>2025-12-13T00:00:00+00:00</lastmod>
<priority>0.80</priority> <priority>0.50</priority>
</url> <changefreq>yearly</changefreq>
<url>
<loc>https://4wdcsa.co.za/login.php</loc>
<lastmod>2025-04-10T11:24:41+00:00</lastmod>
<priority>0.80</priority>
</url>
<url>
<loc>https://4wdcsa.co.za/membership.php</loc>
<lastmod>2025-04-10T11:24:41+00:00</lastmod>
<priority>0.80</priority>
</url>
<url>
<loc>https://4wdcsa.co.za/register.php</loc>
<lastmod>2025-04-10T11:24:41+00:00</lastmod>
<priority>0.64</priority>
</url>
<url>
<loc>https://4wdcsa.co.za/forgot_password.php</loc>
<lastmod>2025-04-10T11:24:41+00:00</lastmod>
<priority>0.64</priority>
</url> </url>
</urlset> </urlset>

View File

@@ -38,13 +38,8 @@ if (isset($_FILES['thumbnail']) && $_FILES['thumbnail']['error'] !== UPLOAD_ERR_
} }
$uploadDir = "assets/uploads/campsites/"; $uploadDir = "assets/uploads/campsites/";
if (!is_dir($uploadDir)) { if (!file_exists($uploadDir)) {
mkdir($uploadDir, 0755, true); mkdir($uploadDir, 0777, true);
}
if (!is_writable($uploadDir)) {
http_response_code(500);
die('Upload directory is not writable.');
} }
$randomFilename = $validationResult['filename']; $randomFilename = $validationResult['filename'];

View File

@@ -1,13 +1,20 @@
<?php <?php
$headerStyle = 'light';
$rootPath = dirname(dirname(__DIR__)); $rootPath = dirname(dirname(__DIR__));
include_once($rootPath . '/header.php'); 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(); checkAdmin();
checkUserSession();
$pageTitle = 'Manage Events';
$breadcrumbs = [['Home' => 'index']];
require_once($rootPath . '/components/banner.php');
// Fetch all events // Fetch all events
$events_query = " $events_query = "
SELECT SELECT
event_id, name, type, location, date, published event_id, name, type, location, date, image, published
FROM events FROM events
ORDER BY date DESC ORDER BY date DESC
"; ";
@@ -22,340 +29,202 @@ if ($result && $result->num_rows > 0) {
?> ?>
<style> <style>
table { .image {
width: 300px;
/* Set your desired width */
height: 250px;
/* Set your desired height */
overflow: hidden;
/* Hide any overflow */
display: block;
/* Ensure proper block behavior */
border-radius: 8px;
}
.image img {
width: 100%; width: 100%;
border-collapse: separate; /* Image scales to fill the container */
border-spacing: 0; height: 100%;
margin: 10px 0; /* Image scales to fill the container */
} object-fit: cover;
/* Fills the container while maintaining aspect ratio */
thead th { object-position: center;
cursor: pointer; /* Aligns the center of the image with the center of the container */
text-align: left; display: block;
padding: 10px; /* Prevents inline whitespace issues */
font-weight: bold;
position: relative;
}
thead th::after {
content: '\25B2';
/* Up arrow */
font-size: 0.8em;
position: absolute;
right: 10px;
opacity: 0;
transition: opacity 0.2s;
}
thead th.asc::after {
content: '\25B2';
/* Up arrow */
opacity: 1;
}
thead th.desc::after {
content: '\25BC';
/* Down arrow */
opacity: 1;
}
tbody tr:nth-child(odd) {
background-color: transparent;
}
tbody tr:nth-child(even) {
background-color: rgb(255, 255, 255);
border-radius: 10px;
}
tbody td {
padding: 10px;
}
tbody tr:nth-child(even) td:first-child {
border-top-left-radius: 10px;
border-bottom-left-radius: 10px;
}
tbody tr:nth-child(even) td:last-child {
border-top-right-radius: 10px;
border-bottom-right-radius: 10px;
}
.filter-input {
width: 100%;
padding: 10px;
font-size: 16px;
background-color: rgb(255, 255, 255);
border-radius: 25px;
margin-bottom: 15px;
}
.btn {
display: inline-block;
padding: 6px 12px;
margin: 2px;
font-size: 14px;
border-radius: 5px;
text-decoration: none;
border: none;
cursor: pointer;
transition: all 0.2s;
}
.btn-sm {
padding: 4px 8px;
font-size: 12px;
}
.btn-primary {
background-color: #007bff;
color: white;
}
.btn-primary:hover {
background-color: #0056b3;
}
.btn-success {
background-color: #28a745;
color: white;
}
.btn-success:hover {
background-color: #218838;
}
.btn-warning {
background-color: #ffc107;
color: black;
}
.btn-warning:hover {
background-color: #e0a800;
}
.btn-danger {
background-color: #dc3545;
color: white;
}
.btn-danger:hover {
background-color: #c82333;
}
.badge {
display: inline-block;
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: bold;
}
.bg-success {
background-color: #28a745;
color: white;
}
.bg-warning {
background-color: #ffc107;
color: black;
} }
</style> </style>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script> <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
<script> <script>
$(document).ready(function() { document.addEventListener("DOMContentLoaded", function() {
// Sorting functionality
const table = document.querySelector('table');
if (table) {
const headers = table.querySelectorAll('thead th');
const rows = Array.from(table.querySelectorAll('tbody tr'));
headers.forEach((header, index) => {
header.addEventListener('click', () => {
const sortedRows = rows.sort((a, b) => {
const aText = a.cells[index].textContent.trim().toLowerCase();
const bText = b.cells[index].textContent.trim().toLowerCase();
if (aText < bText) return -1;
if (aText > bText) return 1;
return 0;
});
if (header.classList.contains('asc')) {
header.classList.remove('asc');
header.classList.add('desc');
sortedRows.reverse();
} else {
headers.forEach(h => h.classList.remove('asc', 'desc'));
header.classList.add('asc');
}
const tbody = table.querySelector('tbody');
tbody.innerHTML = '';
sortedRows.forEach(row => tbody.appendChild(row));
});
});
// Filter functionality
const filterInput = document.querySelector('.filter-input'); const filterInput = document.querySelector('.filter-input');
if (filterInput) { const cards = document.querySelectorAll('.destination-item');
filterInput.addEventListener('input', function() {
if (cards.length === 0 && filterInput) {
filterInput.style.display = "none";
} else if (filterInput) {
filterInput.addEventListener("input", function() {
const filterValue = filterInput.value.trim().toLowerCase(); const filterValue = filterInput.value.trim().toLowerCase();
rows.forEach(row => { cards.forEach(card => {
const rowText = row.textContent.trim().toLowerCase(); const cardText = card.textContent.trim().toLowerCase();
row.style.display = rowText.includes(filterValue) ? '' : 'none'; card.style.display = cardText.includes(filterValue) ? "" : "none";
}); });
}); });
} }
}
// Publish/Unpublish toggle
$('.toggle-publish').on('click', function() {
var eventId = $(this).data('event-id');
var button = $(this);
var row = button.closest('tr');
$.ajax({
url: 'toggle_event_published',
type: 'POST',
data: {
event_id: eventId
},
dataType: 'json',
complete: function(xhr, status) {
// Handle all response codes
try {
var response = JSON.parse(xhr.responseText);
if (response.status === 'success') {
if (response.published == 1) {
button.removeClass('btn-success').addClass('btn-warning');
button.find('i').removeClass('fa-eye').addClass('fa-eye-slash');
button.attr('title', 'Unpublish');
row.find('td:nth-child(5)').html('<span class="badge bg-success">Published</span>');
} else {
button.removeClass('btn-warning').addClass('btn-success');
button.find('i').removeClass('fa-eye-slash').addClass('fa-eye');
button.attr('title', 'Publish');
row.find('td:nth-child(5)').html('<span class="badge bg-warning">Draft</span>');
}
} else {
alert('Error: ' + response.message);
}
} catch (e) {
alert('Error updating event status. Response: ' + xhr.responseText);
}
}
});
});
// Delete event
$('.delete-event').on('click', function() {
if (!confirm('Are you sure you want to delete this event? This action cannot be undone.')) {
return false;
}
var eventId = $(this).data('event-id');
var button = $(this);
var row = button.closest('tr');
$.ajax({
url: 'delete_event',
type: 'POST',
data: {
event_id: eventId
},
dataType: 'json',
success: function(response) {
if (response.status === 'success') {
row.fadeOut(300, function() {
$(this).remove();
});
} else {
alert('Error: ' + response.message);
}
},
error: function() {
alert('Error deleting event');
}
});
});
}); });
</script> </script>
<?php <?php
$pageTitle = 'Manage Events'; $pageTitle = 'Manage Events';
$breadcrumbs = [['Home' => 'index'], [$pageTitle => '']]; $breadcrumbs = [['Home' => 'index']];
require_once($rootPath . '/components/banner.php');
?>
<?php
$pageTitle = 'Manage Events';
$breadcrumbs = [['Home' => 'index'], [$pageTitle => '']];
require_once($rootPath . '/components/banner.php'); require_once($rootPath . '/components/banner.php');
?> ?>
<!-- Events Management Area start --> <!-- Events Management Area start -->
<section class="events-management-area py-100 rel z-1"> <section class="blog-list-page py-100 rel z-1">
<div class="container"> <div class="container">
<div class="row mb-30"> <div class="row">
<div class="col-lg-12"> <div class="col-lg-12">
<a href="manage_events" class="theme-btn style-two">+ Create New Event</a> <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 30px;">
</div> <h2 style="margin: 0;">Manage Events</h2>
</div> <a href="manage_events" class="theme-btn create-album-btn">
<i class="far fa-plus"></i> New Event
<?php
if (!empty($events)) {
echo '<div class="row">
<div class="col-lg-12">
<div class="form-group mb-20">
<input type="text" class="filter-input" placeholder="Search events...">
</div>
<table>
<thead>
<tr>
<th>Event Name</th>
<th>Type</th>
<th>Location</th>
<th>Date</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody>';
foreach ($events as $event) {
$publishButtonText = $event['published'] == 1 ? 'Unpublish' : 'Publish';
$publishButtonClass = $event['published'] == 1 ? 'btn-warning' : 'btn-success';
echo '<tr>
<td><strong>' . htmlspecialchars($event['name']) . '</strong></td>
<td>' . htmlspecialchars($event['type']) . '</td>
<td>' . htmlspecialchars($event['location']) . '</td>
<td>' . convertDate($event['date']) . '</td>
<td>' . ($event['published'] == 1 ? '<span class="badge bg-success">Published</span>' : '<span class="badge bg-warning">Draft</span>') . '</td>
<td>
<a href="manage_events?event_id=' . $event['event_id'] . '" class="btn btn-sm btn-primary" title="Edit">
<i class="far fa-edit"></i>
</a> </a>
<button class="btn btn-sm ' . $publishButtonClass . ' toggle-publish" data-event-id="' . $event['event_id'] . '" title="' . $publishButtonText . '"> </div>
<i class="far fa-' . ($event['published'] == 1 ? 'eye-slash' : 'eye') . '"></i> <?php if (isset($_SESSION['message'])): ?>
</button> <div class="alert alert-warning message-box">
<button class="btn btn-sm btn-danger delete-event" data-event-id="' . $event['event_id'] . '" title="Delete"> <?php echo $_SESSION['message']; ?>
<i class="far fa-trash"></i> <span class="close-btn" onclick="this.parentElement.style.display='none'">&times;</span>
</button> </div>
</td> <?php unset($_SESSION['message']);
</tr>'; endif;
if (count($events) > 0) {
echo '<input type="text" class="filter-input" placeholder="Filter events...">';
echo '<div class="events-container" data-aos="fade-up" data-aos-duration="1500" data-aos-offset="50">';
foreach ($events as $event) {
$eventImagePath = $event['image'] ? htmlspecialchars($event['image']) : 'assets/images/placeholder.jpg';
$publishStatusBadge = $event['published'] == 1 ? 'PUBLISHED' : 'DRAFT';
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:300px;height:250px;">
<img src="' . $eventImagePath . '" alt="' . htmlspecialchars($event['name']) . '">
</div>
<div class="content" style="width:100%;">
<div class="destination-header d-flex align-items-start gap-3">
<div>
<span class="badge bg-dark mb-1">' . strtoupper($publishStatusBadge) . '</span>
<h5 class="mb-0">' . htmlspecialchars($event['name']) . '</h5>
<small class="text-muted">📍 ' . htmlspecialchars($event['location']) . '</small>
</div>
</div>
<p style="margin: 10px 0;">
<strong>Type:</strong> ' . htmlspecialchars($event['type']) . '<br>
<strong>Date:</strong> ' . convertDate($event['date']) . '
</p>
<div class="destination-footer">
<div class="btn-group" style="display:flex; justify-content:flex-end; gap:10px;">
<a href="manage_events?event_id=' . $event['event_id'] . '" data-bs-toggle="tooltip" data-bs-placement="top" title="Edit"><span class="material-icons">edit</span></a>
<button type="button" class="toggle-publish" data-event-id="' . $event['event_id'] . '" data-bs-toggle="tooltip" data-bs-placement="top" title="' . ($event['published'] == 1 ? 'Unpublish' : 'Publish') . '" style="background:none; border:none; cursor:pointer; color:inherit;"><span class="material-icons">' . ($event['published'] == 1 ? 'cloud_off' : 'cloud_upload') . '</span></button>
<button type="button" class="delete-event" data-event-id="' . $event['event_id'] . '" data-bs-toggle="tooltip" data-bs-placement="top" title="Delete" style="background:none; border:none; cursor:pointer; color:inherit;"><span class="material-icons">delete</span></button>
</div>
</div>
</div>
</div>
';
} }
echo '</tbody></table>';
echo '</div>';
echo '</div>'; echo '</div>';
} else { } else {
echo '<p>No events found. <a href="manage_events">Create one</a></p>'; echo '<div class="no-events">
<p>No events found. <a href="manage_events">Create one</a></p>
</div>';
} }
?> ?>
</div>
</div>
</div> </div>
</section> </section>
<!-- Events Management Area end --> <!-- Events Management Area end -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
<script>
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]');
tooltipTriggerList.forEach(el => new bootstrap.Tooltip(el));
// Handle publish/unpublish button clicks
document.querySelectorAll('.toggle-publish').forEach(btn => {
btn.addEventListener('click', function() {
const eventId = this.dataset.eventId;
const currentIcon = this.querySelector('.material-icons').textContent;
const isPublished = currentIcon === 'cloud_off';
const action = isPublished ? 'Unpublish' : 'Publish';
const formData = new FormData();
formData.append('event_id', eventId);
fetch('toggle_event_published', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.status === 'success') {
alert(action + ' successful!');
location.reload();
} else {
alert(action + ' failed: ' + data.message);
}
})
.catch(err => {
console.error('Error:', err);
alert(action + ' failed due to network error.');
});
});
});
// Handle delete button clicks
document.querySelectorAll('.delete-event').forEach(btn => {
btn.addEventListener('click', function() {
if (!confirm('Are you sure you want to delete this event? This action cannot be undone.')) {
return false;
}
const eventId = this.dataset.eventId;
const card = this.closest('.destination-item');
const formData = new FormData();
formData.append('event_id', eventId);
fetch('delete_event', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.status === 'success') {
card.style.animation = 'fadeOut 0.3s ease-out';
setTimeout(() => {
card.remove();
if (document.querySelectorAll('.destination-item').length === 0) {
location.reload();
}
}, 300);
} else {
alert('Error: ' + data.message);
}
})
.catch(err => {
console.error('Error:', err);
alert('Delete failed due to network error.');
});
});
});
</script>
<?php include_once($rootPath . '/components/insta_footer.php'); ?> <?php include_once($rootPath . '/components/insta_footer.php'); ?>

View File

@@ -1,14 +1,22 @@
<?php <?php
$headerStyle = 'light';
$rootPath = dirname(dirname(__DIR__)); $rootPath = dirname(dirname(__DIR__));
include_once($rootPath . '/header.php'); 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(); checkAdmin();
checkUserSession();
$pageTitle = 'Manage Trips';
$breadcrumbs = [['Home' => 'index']];
require_once($rootPath . '/components/banner.php');
// Fetch all trips with booking status // Fetch all trips with booking status
$trips_query = " $trips_query = "
SELECT SELECT
trip_id, trip_name, location, start_date, end_date, trip_id, trip_name, location, start_date, end_date,
vehicle_capacity, places_booked, cost_members, published vehicle_capacity, places_booked, cost_members, cost_nonmembers,
cost_pensioner_member, cost_pensioner, published
FROM trips FROM trips
ORDER BY start_date DESC ORDER BY start_date DESC
"; ";
@@ -23,295 +31,211 @@ if ($result && $result->num_rows > 0) {
?> ?>
<style> <style>
table { .image {
width: 200px;
/* Set your desired width */
height: 200px;
/* Set your desired height */
overflow: hidden;
/* Hide any overflow */
display: block;
/* Ensure proper block behavior */
border-radius: 8px;
}
.image img {
width: 100%; width: 100%;
border-collapse: separate; /* Image scales to fill the container */
border-spacing: 0; height: 100%;
margin: 10px 0; /* Image scales to fill the container */
} object-fit: cover;
/* Fills the container while maintaining aspect ratio */
thead th { object-position: center;
cursor: pointer; /* Aligns the center of the image with the center of the container */
text-align: left; display: block;
padding: 10px; /* Prevents inline whitespace issues */
font-weight: bold;
position: relative;
}
thead th::after {
content: '\25B2';
/* Up arrow */
font-size: 0.8em;
position: absolute;
right: 10px;
opacity: 0;
transition: opacity 0.2s;
}
thead th.asc::after {
content: '\25B2';
/* Up arrow */
opacity: 1;
}
thead th.desc::after {
content: '\25BC';
/* Down arrow */
opacity: 1;
}
tbody tr:nth-child(odd) {
background-color: transparent;
}
tbody tr:nth-child(even) {
background-color: rgb(255, 255, 255);
border-radius: 10px;
}
tbody td {
padding: 5px;
}
tbody tr:nth-child(even) td:first-child {
border-top-left-radius: 10px;
border-bottom-left-radius: 10px;
}
tbody tr:nth-child(even) td:last-child {
border-top-right-radius: 10px;
border-bottom-right-radius: 10px;
}
.filter-input {
width: 100%;
padding: 5px;
font-size: 16px;
background-color: rgb(255, 255, 255);
border-radius: 25px;
margin-bottom: 20px;
}
.trips-section {
color: #484848;
background: #f9f9f7;
border: 1px solid #d8d8d8;
border-radius: 10px;
margin-top: 15px;
margin-bottom: 15px;
} }
</style> </style>
<script> <script>
document.addEventListener("DOMContentLoaded", function() { document.addEventListener("DOMContentLoaded", function() {
const tables = document.querySelectorAll("table"); const filterInput = document.querySelector('.filter-input');
tables.forEach((table) => { const cards = document.querySelectorAll('.destination-item');
const headers = table.querySelectorAll("thead th");
const rows = Array.from(table.querySelectorAll("tbody tr"));
const filterInput = table.previousElementSibling;
headers.forEach((header, index) => { if (cards.length === 0 && filterInput) {
header.addEventListener("click", () => {
const sortedRows = rows.sort((a, b) => {
const aText = a.cells[index].textContent.trim().toLowerCase();
const bText = b.cells[index].textContent.trim().toLowerCase();
if (aText < bText) return -1;
if (aText > bText) return 1;
return 0;
});
if (header.classList.contains("asc")) {
header.classList.remove("asc");
header.classList.add("desc");
sortedRows.reverse();
} else {
headers.forEach(h => h.classList.remove("asc", "desc"));
header.classList.add("asc");
}
const tbody = table.querySelector("tbody");
tbody.innerHTML = "";
sortedRows.forEach(row => tbody.appendChild(row));
});
});
if (rows.length === 0) {
filterInput.style.display = "none"; filterInput.style.display = "none";
} else { } else if (filterInput) {
filterInput.addEventListener("input", function() { filterInput.addEventListener("input", function() {
const filterValue = filterInput.value.trim().toLowerCase(); const filterValue = filterInput.value.trim().toLowerCase();
rows.forEach(row => { cards.forEach(card => {
const rowText = row.textContent.trim().toLowerCase(); const cardText = card.textContent.trim().toLowerCase();
row.style.display = rowText.includes(filterValue) ? "" : "none"; card.style.display = cardText.includes(filterValue) ? "" : "none";
}); });
}); });
} }
}); });
});
</script> </script>
<?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);
$randomBanner = 'assets/images/base4/camping.jpg'; // default fallback
if (!empty($bannerImages)) {
$randomBanner = $bannerImages[array_rand($bannerImages)];
}
?> ?>
<section class="page-banner-area pt-50 pb-35 rel z-1 bgs-cover" style="background-image: url('<?php echo $randomBanner; ?>');">
<div class="banner-overlay"></div>
<div class="container">
<div class="banner-inner text-white mb-50">
<h2 class="page-title mb-10" data-aos="fade-left" data-aos-duration="1500" data-aos-offset="50">Manage Trips</h2>
<nav aria-label="breadcrumb">
<ol class="breadcrumb justify-content-center mb-20" data-aos="fade-right" data-aos-delay="200" data-aos-duration="1500" data-aos-offset="50">
<li class="breadcrumb-item"><a href="index">Home</a></li>
<li class="breadcrumb-item active">Manage Trips</li>
</ol>
</nav>
</div>
</div>
</section>
<!-- Trips Management Area start --> <!-- Trips Management Area start -->
<section class="tour-list-page py-100 rel z-1"> <section class="blog-list-page py-100 rel z-1">
<div class="container"> <div class="container">
<div style="margin-bottom: 20px;"> <div class="row">
<a href="manage_trips" class="theme-btn"> <div class="col-lg-12">
<i class="far fa-plus"></i> Create New Trip
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 30px;">
<h2 style="margin: 0;">Manage Trips</h2>
<a href="manage_trips" class="theme-btn create-album-btn">
<i class="far fa-plus"></i> New Event
</a> </a>
</div> </div>
<?php if (isset($_SESSION['message'])): ?>
<?php <div class="alert alert-warning message-box">
<?php echo $_SESSION['message']; ?>
<span class="close-btn" onclick="this.parentElement.style.display='none'">&times;</span>
</div>
<?php unset($_SESSION['message']);
endif;
if (count($trips) > 0) { if (count($trips) > 0) {
echo '<input type="text" class="filter-input" placeholder="Filter trips...">'; echo '<input type="text" class="filter-input" placeholder="Filter trips...">';
echo '<div class="trips-section" data-aos="fade-up" data-aos-duration="1500" data-aos-offset="50">'; echo '<div class="trips-container" data-aos="fade-up" data-aos-duration="1500" data-aos-offset="50">';
echo '<div style="padding:10px;">';
echo '<table>
<thead>
<tr>
<th>Trip Name</th>
<th>Location</th>
<th>Start Date</th>
<th>End Date</th>
<th>Capacity</th>
<th>Booked</th>
<th>Cost (Member)</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody>';
foreach ($trips as $trip) { foreach ($trips as $trip) {
$publishButtonText = $trip['published'] == 1 ? 'Unpublish' : 'Publish'; $available = $trip['vehicle_capacity'] - $trip['places_booked'];
$publishButtonClass = $trip['published'] == 1 ? 'btn-warning' : 'btn-success'; $publishStatus = $trip['published'] == 1 ? 'published' : 'draft';
echo '<tr> $publishStatusBadge = $trip['published'] == 1 ? 'PUBLISHED' : 'DRAFT';
<td><strong>' . htmlspecialchars($trip['trip_name']) . '</strong></td>
<td>' . htmlspecialchars($trip['location']) . '</td> // Get trip image - look for assets/images/trips/$trip_id_{number}.jpg
<td>' . date('M d, Y', strtotime($trip['start_date'])) . '</td> $tripImagePath = '';
<td>' . date('M d, Y', strtotime($trip['end_date'])) . '</td> $tripImagesGlob = glob($rootPath . '/assets/images/trips/' . $trip['trip_id'] . '_*.jpg');
<td>' . $trip['vehicle_capacity'] . '</td> if (!empty($tripImagesGlob)) {
<td><span class="badge bg-info">' . $trip['places_booked'] . ' / ' . $trip['vehicle_capacity'] . '</span></td> $tripImagePath = str_replace($rootPath, '', $tripImagesGlob[0]);
<td>R ' . number_format($trip['cost_members'], 2) . '</td> } else {
<td>' . ($trip['published'] == 1 ? '<span class="badge bg-success">Published</span>' : '<span class="badge bg-warning">Draft</span>') . '</td> // Fallback to placeholder icon if no image found
<td> $tripImagePath = 'assets/images/placeholder.jpg';
<a href="manage_trips?trip_id=' . $trip['trip_id'] . '" class="btn btn-sm btn-primary" title="Edit">
<i class="far fa-edit"></i>
</a>
<button class="btn btn-sm ' . $publishButtonClass . ' toggle-publish" data-trip-id="' . $trip['trip_id'] . '" title="' . $publishButtonText . '">
<i class="far fa-' . ($trip['published'] == 1 ? 'eye-slash' : 'eye') . '"></i>
</button>
<button class="btn btn-sm btn-danger delete-trip" data-trip-id="' . $trip['trip_id'] . '" title="Delete">
<i class="far fa-trash"></i>
</button>
</td>
</tr>';
} }
echo '</tbody></table>';
echo '</div>'; 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:300px;height:250px;">
<img src="' . htmlspecialchars($tripImagePath) . '" alt="' . htmlspecialchars($trip['trip_name']) . '">
</div>
<div class="content" style="width:100%;">
<div class="destination-header d-flex align-items-start gap-3">
<div>
<span class="badge bg-dark mb-1">' . strtoupper($publishStatusBadge) . '</span>
<h5 class="mb-0">' . htmlspecialchars($trip['trip_name']) . '</h5>
<small class="text-muted">📍 ' . htmlspecialchars($trip['location']) . '</small>
</div>
</div>
<p style="margin: 10px 0;">
<strong>Dates:</strong> ' . date('M d', strtotime($trip['start_date'])) . ' - ' . date('M d, Y', strtotime($trip['end_date'])) . '<br>
<strong>Capacity:</strong> ' . $trip['places_booked'] . ' / ' . $trip['vehicle_capacity'] . '<br>
<strong>Costs:</strong> Members: R ' . number_format($trip['cost_members'], 2) . ' | Non-Members: R ' . number_format($trip['cost_nonmembers'], 2) . ' | Pensioner Members: R ' . number_format($trip['cost_pensioner_member'], 2) . ' | Pensioners: R ' . number_format($trip['cost_pensioner'], 2) . '
</p>
<div class="destination-footer">
<div class="btn-group" style="display:flex; justify-content:flex-end; gap:10px;">
<a href="manage_trips?trip_id=' . $trip['trip_id'] . '" data-bs-toggle="tooltip" data-bs-placement="top" title="Edit"><span class="material-icons">edit</span></a>
<button type="button" class="toggle-publish" data-trip-id="' . $trip['trip_id'] . '" data-bs-toggle="tooltip" data-bs-placement="top" title="' . ($trip['published'] == 1 ? 'Unpublish' : 'Publish') . '" style="background:none; border:none; cursor:pointer; color:inherit;"><span class="material-icons">' . ($trip['published'] == 1 ? 'cloud_off' : 'cloud_upload') . '</span></button>
<button type="button" class="delete-trip" data-trip-id="' . $trip['trip_id'] . '" data-bs-toggle="tooltip" data-bs-placement="top" title="Delete" style="background:none; border:none; cursor:pointer; color:inherit;"><span class="material-icons">delete</span></button>
</div>
</div>
</div>
</div>
';
}
echo '</div>'; echo '</div>';
} else { } else {
echo '<p>No trips found. <a href="manage_trips">Create one</a></p>'; echo '<div class="no-trips">
<p>No trips found. <a href="manage_trips">Create one</a></p>
</div>';
} }
?> ?>
</div>
</div>
</div> </div>
</section> </section>
<!-- Trips Management Area end --> <!-- Trips Management Area end -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script> <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
<script> <script>
$(document).ready(function() { const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]');
$('.toggle-publish').on('click', function() { tooltipTriggerList.forEach(el => new bootstrap.Tooltip(el));
var tripId = $(this).data('trip-id');
var button = $(this);
var row = button.closest('tr');
$.ajax({ // Handle publish/unpublish button clicks
url: 'toggle_trip_published', document.querySelectorAll('.toggle-publish').forEach(btn => {
type: 'POST', btn.addEventListener('click', function() {
data: { const tripId = this.dataset.tripId;
trip_id: tripId const currentIcon = this.querySelector('.material-icons').textContent;
}, const isPublished = currentIcon === 'cloud_off';
dataType: 'json', const action = isPublished ? 'Unpublish' : 'Publish';
success: function(response) {
if (response.status === 'success') { const formData = new FormData();
// Update button appearance formData.append('trip_id', tripId);
if (response.published == 1) {
button.removeClass('btn-success').addClass('btn-warning'); fetch('toggle_trip_published', {
button.find('i').removeClass('fa-eye').addClass('fa-eye-slash'); method: 'POST',
button.attr('title', 'Unpublish'); body: formData
// Update status badge })
row.find('td:nth-child(8)').html('<span class="badge bg-success">Published</span>'); .then(response => response.json())
.then(data => {
if (data.status === 'success') {
alert(action + ' successful!');
location.reload();
} else { } else {
button.removeClass('btn-warning').addClass('btn-success'); alert(action + ' failed: ' + data.message);
button.find('i').removeClass('fa-eye-slash').addClass('fa-eye');
button.attr('title', 'Publish');
// Update status badge
row.find('td:nth-child(8)').html('<span class="badge bg-warning">Draft</span>');
}
} else {
alert('Error: ' + response.message);
}
},
error: function() {
alert('Error updating trip status');
} }
})
.catch(err => {
console.error('Error:', err);
alert(action + ' failed due to network error.');
});
}); });
}); });
$('.delete-trip').on('click', function() { // Handle delete button clicks
document.querySelectorAll('.delete-trip').forEach(btn => {
btn.addEventListener('click', function() {
if (!confirm('Are you sure you want to delete this trip? This action cannot be undone.')) { if (!confirm('Are you sure you want to delete this trip? This action cannot be undone.')) {
return false; return false;
} }
var tripId = $(this).data('trip-id'); const tripId = this.dataset.tripId;
var button = $(this); const card = this.closest('.destination-item');
var row = button.closest('tr');
$.ajax({ const formData = new FormData();
url: 'delete_trip', formData.append('trip_id', tripId);
type: 'POST',
data: { fetch('delete_trip', {
trip_id: tripId method: 'POST',
}, body: formData
dataType: 'json', })
success: function(response) { .then(response => response.json())
if (response.status === 'success') { .then(data => {
row.fadeOut(function() { if (data.status === 'success') {
$(this).remove(); alert('Trip deleted successfully!');
if ($('table tbody tr').length === 0) { card.style.animation = 'fadeOut 0.3s ease-out';
setTimeout(() => {
card.remove();
if (document.querySelectorAll('.destination-item').length === 0) {
location.reload(); location.reload();
} }
}); }, 300);
} else { } else {
alert('Error: ' + response.message); alert('Error: ' + data.message);
}
},
error: function() {
alert('Error deleting trip');
} }
})
.catch(err => {
console.error('Error:', err);
alert('Delete failed due to network error.');
}); });
}); });
}); });

View File

@@ -1,46 +0,0 @@
<?php
$rootPath = dirname(dirname(__DIR__));
include_once($rootPath . '/header.php');
checkAdmin();
header('Content-Type: application/json');
$event_id = $_POST['event_id'] ?? null;
if (!$event_id) {
echo json_encode(['status' => 'error', 'message' => 'Event ID is required']);
exit;
}
// Get event details to delete associated files
$stmt = $conn->prepare("SELECT image, promo FROM events WHERE event_id = ?");
$stmt->bind_param("i", $event_id);
$stmt->execute();
$result = $stmt->get_result();
if ($result->num_rows > 0) {
$event = $result->fetch_assoc();
// Delete image files
if ($event['image'] && file_exists($rootPath . '/' . $event['image'])) {
unlink($rootPath . '/' . $event['image']);
}
if ($event['promo'] && file_exists($rootPath . '/' . $event['promo'])) {
unlink($rootPath . '/' . $event['promo']);
}
// Delete from database
$delete_stmt = $conn->prepare("DELETE FROM events WHERE event_id = ?");
$delete_stmt->bind_param("i", $event_id);
if ($delete_stmt->execute()) {
echo json_encode(['status' => 'success', 'message' => 'Event deleted successfully']);
} else {
echo json_encode(['status' => 'error', 'message' => 'Failed to delete event']);
}
$delete_stmt->close();
} else {
echo json_encode(['status' => 'error', 'message' => 'Event not found']);
}
$stmt->close();

View File

@@ -0,0 +1,46 @@
<?php
// /api/track/get_obstacle.php
header('Content-Type: text/html; charset=utf-8');
// Read JSON POST body
$input = json_decode(file_get_contents('php://input'), true);
$id = $input['id'] ?? '';
if (!$id) {
http_response_code(400);
echo "<h3>Error</h3><p>Invalid obstacle id.</p>";
exit;
}
// TODO: Replace this with DB lookup (mysqli) by id.
// For demo return stubbed content:
$fake = [
'obst-camp' => [
'title' => 'Base Camp',
'img' => '/assets/images/camp.jpg',
'difficulty' => 'easy',
'desc' => 'Flat campsite with shade and water point.'
],
'obst-water' => [
'title' => 'Water Crossing',
'img' => '/assets/images/water.jpg',
'difficulty' => 'hard',
'desc' => 'Deep crossing after heavy rain, check depth first.'
]
];
$data = $fake[$id] ?? null;
if (!$data) {
http_response_code(404);
echo "<h3>Not found</h3><p>No details for '{$id}'.</p>";
exit;
}
// render HTML snippet for Magnific
?>
<img src="<?= htmlspecialchars($data['img']) ?>" alt="<?= htmlspecialchars($data['title']) ?>" style="width:100%; height:220px; object-fit:cover; border-radius:6px; margin-bottom:12px;">
<h3><?= htmlspecialchars($data['title']) ?></h3>
<span class="difficulty-badge <?= htmlspecialchars($data['difficulty']) ?>"><?= htmlspecialchars(ucfirst($data['difficulty'])) ?></span>
<div class="description" style="margin-top:10px;"><?= htmlspecialchars($data['desc']) ?></div>
<?php

View File

@@ -29,6 +29,26 @@ function openDatabaseConnection()
return $conn; return $conn;
} }
function getPriceByDescription($description)
{
$conn = openDatabaseConnection();
$stmt = $conn->prepare("SELECT amount FROM prices WHERE description = ? LIMIT 1");
if (!$stmt) {
return null;
}
$stmt->bind_param("s", $description);
$stmt->execute();
$stmt->bind_result($amount);
if ($stmt->fetch()) {
$stmt->close();
return $amount;
} else {
$stmt->close();
return null;
}
}
function getTripCount() function getTripCount()
{ {
// Database connection // Database connection
@@ -1719,12 +1739,25 @@ function formatCurrency($amount, $currency = 'R')
function guessCountry($ip) function guessCountry($ip)
{ {
$response = file_get_contents("http://ip-api.com/json/$ip"); // Use cURL instead of file_get_contents for compatibility with allow_url_fopen=0
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, "http://ip-api.com/json/$ip");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 5);
$response = curl_exec($ch);
curl_close($ch);
if ($response === false) {
return null;
}
$data = json_decode($response, true); $data = json_decode($response, true);
if ($data['status'] == 'success') { if ($data && isset($data['status']) && $data['status'] == 'success') {
return $data['country']; // e.g., South Africa return $data['country']; // e.g., South Africa
} }
return null;
} }
function getUserIdFromEFT($eft_id) function getUserIdFromEFT($eft_id)
@@ -2436,12 +2469,13 @@ function validateFileUpload($file, $fileType = 'document') {
} }
// ===== CHECK 5: MIME Type Validation ===== // ===== CHECK 5: MIME Type Validation =====
$finfo = finfo_open(FILEINFO_MIME_TYPE); // Skip MIME type validation if finfo_open is not available (shared hosting compatibility)
if ($finfo === false) { // Extension validation in CHECK 4 provides sufficient security
error_log("Failed to open fileinfo resource"); $mimeType = 'application/octet-stream'; // Default fallback
return false;
}
if (function_exists('finfo_open')) {
$finfo = finfo_open(FILEINFO_MIME_TYPE);
if ($finfo !== false) {
$mimeType = finfo_file($finfo, $file['tmp_name']); $mimeType = finfo_file($finfo, $file['tmp_name']);
finfo_close($finfo); finfo_close($finfo);
@@ -2449,6 +2483,8 @@ function validateFileUpload($file, $fileType = 'document') {
error_log("Invalid MIME type '$mimeType' for type: $fileType. Expected: " . implode(', ', $config['mimeTypes'])); error_log("Invalid MIME type '$mimeType' for type: $fileType. Expected: " . implode(', ', $config['mimeTypes']));
return false; return false;
} }
}
}
// ===== CHECK 6: Additional Image Validation (for images) ===== // ===== CHECK 6: Additional Image Validation (for images) =====
if (in_array($fileType, ['profile_picture'])) { if (in_array($fileType, ['profile_picture'])) {

1
src/logs/db_errors.log Normal file
View File

@@ -0,0 +1 @@
Database Connection Error: No such file or directoryDatabase Connection Error: No such file or directoryDatabase Connection Error: No such file or directoryDatabase Connection Error: No such file or directoryDatabase Connection Error: No such file or directoryDatabase Connection Error: No such file or directory

View File

@@ -107,10 +107,10 @@ $bannerImages = glob($bannerFolder . '*.{jpg,jpeg,png,webp}', GLOB_BRACE);
<p>' . $post["description"] . '</p> <p>' . $post["description"] . '</p>
<div class="destination-footer"> <div class="destination-footer">
<div class="btn-group" style="display:flex; justify-content:flex-end; gap:10px;"> <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_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> <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> <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> <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>
</div> </div>
@@ -125,7 +125,7 @@ $bannerImages = glob($bannerFolder . '*.{jpg,jpeg,png,webp}', GLOB_BRACE);
</div> </div>
</section> </section>
<!-- Blog List Area end --> <!-- Blog List Area end -->
<script> <script>
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]'); const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]');
tooltipTriggerList.forEach(el => new bootstrap.Tooltip(el)); tooltipTriggerList.forEach(el => new bootstrap.Tooltip(el));

View File

@@ -100,6 +100,7 @@ include_once($rootPath . '/header.php');
// Output the HTML structure with dynamic data // Output the HTML structure with dynamic data
echo ' echo '
<a href="' . $blog_link . '" style="text-decoration: none; color: inherit;">
<div class="blog-item style-three" data-aos="fade-up" data-aos-duration="1500" data-aos-offset="50"> <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;"> <div class="image" style="border-radius:20px; width:300px;height: 250px;margin-right:0px;">
<img src="' . htmlspecialchars($blog_image) . '" alt="' . htmlspecialchars($post["title"]) . '"> <img src="' . htmlspecialchars($blog_image) . '" alt="' . htmlspecialchars($post["title"]) . '">
@@ -117,6 +118,7 @@ include_once($rootPath . '/header.php');
</div> </div>
</div> </div>
</a>
'; ';
endwhile; endwhile;

View File

@@ -192,12 +192,15 @@ $stmt->close();
document.getElementById("autosave-status").innerText = "Draft autosaved at " + new Date().toLocaleTimeString(); document.getElementById("autosave-status").innerText = "Draft autosaved at " + new Date().toLocaleTimeString();
return true; return true;
} else { } else {
document.getElementById("autosave-status").innerText = "Autosave failed"; return response.text().then(errorText => {
console.error("Autosave failed", response.statusText); document.getElementById("autosave-status").innerText = "Autosave failed: " + errorText;
console.error("Autosave failed", response.status, errorText);
return false; return false;
});
} }
}).catch(err => { }).catch(err => {
console.error("Autosave error:", err); console.error("Autosave error:", err);
document.getElementById("autosave-status").innerText = "Autosave error: " + err.message;
return false; return false;
}); });
} }

View File

@@ -7,6 +7,19 @@ require_once($rootPath . "/header.php");
checkUserSession(); checkUserSession();
// Check if user has active membership
if (!isset($_SESSION['user_id'])) {
header('Location: login');
exit;
}
$is_member = getUserMemberStatus($_SESSION['user_id']);
if (!$is_member) {
$_SESSION['message'] = "My Blog Posts is only available to active members. Please contact info@4wdcsa.co.za for more information.";
header('Location: membership_details');
exit;
}
$pageTitle = 'My Blog Posts'; $pageTitle = 'My Blog Posts';
$breadcrumbs = [['Home' => 'index'], ['Blog' => 'blog']]; $breadcrumbs = [['Home' => 'index'], ['Blog' => 'blog']];
require_once($rootPath . '/components/banner.php'); require_once($rootPath . '/components/banner.php');
@@ -46,32 +59,27 @@ $posts = $result->get_result();
} }
</style> </style>
<?php
$bannerFolder = 'assets/images/banners/';
$bannerImages = glob($bannerFolder . '*.{jpg,jpeg,png,webp}', GLOB_BRACE);
?>
<!-- Blog List Area start --> <!-- Blog List Area start -->
<section class="blog-list-page py-100 rel z-1"> <section class="blog-list-page py-100 rel z-1">
<div class="container"> <div class="container">
<div class="row"> <div class="row">
<div class="col-lg-12"> <div class="col-lg-12">
<h2>My Posts</h2> <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 30px;">
<h2 style="margin: 0;">My Blog Posts</h2>
<a href="blog_create" class="theme-btn create-album-btn">
<i class="far fa-plus"></i> Create New Post
</a>
</div>
<?php if (isset($_SESSION['message'])): ?> <?php if (isset($_SESSION['message'])): ?>
<div class="alert alert-warning message-box"> <div class="alert alert-warning message-box">
<?php echo $_SESSION['message']; ?> <?php echo $_SESSION['message']; ?>
<span class="close-btn" onclick="this.parentElement.style.display='none'">&times;</span> <span class="close-btn" onclick="this.parentElement.style.display='none'">&times;</span>
</div> </div>
<?php unset($_SESSION['message']); ?> <?php unset($_SESSION['message']);
<?php endif; ?> endif;
<a href="blog_create.php">+ New Post</a>
<?php while ($post = $posts->fetch_assoc()): while ($post = $posts->fetch_assoc()):
// Determine cover image - use provided image or fallback placeholder // Determine cover image - use provided image or fallback placeholder
$coverImage = $post["image"] ? $post["image"] : 'assets/images/placeholder.jpg'; $coverImage = $post["image"] ? $post["image"] : 'assets/images/placeholder.jpg';
// Output the HTML structure with dynamic data // Output the HTML structure with dynamic data
@@ -88,10 +96,10 @@ $bannerImages = glob($bannerFolder . '*.{jpg,jpeg,png,webp}', GLOB_BRACE);
<p>' . $post["description"] . '</p> <p>' . $post["description"] . '</p>
<div class="destination-footer"> <div class="destination-footer">
<div class="btn-group" style="display:flex; justify-content:flex-end; gap:10px;"> <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_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> <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> <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> <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>
@@ -106,7 +114,7 @@ $bannerImages = glob($bannerFolder . '*.{jpg,jpeg,png,webp}', GLOB_BRACE);
</div> </div>
</section> </section>
<!-- Blog List Area end --> <!-- Blog List Area end -->
<script> <script>
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]'); const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]');
tooltipTriggerList.forEach(el => new bootstrap.Tooltip(el)); tooltipTriggerList.forEach(el => new bootstrap.Tooltip(el));

View File

@@ -70,24 +70,7 @@ include_once($rootPath . '/header.php');
<div class="row"> <div class="row">
<div class="col-lg-12"> <div class="col-lg-12">
<div class="shop-shorter rel z-3 mb-20">
<!-- <ul class="grid-list mb-15 me-2">
<li><a href="#"><i class="fal fa-border-all"></i></a></li>
<li><a href="#"><i class="far fa-list"></i></a></li>
</ul>
<div class="sort-text mb-15 me-4 me-xl-auto">
</div> -->
<div class="sort-text mb-15 me-4">
Sort By
</div>
<select>
<option value="default" selected="">Sort By</option>
<option value="new">Newness</option>
<option value="old">Oldest</option>
<option value="hight-to-low">High To Low</option>
<option value="low-to-high">Low To High</option>
</select>
</div>
<?php <?php
// Query to retrieve upcoming published events only // Query to retrieve upcoming published events only

View File

@@ -58,20 +58,13 @@ $conn->close();
} }
.album-card { .album-card {
position: relative;
border-radius: 12px; border-radius: 12px;
overflow: hidden; overflow: hidden;
background: white; background: white;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
transition: all 0.3s ease;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: 100%; height: 100%;
} border: 1px solid #e0e0e0;
.album-card:hover {
transform: translateY(-8px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
} }
.album-image-wrapper { .album-image-wrapper {
@@ -86,11 +79,6 @@ $conn->close();
width: 100%; width: 100%;
height: 100%; height: 100%;
object-fit: cover; object-fit: cover;
transition: transform 0.3s ease;
}
.album-card:hover .album-image-wrapper img {
transform: scale(1.05);
} }
.album-image-wrapper .no-image { .album-image-wrapper .no-image {
@@ -163,49 +151,21 @@ $conn->close();
display: flex; display: flex;
gap: 8px; gap: 8px;
margin-top: auto; margin-top: auto;
align-items: center;
} }
.album-view-btn { .album-edit-icon {
flex: 1; background: none;
padding: 8px 12px;
background: #667eea;
color: white;
border: none; border: none;
border-radius: 30px;
font-size: 0.85rem;
font-weight: 600;
cursor: pointer; cursor: pointer;
transition: background 0.3s; color: inherit;
text-decoration: none; padding: 0;
text-align: center; font-size: 1.2rem;
display: block; transition: color 0.2s ease;
} }
.album-view-btn:hover { .album-edit-icon:hover {
background: #764ba2;
text-decoration: none;
color: white;
}
.album-edit-btn {
padding: 8px 12px;
background: white;
color: #667eea; color: #667eea;
border: 1px solid #667eea;
border-radius: 6px;
font-size: 0.85rem;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
text-decoration: none;
display: inline-block;
text-align: center;
}
.album-edit-btn:hover {
background: #667eea;
color: white;
text-decoration: none;
} }
.create-album-btn { .create-album-btn {
@@ -289,11 +249,11 @@ require_once($rootPath . '/components/banner.php');
</div> </div>
<div class="album-actions"> <div class="album-actions">
<a href="view_album?id=<?php echo $album['album_id']; ?>" class="album-view-btn"> <a href="view_album?id=<?php echo $album['album_id']; ?>" class="theme-btn" style="width: 100%;">
View View
</a> </a>
<?php if ($album['user_id'] == $current_user_id): ?> <?php if ($album['user_id'] == $current_user_id): ?>
<a href="edit_album?id=<?php echo $album['album_id']; ?>" class="album-edit-btn"> <a href="edit_album?id=<?php echo $album['album_id']; ?>" class="album-edit-icon" title="Edit">
<i class="far fa-edit"></i> <i class="far fa-edit"></i>
</a> </a>
<?php endif; ?> <?php endif; ?>

View File

@@ -39,7 +39,8 @@ if (isset($_SESSION['user_id']) && isset($conn) && $conn !== null) {
<li>... and many more!</li> <li>... and many more!</li>
</ul> </ul>
</div> </div>
<h2>R 2,500/year</h2> <?php $annualFee = getPriceByDescription('membership_fees'); ?>
<h2>R <?php echo number_format($annualFee, 0); ?>/year</h2>
<p>We go above and beyond to make your travel dreams reality hidden gems and must-see <p>We go above and beyond to make your travel dreams reality hidden gems and must-see
attractions</p> attractions</p>
<a href="membership_application" class="theme-btn mt-10 style-two"> <a href="membership_application" class="theme-btn mt-10 style-two">

660
src/pages/track-map.php Normal file
View File

@@ -0,0 +1,660 @@
<?php
$headerStyle = 'light';
$rootPath = dirname(dirname(__DIR__));
include_once($rootPath . '/header.php');
?>
<!-- Leaflet CSS -->
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
crossorigin=""/>
<style>
.track-map-section {
padding: 0;
margin: 0;
}
.track-info-box {
background: #f9f9f9;
padding: 30px;
margin: 20px auto;
max-width: 1200px;
border-radius: 8px;
}
.track-info-box h3 {
font-size: 1.5rem;
margin-bottom: 15px;
color: #333;
}
.track-info-box p {
color: #666;
line-height: 1.6;
margin-bottom: 10px;
}
.legend {
display: flex;
flex-wrap: wrap;
gap: 30px;
margin-top: 20px;
padding-top: 20px;
border-top: 1px solid #ddd;
justify-content: center;
}
.legend-item {
display: flex;
align-items: center;
gap: 10px;
}
/* Shared marker styling for both legend and map obstacles */
.legend-marker,
.obstacle-marker {
width: 30px;
height: 30px;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
font-weight: bold;
color: white;
transform: rotate(45deg);
}
.legend-marker span,
.obstacle-marker span {
transform: rotate(-45deg);
}
.legend-marker.red,
.obstacle-marker.red {
background: #e61e25;
}
.legend-marker.green,
.obstacle-marker.green {
background: #28a745;
}
.legend-marker.black,
.obstacle-marker.black {
background: #343a40;
}
.legend-marker.split,
.obstacle-marker.split {
background: linear-gradient(45deg, #e61e25 50%, #28a745 50%);
}
.obstacle-marker {
cursor: pointer;
}
/* Leaflet marker container */
.custom-marker-container {
background: transparent;
border: none;
}
#map {
width: 100%;
height: 700px;
margin: 0;
padding: 0;
}
.obstacle-popup h4 {
margin: 0 0 10px 0;
color: #333;
}
.obstacle-popup .difficulty-badge {
display: inline-block;
padding: 4px 10px;
border-radius: 12px;
font-size: 0.8rem;
font-weight: bold;
margin-bottom: 10px;
text-transform: uppercase;
}
.obstacle-popup .difficulty-badge.easy {
background: #d4edda;
color: #155724;
}
.obstacle-popup .difficulty-badge.medium {
background: #fff3cd;
color: #856404;
}
.obstacle-popup .difficulty-badge.hard {
background: #f8d7da;
color: #721c24;
}
.obstacle-popup .difficulty-badge.extreme {
background: #d1ecf1;
color: #0c5460;
}
.obstacle-popup img {
width: 100%;
max-width: 300px;
margin: 10px 0;
border-radius: 4px;
}
.obstacle-popup .description {
color: #666;
line-height: 1.5;
margin-top: 10px;
}
.obstacle-marker:hover {
transform: rotate(45deg) scale(1.1);
}
.obstacle-marker span {
transform: rotate(-45deg);
color: white;
font-weight: bold;
font-size: 20px;
text-shadow: 1px 1px 2px rgba(0,0,0,0.5);
}
/* Obstacle Form Modal */
.obstacle-modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.5);
z-index: 10000;
align-items: center;
justify-content: center;
}
.obstacle-modal.show {
display: flex;
}
.obstacle-modal-content {
background: white;
padding: 30px;
border-radius: 8px;
max-width: 500px;
width: 90%;
box-shadow: 0 4px 20px rgba(0,0,0,0.3);
}
.obstacle-modal-content h3 {
margin: 0 0 20px 0;
color: #333;
}
.obstacle-modal-content .form-group {
margin-bottom: 15px;
}
.obstacle-modal-content label {
display: block;
margin-bottom: 5px;
font-weight: 600;
color: #555;
}
.obstacle-modal-content input,
.obstacle-modal-content select {
width: 100%;
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
}
.obstacle-modal-content .btn-group {
display: flex;
gap: 10px;
margin-top: 20px;
}
.obstacle-modal-content .btn-group button {
flex: 1;
}
.alert-message {
position: fixed;
top: 20px;
right: 20px;
padding: 15px 20px;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
z-index: 10001;
display: none;
animation: slideIn 0.3s ease;
}
.alert-message.show {
display: block;
}
.alert-message.success {
background: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.alert-message.error {
background: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
@keyframes slideIn {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
</style>
<?php
$pageTitle = 'BASE4 4x4 Training Track';
$breadcrumbs = [['Home' => 'index.php']];
require_once($rootPath . '/components/banner.php');
?>
<!-- Track Map Section -->
<section class="track-map-section">
<div class="container">
<div class="track-info-box">
<h3>BASE4 4x4 Training Track</h3>
<p>The training track at BASE4 was first created when the property was acquired in 2000. It has since been developed to provide a variety of obstacles and terrain challenges suitable for all skill levels. Open to all members. Join us on our next Driver Training Course to enhance your off-road skills and confidence and put your vehicle to the test.</p>
<?php if ($role === 'superadmin'): ?>
<div style="margin: 20px 0; padding: 15px; background: #fff3cd; border-radius: 8px; border-left: 4px solid #ffc107;">
<button id="toggleEditMode" class="btn btn-warning" style="margin-bottom: 10px;">
🔧 Enable Edit Mode
</button>
<p id="editModeStatus" style="margin: 0; color: #856404; font-weight: bold; display: none;">
✏️ Edit Mode Active - Click on map to place new markers, drag to reposition
</p>
</div>
<?php endif; ?>
<div class="legend">
<div class="legend-item">
<div class="legend-marker green"><span></span></div>
<span>Beginner</span>
</div>
<div class="legend-item">
<div class="legend-marker red"><span></span></div>
<span>Intermediate</span>
</div>
<div class="legend-item">
<div class="legend-marker black"><span></span></div>
<span>Advanced</span>
</div>
</div>
</div>
</div>
<div id="map"></div>
</section>
<!-- Obstacle Form Modal -->
<div id="obstacleModal" class="obstacle-modal">
<div class="obstacle-modal-content">
<h3>Add New Obstacle</h3>
<form id="obstacleForm">
<input type="hidden" id="clickedLat" name="clickedLat">
<input type="hidden" id="clickedLng" name="clickedLng">
<div class="form-group">
<label for="obstacleNumber">Obstacle Number *</label>
<input type="number" id="obstacleNumber" name="obstacleNumber" required>
</div>
<div class="form-group">
<label for="markerColor">Marker Color *</label>
<select id="markerColor" name="markerColor" required>
<option value="green">Green (Beginner)</option>
<option value="red">Red (Intermediate)</option>
<option value="black">Black (Advanced)</option>
<option value="split">Split (Mixed)</option>
</select>
</div>
<div class="form-group">
<label for="obstacleName">Name</label>
<input type="text" id="obstacleName" name="obstacleName" value="New Obstacle">
</div>
<div class="form-group">
<label for="obstacleDifficulty">Difficulty</label>
<select id="obstacleDifficulty" name="obstacleDifficulty">
<option value="Easy">Easy</option>
<option value="Medium" selected>Medium</option>
<option value="Hard">Hard</option>
<option value="Extreme">Extreme</option>
</select>
</div>
<div class="btn-group">
<button type="button" class="btn btn-secondary" onclick="closeObstacleModal()">Cancel</button>
<button type="submit" class="btn btn-primary">Save Obstacle</button>
</div>
</form>
</div>
</div>
<!-- Alert Message -->
<div id="alertMessage" class="alert-message"></div>
<?php
require_once($rootPath . '/components/insta_footer.php');
?>
<!-- Leaflet JS -->
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="
crossorigin=""></script>
<!-- Track Map JavaScript -->
<script>
console.log('Track map script loaded');
// Check if Leaflet is available
if (typeof L === 'undefined') {
console.error('Leaflet library not loaded!');
} else {
console.log('Leaflet library is available, version:', L.version);
}
document.addEventListener('DOMContentLoaded', () => {
console.log('DOM loaded, initializing map...');
const mapElement = document.getElementById('map');
console.log('Map element:', mapElement);
if (!mapElement) {
console.error('Map element not found!');
return;
}
console.log('Map element dimensions:', mapElement.offsetWidth, 'x', mapElement.offsetHeight);
try {
// Image dimensions: 2876 x 2035 pixels
const imageWidth = 7942;
const imageHeight = 3913;
// Create map with simple CRS (pixel coordinates)
// Note: Leaflet uses [y, x] format, so bounds are [[0, 0], [height, width]]
const bounds = [[0, 0], [imageHeight, imageWidth]];
const map = L.map('map', {
crs: L.CRS.Simple,
minZoom: -1.5,
maxZoom: 2,
center: [imageHeight / 2, imageWidth / 2],
zoom: -1,
maxBounds: bounds,
maxBoundsViscosity: 1.0
});
console.log('Map object created with CRS.Simple:', map);
// Add aerial image overlay
const imageUrl = '/assets/images/track-aerial.jpg';
L.imageOverlay(imageUrl, bounds).addTo(map);
console.log('Aerial image overlay added');
// Add SVG overlay
const svgUrl = '/assets/images/track-route.svg';
L.imageOverlay(svgUrl, bounds, {
opacity: 1,
interactive: false
}).addTo(map);
console.log('SVG route overlay added');
// Fit map to image bounds
map.fitBounds(bounds);
console.log('Map initialized successfully');
// Edit mode state
let editMode = false;
let markers = [];
// Edit mode toggle (only for admins)
const toggleBtn = document.getElementById('toggleEditMode');
const statusText = document.getElementById('editModeStatus');
if (toggleBtn) {
toggleBtn.addEventListener('click', () => {
editMode = !editMode;
if (editMode) {
toggleBtn.textContent = '🔒 Disable Edit Mode';
toggleBtn.classList.remove('btn-warning');
toggleBtn.classList.add('btn-success');
statusText.style.display = 'block';
// Make existing markers draggable
markers.forEach(m => m.marker.dragging.enable());
} else {
toggleBtn.textContent = '🔧 Enable Edit Mode';
toggleBtn.classList.remove('btn-success');
toggleBtn.classList.add('btn-warning');
statusText.style.display = 'none';
// Disable dragging
markers.forEach(m => m.marker.dragging.disable());
}
});
}
// Click on map to add new marker (only in edit mode)
map.on('click', (e) => {
if (!editMode) return;
const coords = e.latlng;
// Store clicked coordinates and show modal
document.getElementById('clickedLat').value = coords.lat;
document.getElementById('clickedLng').value = coords.lng;
document.getElementById('obstacleModal').classList.add('show');
});
// Modal functions
window.closeObstacleModal = function() {
document.getElementById('obstacleModal').classList.remove('show');
document.getElementById('obstacleForm').reset();
};
window.showAlert = function(message, type = 'success') {
const alertDiv = document.getElementById('alertMessage');
alertDiv.textContent = message;
alertDiv.className = 'alert-message ' + type + ' show';
setTimeout(() => {
alertDiv.classList.remove('show');
}, 4000);
};
// Handle form submission
document.getElementById('obstacleForm').addEventListener('submit', function(e) {
e.preventDefault();
const lat = parseFloat(document.getElementById('clickedLat').value);
const lng = parseFloat(document.getElementById('clickedLng').value);
const obstacleNumber = document.getElementById('obstacleNumber').value;
const markerColor = document.getElementById('markerColor').value;
const name = document.getElementById('obstacleName').value;
const difficulty = document.getElementById('obstacleDifficulty').value;
// Create temporary marker
const markerHtml = `
<div class="obstacle-marker ${markerColor}">
<span>${obstacleNumber}</span>
</div>
`;
const customIcon = L.divIcon({
html: markerHtml,
className: 'custom-marker-container',
iconSize: [40, 40],
iconAnchor: [20, 20]
});
const marker = L.marker([lat, lng], {
icon: customIcon,
draggable: true
}).addTo(map);
// Save to database
const obstacleData = {
obstacle_number: obstacleNumber,
x_position: Math.round(lng),
y_position: Math.round(lat),
marker_color: markerColor,
name: name,
difficulty: difficulty,
description: 'New obstacle - edit details in admin panel'
};
saveObstacle(obstacleData, marker);
closeObstacleModal();
});
function saveObstacle(data, marker) {
fetch('/src/processors/track-obstacles.php?action=create', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
})
.then(response => response.json())
.then(result => {
if (result.status === 'success') {
showAlert('Obstacle #' + data.obstacle_number + ' created successfully!', 'success');
marker.obstacleId = result.obstacle_id;
markers.push({ marker, data: {...data, obstacle_id: result.obstacle_id} });
// Add dragend event to update position
marker.on('dragend', function() {
const pos = marker.getLatLng();
updateObstaclePosition(marker.obstacleId, Math.round(pos.lng), Math.round(pos.lat));
});
} else {
showAlert('Error: ' + result.message, 'error');
map.removeLayer(marker);
}
})
.catch(error => {
showAlert('Error creating obstacle: ' + error, 'error');
map.removeLayer(marker);
});
}
function updateObstaclePosition(obstacleId, x, y) {
fetch('/src/processors/track-obstacles.php?action=updatePosition', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
obstacle_id: obstacleId,
x_position: x,
y_position: y
})
})
.then(response => response.json())
.then(result => {
if (result.status === 'success') {
showAlert('Position updated', 'success');
} else {
showAlert('Error updating position: ' + result.message, 'error');
}
});
}
// Fetch and add obstacle markers
fetch('/src/processors/track-obstacles.php?action=getAll')
.then(response => response.json())
.then(result => {
console.log('Obstacles data:', result);
if (result.status === 'success' && result.data) {
result.data.forEach((obstacle, index) => {
// Leaflet uses [y, x] format for coordinates
const position = [obstacle.y_position, obstacle.x_position];
// Create custom marker HTML
const markerHtml = `
<div class="obstacle-marker ${obstacle.marker_color}">
<span>${obstacle.obstacle_number}</span>
</div>
`;
// Create custom icon
const customIcon = L.divIcon({
html: markerHtml,
className: 'custom-marker-container',
iconSize: [40, 40],
iconAnchor: [20, 20]
});
// Create popup content
const popupContent = `
<div class="obstacle-popup">
<h4>${obstacle.name}</h4>
<span class="difficulty-badge ${obstacle.difficulty.toLowerCase()}">${obstacle.difficulty}</span>
${obstacle.image_path ? `<img src="${obstacle.image_path}" alt="${obstacle.name}" style="width: 100%; max-width: 300px; margin: 10px 0; border-radius: 8px;">` : ''}
<p>${obstacle.description}</p>
</div>
`;
// Add marker to map
const marker = L.marker(position, {
icon: customIcon,
draggable: false
})
.addTo(map)
.bindPopup(popupContent, {
maxWidth: 350,
className: 'obstacle-popup-container'
});
marker.obstacleId = obstacle.obstacle_id;
markers.push({ marker, data: obstacle });
// Add dragend event for position updates
marker.on('dragend', function() {
const pos = marker.getLatLng();
updateObstaclePosition(obstacle.obstacle_id, Math.round(pos.lng), Math.round(pos.lat));
});
});
console.log('Added ' + result.data.length + ' obstacle markers');
}
})
.catch(error => {
console.error('Error loading obstacles:', error);
});
} catch (error) {
console.error('Error initializing map:', error);
}
});
</script>
<?php ob_end_flush(); ?>

View File

@@ -5,6 +5,11 @@ require_once($rootPath . "/src/config/connection.php");
require_once($rootPath . "/src/config/functions.php"); require_once($rootPath . "/src/config/functions.php");
session_start(); session_start();
// Enable error reporting for debugging
error_reporting(E_ALL);
ini_set('display_errors', 0); // Don't display, but log them
ini_set('log_errors', 1);
if (!isset($_SESSION['user_id'])) { if (!isset($_SESSION['user_id'])) {
http_response_code(401); http_response_code(401);
echo "Not authorized"; echo "Not authorized";
@@ -32,36 +37,42 @@ echo $author_id;
$cover_image_path = null; $cover_image_path = null;
// Only attempt upload if a file was submitted // Only attempt upload if a file was submitted
if (!empty($_FILES['cover_image']['name'])) { if (!empty($_FILES['cover_image']['name']) && $_FILES['cover_image']['error'] === UPLOAD_ERR_OK) {
$uploadDir = $rootPath . "/uploads/blogs/" . $article_id . "/"; $uploadDir = $rootPath . "/uploads/blogs/" . $article_id . "/";
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0755, true); // Create directory if it doesn't exist (match working pattern)
if (!file_exists($uploadDir)) {
mkdir($uploadDir, 0777, true);
} }
// Validate file using existing function // Simple validation - check extension
$file_result = validateFileUpload($_FILES['cover_image'], 'profile_picture'); $extension = strtolower(pathinfo($_FILES['cover_image']['name'], PATHINFO_EXTENSION));
if ($file_result === false) { $allowedExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
if (!in_array($extension, $allowedExtensions)) {
http_response_code(400); http_response_code(400);
echo "Invalid file upload"; echo "Invalid file type. Allowed: jpg, jpeg, png, gif, webp";
exit; exit;
} }
// Use fixed filename "cover" to avoid creating multiple copies on autosave // Use fixed filename "cover" to avoid creating multiple copies on autosave
$extension = $file_result['extension'];
$filename = "cover." . $extension; $filename = "cover." . $extension;
// Delete old cover if it exists with different extension // Delete old cover if it exists with different extension
array_map('unlink', glob($uploadDir . "cover.*")); $oldCovers = glob($uploadDir . "cover.*");
if ($oldCovers) {
foreach ($oldCovers as $oldCover) {
@unlink($oldCover);
}
}
$targetPath = $uploadDir . $filename; $targetPath = $uploadDir . $filename;
$cover_image_path = "/uploads/blogs/" . $article_id . "/" . $filename; $cover_image_path = "/uploads/blogs/" . $article_id . "/" . $filename;
// Move the uploaded file // Move the uploaded file
if (move_uploaded_file($_FILES['cover_image']['tmp_name'], $targetPath)) { if (!move_uploaded_file($_FILES['cover_image']['tmp_name'], $targetPath)) {
// File moved successfully, $cover_image_path is set
} else {
http_response_code(500); http_response_code(500);
echo "Failed to move uploaded file."; echo "Failed to move uploaded file";
exit; exit;
} }
} }

View File

@@ -26,8 +26,8 @@ if (isset($_FILES['cover_image']) && $_FILES['cover_image']['error'] === UPLOAD_
$upload_dir = $rootPath . '/uploads/blogs/' . $folder_id . '/'; $upload_dir = $rootPath . '/uploads/blogs/' . $folder_id . '/';
// Create directory if it doesn't exist // Create directory if it doesn't exist
if (!is_dir($upload_dir)) { if (!file_exists($upload_dir)) {
mkdir($upload_dir, 0755, true); mkdir($upload_dir, 0777, true);
} }
// Validate and process the file // Validate and process the file

View File

@@ -0,0 +1,76 @@
<?php
ob_start();
header('Content-Type: application/json');
$rootPath = dirname(dirname(__DIR__));
require_once($rootPath . "/src/config/env.php");
require_once($rootPath . '/src/config/functions.php');
require_once($rootPath . '/src/config/connection.php');
// Start session if not already started
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
// Check admin status
if (empty($_SESSION['user_id'])) {
ob_end_clean();
echo json_encode(['status' => 'error', 'message' => 'Unauthorized access']);
exit;
}
$user_role = getUserRole();
if (!in_array($user_role, ['admin', 'superadmin'])) {
ob_end_clean();
echo json_encode(['status' => 'error', 'message' => 'Unauthorized access']);
exit;
}
try {
$event_id = intval($_POST['event_id'] ?? 0);
if ($event_id <= 0) {
throw new Exception('Invalid event ID');
}
// Get event details to delete associated files
$stmt = $conn->prepare("SELECT image, promo FROM events WHERE event_id = ?");
$stmt->bind_param("i", $event_id);
$stmt->execute();
$result = $stmt->get_result();
if ($result->num_rows > 0) {
$event = $result->fetch_assoc();
// Delete image files
if ($event['image'] && file_exists($rootPath . '/' . $event['image'])) {
unlink($rootPath . '/' . $event['image']);
}
if ($event['promo'] && file_exists($rootPath . '/' . $event['promo'])) {
unlink($rootPath . '/' . $event['promo']);
}
// Delete from database
$delete_stmt = $conn->prepare("DELETE FROM events WHERE event_id = ?");
$delete_stmt->bind_param("i", $event_id);
if ($delete_stmt->execute()) {
ob_end_clean();
echo json_encode(['status' => 'success', 'message' => 'Event deleted successfully']);
} else {
ob_end_clean();
echo json_encode(['status' => 'error', 'message' => 'Failed to delete event']);
}
$delete_stmt->close();
} else {
ob_end_clean();
echo json_encode(['status' => 'error', 'message' => 'Event not found']);
}
$stmt->close();
} catch (Exception $e) {
ob_end_clean();
echo json_encode(['status' => 'error', 'message' => $e->getMessage()]);
}

View File

@@ -174,27 +174,33 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if ($stmt->execute()) { if ($stmt->execute()) {
// Insert into the membership fees table // Insert into the membership fees table
$payment_amount = calculateProrata(210); // Assuming a fixed membership fee, adjust as needed
$payment_date = date('Y-m-d');
$membership_start_date = $payment_date;
// $membership_end_date = date('Y-12-31');
// Get today's date
$today = new DateTime(); $today = new DateTime();
$month = (int)$today->format('n');
$year = (int)$today->format('Y');
$payment_date = $today->format('Y-m-d');
$membership_start_date = $payment_date;
// Determine the target February if ($month == 12 || $month == 1 || $month == 2) {
if ($today->format('n') > 2) { // December, January, February: charge full fee, valid till end of next Feb
// If we're past February, target is next year's Feb 28/29 $payment_amount = getPriceByDescription('membership_fees');
$year = $today->format('Y') + 1; // If Dec, Jan, Feb, set end to next year's Feb
} else { $end_year = ($month == 12) ? $year + 2 : $year + 1;
// Otherwise, this year's February $membership_end_date = (new DateTime("$end_year-02-01"))
$year = $today->format('Y');
}
// Handle leap year (Feb 29) automatically
$membership_end_date = (new DateTime("$year-02-01"))
->modify('last day of this month') ->modify('last day of this month')
->format('Y-m-d'); ->format('Y-m-d');
} else {
// Prorata for Mar-Nov
$payment_amount = calculateProrata(getPriceByDescription('pro_rata'));
// End of next Feb if after Feb, else this Feb
if ($month > 2) {
$end_year = $year + 1;
} else {
$end_year = $year;
}
$membership_end_date = (new DateTime("$end_year-02-01"))
->modify('last day of this month')
->format('Y-m-d');
}
$stmt = $conn->prepare("INSERT INTO membership_fees (user_id, payment_amount, payment_date, membership_start_date, membership_end_date, payment_status, payment_id) $stmt = $conn->prepare("INSERT INTO membership_fees (user_id, payment_amount, payment_date, membership_start_date, membership_end_date, payment_status, payment_id)
VALUES (?, ?, ?, ?, ?, 'PENDING', ?)"); VALUES (?, ?, ?, ?, ?, 'PENDING', ?)");

View File

@@ -1,6 +1,11 @@
<?php <?php
$rootPath = dirname(dirname(__DIR__)); $rootPath = dirname(dirname(__DIR__));
include_once($rootPath . '/header.php'); require_once($rootPath . "/src/config/env.php");
require_once($rootPath . "/src/config/session.php");
require_once($rootPath . "/src/config/connection.php");
require_once($rootPath . "/src/config/functions.php");
// session_start();
checkAdmin(); checkAdmin();
header('Content-Type: application/json'); header('Content-Type: application/json');
@@ -49,7 +54,7 @@ if ($_GET['action'] ?? null === 'delete') {
} }
// Check CSRF token // Check CSRF token
if (!isset($_POST['csrf_token']) || !verifyCsrfToken($_POST['csrf_token'])) { if (!isset($_POST['csrf_token']) || !validateCSRFToken($_POST['csrf_token'])) {
echo json_encode(['status' => 'error', 'message' => 'CSRF token validation failed']); echo json_encode(['status' => 'error', 'message' => 'CSRF token validation failed']);
exit; exit;
} }
@@ -73,17 +78,17 @@ if (!$name || !$type || !$location || !$date || !$time || !$feature || !$descrip
$image_path = null; $image_path = null;
if (!empty($_FILES['image']['name'])) { if (!empty($_FILES['image']['name'])) {
$upload_dir = $rootPath . '/assets/images/events/'; $upload_dir = $rootPath . '/assets/images/events/';
if (!is_dir($upload_dir)) { if (!file_exists($upload_dir)) {
mkdir($upload_dir, 0755, true); mkdir($upload_dir, 0777, true);
} }
$file_name = uniqid() . '_' . basename($_FILES['image']['name']); $file_name = uniqid() . '_' . basename($_FILES['image']['name']);
$target_file = $upload_dir . $file_name; $target_file = $upload_dir . $file_name;
$file_type = mime_content_type($_FILES['image']['tmp_name']);
// Validate image file // Validate file extension
$allowed_types = ['image/jpeg', 'image/png', 'image/gif', 'image/webp']; $ext = strtolower(pathinfo($file_name, PATHINFO_EXTENSION));
if (!in_array($file_type, $allowed_types)) { $allowed_extensions = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
if (!in_array($ext, $allowed_extensions)) {
echo json_encode(['status' => 'error', 'message' => 'Invalid image file type. Only JPEG, PNG, GIF, and WebP are allowed']); echo json_encode(['status' => 'error', 'message' => 'Invalid image file type. Only JPEG, PNG, GIF, and WebP are allowed']);
exit; exit;
} }
@@ -103,17 +108,17 @@ if (!empty($_FILES['image']['name'])) {
$promo_path = null; $promo_path = null;
if (!empty($_FILES['promo']['name'])) { if (!empty($_FILES['promo']['name'])) {
$upload_dir = $rootPath . '/assets/images/events/'; $upload_dir = $rootPath . '/assets/images/events/';
if (!is_dir($upload_dir)) { if (!file_exists($upload_dir)) {
mkdir($upload_dir, 0755, true); mkdir($upload_dir, 0777, true);
} }
$file_name = uniqid() . '_promo_' . basename($_FILES['promo']['name']); $file_name = uniqid() . '_promo_' . basename($_FILES['promo']['name']);
$target_file = $upload_dir . $file_name; $target_file = $upload_dir . $file_name;
$file_type = mime_content_type($_FILES['promo']['tmp_name']);
// Validate image file // Validate file extension
$allowed_types = ['image/jpeg', 'image/png', 'image/gif', 'image/webp']; $ext = strtolower(pathinfo($file_name, PATHINFO_EXTENSION));
if (!in_array($file_type, $allowed_types)) { $allowed_extensions = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
if (!in_array($ext, $allowed_extensions)) {
echo json_encode(['status' => 'error', 'message' => 'Invalid promo image file type. Only JPEG, PNG, GIF, and WebP are allowed']); echo json_encode(['status' => 'error', 'message' => 'Invalid promo image file type. Only JPEG, PNG, GIF, and WebP are allowed']);
exit; exit;
} }

View File

@@ -36,7 +36,7 @@ if (isset($_POST['signature'])) {
$filePath = $rootPath . '/uploads/signatures/' . $fileName; $filePath = $rootPath . '/uploads/signatures/' . $fileName;
// Ensure the directory exists // Ensure the directory exists
if (!is_dir($rootPath . '/uploads/signatures')) { if (!file_exists($rootPath . '/uploads/signatures')) {
mkdir($rootPath . '/uploads/signatures', 0777, true); mkdir($rootPath . '/uploads/signatures', 0777, true);
} }

View File

@@ -136,8 +136,8 @@ try {
$upload_dir = $rootPath . '/assets/images/trips/'; $upload_dir = $rootPath . '/assets/images/trips/';
// Create directory if it doesn't exist // Create directory if it doesn't exist
if (!is_dir($upload_dir)) { if (!file_exists($upload_dir)) {
mkdir($upload_dir, 0755, true); mkdir($upload_dir, 0777, true);
} }
$allowed_extensions = ['jpg', 'jpeg', 'png', 'gif', 'webp']; $allowed_extensions = ['jpg', 'jpeg', 'png', 'gif', 'webp'];

View File

@@ -52,26 +52,25 @@ try {
// Create album directory // Create album directory
$albumDir = $rootPath . '/assets/uploads/gallery/' . $album_id; $albumDir = $rootPath . '/assets/uploads/gallery/' . $album_id;
if (!is_dir($albumDir)) { if (!file_exists($albumDir)) {
if (!mkdir($albumDir, 0755, true)) { mkdir($albumDir, 0777, true);
throw new Exception('Failed to create album directory');
}
} }
// Handle cover image upload // Handle cover image upload
$coverImagePath = null; $coverImagePath = null;
if (isset($_FILES['cover_image']) && $_FILES['cover_image']['error'] !== UPLOAD_ERR_NO_FILE) { if (isset($_FILES['cover_image']) && $_FILES['cover_image']['error'] === UPLOAD_ERR_OK) {
$allowedMimes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
$maxSize = 5 * 1024 * 1024; // 5MB $maxSize = 5 * 1024 * 1024; // 5MB
$fileName = $_FILES['cover_image']['name']; $fileName = $_FILES['cover_image']['name'];
$fileTmpName = $_FILES['cover_image']['tmp_name']; $fileTmpName = $_FILES['cover_image']['tmp_name'];
$fileSize = $_FILES['cover_image']['size']; $fileSize = $_FILES['cover_image']['size'];
$fileMime = mime_content_type($fileTmpName);
// Validate file // Validate file extension
if (!in_array($fileMime, $allowedMimes)) { $ext = strtolower(pathinfo($fileName, PATHINFO_EXTENSION));
throw new Exception('Invalid cover image file type'); $allowedExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
if (!in_array($ext, $allowedExtensions)) {
throw new Exception('Invalid cover image file type. Allowed: jpg, jpeg, png, gif, webp');
} }
if ($fileSize > $maxSize) { if ($fileSize > $maxSize) {
@@ -96,8 +95,7 @@ try {
} }
// Handle photo uploads // Handle photo uploads
if (isset($_FILES['photos']) && $_FILES['photos']['error'][0] !== UPLOAD_ERR_NO_FILE) { if (isset($_FILES['photos']) && $_FILES['photos']['error'][0] === UPLOAD_ERR_OK) {
$allowedMimes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
$maxSize = 5 * 1024 * 1024; // 5MB $maxSize = 5 * 1024 * 1024; // 5MB
$displayOrder = 1; $displayOrder = 1;
@@ -111,11 +109,13 @@ try {
$fileName = $_FILES['photos']['name'][$i]; $fileName = $_FILES['photos']['name'][$i];
$fileTmpName = $_FILES['photos']['tmp_name'][$i]; $fileTmpName = $_FILES['photos']['tmp_name'][$i];
$fileSize = $_FILES['photos']['size'][$i]; $fileSize = $_FILES['photos']['size'][$i];
$fileMime = mime_content_type($fileTmpName);
// Validate file // Validate file extension
if (!in_array($fileMime, $allowedMimes)) { $ext = strtolower(pathinfo($fileName, PATHINFO_EXTENSION));
throw new Exception('Invalid file type: ' . $fileName); $allowedExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
if (!in_array($ext, $allowedExtensions)) {
throw new Exception('Invalid file type: ' . $fileName . '. Allowed: jpg, jpeg, png, gif, webp');
} }
if ($fileSize > $maxSize) { if ($fileSize > $maxSize) {

View File

@@ -43,14 +43,9 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$filename = str_replace(' ', '_', $eft_id) . '.pdf'; $filename = str_replace(' ', '_', $eft_id) . '.pdf';
$target_file = $target_dir . $filename; $target_file = $target_dir . $filename;
// Make sure target directory exists and writable // Make sure target directory exists
if (!is_dir($target_dir)) { if (!file_exists($target_dir)) {
mkdir($target_dir, 0755, true); mkdir($target_dir, 0777, true);
}
if (!is_writable($target_dir)) {
echo "<div class='alert alert-danger'>Upload directory is not writable: $target_dir</div>";
exit;
} }
if (move_uploaded_file($_FILES['pop_file']['tmp_name'], $target_file)) { if (move_uploaded_file($_FILES['pop_file']['tmp_name'], $target_file)) {

View File

@@ -0,0 +1,164 @@
<?php
/**
* TRACK OBSTACLES API ENDPOINT
*
* Returns all track obstacles as JSON for the interactive map.
*
* Usage:
* GET /src/processors/track-obstacles.php?action=getAll
*
* Response:
* {
* "status": "success",
* "data": [
* {
* "obstacle_id": 1,
* "name": "Rock Crawl",
* "x_position": 150,
* "y_position": 200,
* "difficulty": "medium",
* "description": "Navigate through rocky terrain...",
* "image_path": "assets/images/obstacles/obstacle1.jpg",
* "marker_color": "green"
* },
* ...
* ]
* }
*/
// Set headers for JSON response
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type');
// Load configuration and database
$rootPath = 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 . "/classes/DatabaseService.php");
// Get database instance
$db = new DatabaseService($conn);
try {
// Get action from query string
$action = $_GET['action'] ?? 'getAll';
if ($action === 'getAll') {
// Fetch all obstacles from the database
$sql = "SELECT
obstacle_id,
obstacle_number,
name,
x_position,
y_position,
difficulty,
description,
image_path,
marker_color
FROM track_obstacles
ORDER BY obstacle_id ASC";
$result = $conn->query($sql);
if ($result === false) {
throw new Exception("Database query failed: " . $conn->error);
}
$obstacles = [];
while ($row = $result->fetch_assoc()) {
$obstacles[] = $row;
}
echo json_encode([
'status' => 'success',
'data' => $obstacles
]);
} elseif ($action === 'create') {
// Create new obstacle (superadmin only)
$role = getUserRole();
if ($role !== 'superadmin') {
http_response_code(403);
echo json_encode(['status' => 'error', 'message' => 'Unauthorized']);
exit;
}
$input = json_decode(file_get_contents('php://input'), true);
$sql = "INSERT INTO track_obstacles
(obstacle_number, name, x_position, y_position, difficulty, description, marker_color)
VALUES (?, ?, ?, ?, ?, ?, ?)";
$insertId = $db->insert($sql, [
$input['obstacle_number'],
$input['name'],
$input['x_position'],
$input['y_position'],
$input['difficulty'],
$input['description'],
$input['marker_color']
], 'ssiisss');
if ($insertId) {
echo json_encode([
'status' => 'success',
'message' => 'Obstacle created',
'obstacle_id' => $insertId
]);
} else {
throw new Exception("Failed to create obstacle: " . $db->getLastError());
}
} elseif ($action === 'updatePosition') {
// Update obstacle position (superadmin only)
$role = getUserRole();
if ($role !== 'superadmin') {
http_response_code(403);
echo json_encode(['status' => 'error', 'message' => 'Unauthorized']);
exit;
}
$input = json_decode(file_get_contents('php://input'), true);
$sql = "UPDATE track_obstacles
SET x_position = ?, y_position = ?
WHERE obstacle_id = ?";
$result = $db->update($sql, [
$input['x_position'],
$input['y_position'],
$input['obstacle_id']
], 'iii');
if ($result !== false) {
echo json_encode([
'status' => 'success',
'message' => 'Position updated'
]);
} else {
throw new Exception("Failed to update position: " . $db->getLastError());
}
} else {
// Invalid action
http_response_code(400);
echo json_encode([
'status' => 'error',
'message' => 'Invalid action specified'
]);
}
} catch (Exception $e) {
// Return error response
http_response_code(500);
echo json_encode([
'status' => 'error',
'message' => 'Server error: ' . $e->getMessage()
]);
}
exit();
?>

View File

@@ -76,26 +76,30 @@ try {
$updateStmt->close(); $updateStmt->close();
// Handle cover image upload if provided // Handle cover image upload if provided
if (isset($_FILES['cover_image']) && $_FILES['cover_image']['error'] !== UPLOAD_ERR_NO_FILE) { if (isset($_FILES['cover_image']) && $_FILES['cover_image']['error'] === UPLOAD_ERR_OK) {
$allowedMimes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
$maxSize = 5 * 1024 * 1024; // 5MB
$fileName = $_FILES['cover_image']['name']; $fileName = $_FILES['cover_image']['name'];
$fileTmpName = $_FILES['cover_image']['tmp_name']; $fileTmpName = $_FILES['cover_image']['tmp_name'];
$fileSize = $_FILES['cover_image']['size']; $fileSize = $_FILES['cover_image']['size'];
$fileMime = mime_content_type($fileTmpName);
// Validate file // Validate file extension
if (!in_array($fileMime, $allowedMimes)) { $ext = strtolower(pathinfo($fileName, PATHINFO_EXTENSION));
throw new Exception('Invalid cover image file type'); $allowedExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
if (!in_array($ext, $allowedExtensions)) {
throw new Exception('Invalid cover image file type. Allowed: jpg, jpeg, png, gif, webp');
} }
if ($fileSize > $maxSize) { if ($fileSize > 5 * 1024 * 1024) {
throw new Exception('Cover image file too large (max 5MB)'); throw new Exception('Cover image file too large (max 5MB)');
} }
$albumDir = $rootPath . '/assets/uploads/gallery/' . $album_id; $albumDir = $rootPath . '/assets/uploads/gallery/' . $album_id;
// Create directory if it doesn't exist (match working pattern)
if (!file_exists($albumDir)) {
mkdir($albumDir, 0777, true);
}
// Delete old cover if it exists // Delete old cover if it exists
$oldCoverStmt = $conn->prepare("SELECT cover_image FROM photo_albums WHERE album_id = ?"); $oldCoverStmt = $conn->prepare("SELECT cover_image FROM photo_albums WHERE album_id = ?");
$oldCoverStmt->bind_param("i", $album_id); $oldCoverStmt->bind_param("i", $album_id);
@@ -104,16 +108,15 @@ try {
if ($oldCoverResult->num_rows > 0) { if ($oldCoverResult->num_rows > 0) {
$oldCover = $oldCoverResult->fetch_assoc(); $oldCover = $oldCoverResult->fetch_assoc();
if ($oldCover['cover_image']) { if ($oldCover['cover_image']) {
$oldCoverPath = $_SERVER['DOCUMENT_ROOT'] . $oldCover['cover_image']; $oldCoverPath = $rootPath . $oldCover['cover_image'];
if (file_exists($oldCoverPath)) { if (file_exists($oldCoverPath)) {
unlink($oldCoverPath); @unlink($oldCoverPath);
} }
} }
} }
$oldCoverStmt->close(); $oldCoverStmt->close();
// Generate unique filename // Generate unique filename
$ext = pathinfo($fileName, PATHINFO_EXTENSION);
$newFileName = 'cover_' . uniqid() . '.' . $ext; $newFileName = 'cover_' . uniqid() . '.' . $ext;
$filePath = $albumDir . '/' . $newFileName; $filePath = $albumDir . '/' . $newFileName;
$coverImagePath = '/assets/uploads/gallery/' . $album_id . '/' . $newFileName; $coverImagePath = '/assets/uploads/gallery/' . $album_id . '/' . $newFileName;
@@ -130,12 +133,15 @@ try {
} }
// Handle photo uploads if any // Handle photo uploads if any
if (isset($_FILES['photos']) && $_FILES['photos']['error'][0] !== UPLOAD_ERR_NO_FILE) { if (isset($_FILES['photos']) && $_FILES['photos']['error'][0] === UPLOAD_ERR_OK) {
$allowedMimes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
$maxSize = 5 * 1024 * 1024; // 5MB $maxSize = 5 * 1024 * 1024; // 5MB
$albumDir = $rootPath . '/assets/uploads/gallery/' . $album_id; $albumDir = $rootPath . '/assets/uploads/gallery/' . $album_id;
// Create directory if it doesn't exist (match working pattern)
if (!file_exists($albumDir)) {
mkdir($albumDir, 0777, true);
}
// Get current max display order // Get current max display order
$orderStmt = $conn->prepare("SELECT MAX(display_order) as max_order FROM photos WHERE album_id = ?"); $orderStmt = $conn->prepare("SELECT MAX(display_order) as max_order FROM photos WHERE album_id = ?");
$orderStmt->bind_param("i", $album_id); $orderStmt->bind_param("i", $album_id);
@@ -153,15 +159,17 @@ try {
$fileName = $_FILES['photos']['name'][$i]; $fileName = $_FILES['photos']['name'][$i];
$fileTmpName = $_FILES['photos']['tmp_name'][$i]; $fileTmpName = $_FILES['photos']['tmp_name'][$i];
$fileSize = $_FILES['photos']['size'][$i]; $fileSize = $_FILES['photos']['size'][$i];
$fileMime = mime_content_type($fileTmpName);
// Validate file // Validate file extension
if (!in_array($fileMime, $allowedMimes)) { $ext = strtolower(pathinfo($fileName, PATHINFO_EXTENSION));
throw new Exception('Invalid file type: ' . $fileName); $allowedExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
if (!in_array($ext, $allowedExtensions)) {
throw new Exception('Invalid file type: ' . $fileName . '. Allowed: jpg, jpeg, png, gif, webp');
} }
if ($fileSize > $maxSize) { if ($fileSize > $maxSize) {
throw new Exception('File too large: ' . $fileName); throw new Exception('File too large: ' . $fileName . ' (max 5MB)');
} }
// Generate unique filename // Generate unique filename

View File

@@ -43,15 +43,9 @@ if (isset($_FILES['profile_picture']) && $_FILES['profile_picture']['error'] !=
$target_dir = $rootPath . "/assets/images/pp/"; $target_dir = $rootPath . "/assets/images/pp/";
$target_file = $target_dir . $randomFilename; $target_file = $target_dir . $randomFilename;
// Ensure upload directory exists and is writable // Ensure upload directory exists
if (!is_dir($target_dir)) { if (!file_exists($target_dir)) {
mkdir($target_dir, 0755, true); mkdir($target_dir, 0777, true);
}
if (!is_writable($target_dir)) {
$response['message'] = 'Upload directory is not writable.';
echo json_encode($response);
exit();
} }
// Move the uploaded file // Move the uploaded file

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB