Compare commits
9 Commits
feature/bl
...
a66382661d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a66382661d | ||
|
|
32e50ffc39 | ||
|
|
cce181e2d0 | ||
|
|
48ee7592b2 | ||
|
|
abb8eb23e5 | ||
|
|
2acbeac7ca | ||
|
|
5808788b9e | ||
|
|
bbc0aecbcb | ||
|
|
752ea6e5e9 |
@@ -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
@@ -0,0 +1,4 @@
|
|||||||
|
; memory_limit = 512M
|
||||||
|
upload_max_filesize = 64M
|
||||||
|
post_max_size = 64M
|
||||||
|
max_execution_time = 120
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
<?php
|
|
||||||
// Redirector file - loads the actual page from src/pages/other/
|
|
||||||
require_once __DIR__ . '/src/pages/other/about.php';
|
|
||||||
@@ -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; }
|
||||||
|
|||||||
|
After Width: | Height: | Size: 494 KiB |
BIN
assets/images/obstacles/01_03.png
Normal file
|
After Width: | Height: | Size: 6.6 MiB |
BIN
assets/images/pp/6318a13edd2e79cf13ff60a74ebcb858.png
Normal file
|
After Width: | Height: | Size: 291 KiB |
BIN
assets/images/pp/857004ff86e047673beaafba95a1ebc6.jpg
Normal file
|
After Width: | Height: | Size: 170 KiB |
BIN
assets/images/pp/c07abeef5590d7e141080a53581bb5cb.png
Normal file
|
After Width: | Height: | Size: 291 KiB |
BIN
assets/images/track-aerial.jpg
Normal file
|
After Width: | Height: | Size: 4.2 MiB |
115
assets/images/track-route.svg
Normal file
|
After Width: | Height: | Size: 24 KiB |
13126
assets/images/track-route2.svg
Normal file
|
After Width: | Height: | Size: 2.6 MiB |
66
assets/js/map.js
Normal 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);
|
||||||
|
}
|
||||||
|
});
|
||||||
25
docs/migrations/005_create_track_obstacles_table.sql
Normal 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;
|
||||||
@@ -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
@@ -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">×</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>
|
||||||
@@ -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";
|
|
||||||
?>
|
|
||||||
139
sitemap.xml
@@ -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>
|
||||||
|
|||||||
@@ -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'];
|
||||||
|
|||||||
@@ -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 filterInput = document.querySelector('.filter-input');
|
||||||
const table = document.querySelector('table');
|
const cards = document.querySelectorAll('.destination-item');
|
||||||
if (table) {
|
|
||||||
const headers = table.querySelectorAll('thead th');
|
|
||||||
const rows = Array.from(table.querySelectorAll('tbody tr'));
|
|
||||||
|
|
||||||
headers.forEach((header, index) => {
|
if (cards.length === 0 && filterInput) {
|
||||||
header.addEventListener('click', () => {
|
filterInput.style.display = "none";
|
||||||
const sortedRows = rows.sort((a, b) => {
|
} else if (filterInput) {
|
||||||
const aText = a.cells[index].textContent.trim().toLowerCase();
|
filterInput.addEventListener("input", function() {
|
||||||
const bText = b.cells[index].textContent.trim().toLowerCase();
|
const filterValue = filterInput.value.trim().toLowerCase();
|
||||||
|
cards.forEach(card => {
|
||||||
if (aText < bText) return -1;
|
const cardText = card.textContent.trim().toLowerCase();
|
||||||
if (aText > bText) return 1;
|
card.style.display = cardText.includes(filterValue) ? "" : "none";
|
||||||
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');
|
|
||||||
if (filterInput) {
|
|
||||||
filterInput.addEventListener('input', function() {
|
|
||||||
const filterValue = filterInput.value.trim().toLowerCase();
|
|
||||||
rows.forEach(row => {
|
|
||||||
const rowText = row.textContent.trim().toLowerCase();
|
|
||||||
row.style.display = rowText.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
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<?php if (isset($_SESSION['message'])): ?>
|
||||||
|
<div class="alert alert-warning message-box">
|
||||||
|
<?php echo $_SESSION['message']; ?>
|
||||||
|
<span class="close-btn" onclick="this.parentElement.style.display='none'">×</span>
|
||||||
|
</div>
|
||||||
|
<?php unset($_SESSION['message']);
|
||||||
|
endif;
|
||||||
|
|
||||||
<?php
|
if (count($events) > 0) {
|
||||||
if (!empty($events)) {
|
echo '<input type="text" class="filter-input" placeholder="Filter events...">';
|
||||||
echo '<div class="row">
|
echo '<div class="events-container" data-aos="fade-up" data-aos-duration="1500" data-aos-offset="50">';
|
||||||
<div class="col-lg-12">
|
|
||||||
<div class="form-group mb-20">
|
foreach ($events as $event) {
|
||||||
<input type="text" class="filter-input" placeholder="Search events...">
|
$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>
|
</div>
|
||||||
<table>
|
';
|
||||||
<thead>
|
}
|
||||||
<tr>
|
|
||||||
<th>Event Name</th>
|
echo '</div>';
|
||||||
<th>Type</th>
|
} else {
|
||||||
<th>Location</th>
|
echo '<div class="no-events">
|
||||||
<th>Date</th>
|
<p>No events found. <a href="manage_events">Create one</a></p>
|
||||||
<th>Status</th>
|
</div>';
|
||||||
<th>Actions</th>
|
}
|
||||||
</tr>
|
?>
|
||||||
</thead>
|
|
||||||
<tbody>';
|
</div>
|
||||||
foreach ($events as $event) {
|
|
||||||
$publishButtonText = $event['published'] == 1 ? 'Unpublish' : 'Publish';
|
</div>
|
||||||
$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>
|
|
||||||
<button class="btn btn-sm ' . $publishButtonClass . ' toggle-publish" data-event-id="' . $event['event_id'] . '" title="' . $publishButtonText . '">
|
|
||||||
<i class="far fa-' . ($event['published'] == 1 ? 'eye-slash' : 'eye') . '"></i>
|
|
||||||
</button>
|
|
||||||
<button class="btn btn-sm btn-danger delete-event" data-event-id="' . $event['event_id'] . '" title="Delete">
|
|
||||||
<i class="far fa-trash"></i>
|
|
||||||
</button>
|
|
||||||
</td>
|
|
||||||
</tr>';
|
|
||||||
}
|
|
||||||
echo '</tbody></table>';
|
|
||||||
echo '</div>';
|
|
||||||
echo '</div>';
|
|
||||||
} else {
|
|
||||||
echo '<p>No events found. <a href="manage_events">Create one</a></p>';
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
</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'); ?>
|
||||||
|
|||||||
@@ -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,131 +31,48 @@ 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", () => {
|
filterInput.style.display = "none";
|
||||||
const sortedRows = rows.sort((a, b) => {
|
} else if (filterInput) {
|
||||||
const aText = a.cells[index].textContent.trim().toLowerCase();
|
filterInput.addEventListener("input", function() {
|
||||||
const bText = b.cells[index].textContent.trim().toLowerCase();
|
const filterValue = filterInput.value.trim().toLowerCase();
|
||||||
|
cards.forEach(card => {
|
||||||
if (aText < bText) return -1;
|
const cardText = card.textContent.trim().toLowerCase();
|
||||||
if (aText > bText) return 1;
|
card.style.display = cardText.includes(filterValue) ? "" : "none";
|
||||||
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";
|
|
||||||
} else {
|
|
||||||
filterInput.addEventListener("input", function() {
|
|
||||||
const filterValue = filterInput.value.trim().toLowerCase();
|
|
||||||
rows.forEach(row => {
|
|
||||||
const rowText = row.textContent.trim().toLowerCase();
|
|
||||||
row.style.display = rowText.includes(filterValue) ? "" : "none";
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -155,164 +80,163 @@ if ($result && $result->num_rows > 0) {
|
|||||||
$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
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<?php
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 30px;">
|
||||||
if (count($trips) > 0) {
|
<h2 style="margin: 0;">Manage Trips</h2>
|
||||||
echo '<input type="text" class="filter-input" placeholder="Filter trips...">';
|
<a href="manage_trips" class="theme-btn create-album-btn">
|
||||||
echo '<div class="trips-section" data-aos="fade-up" data-aos-duration="1500" data-aos-offset="50">';
|
<i class="far fa-plus"></i> New Event
|
||||||
echo '<div style="padding:10px;">';
|
</a>
|
||||||
echo '<table>
|
</div>
|
||||||
<thead>
|
<?php if (isset($_SESSION['message'])): ?>
|
||||||
<tr>
|
<div class="alert alert-warning message-box">
|
||||||
<th>Trip Name</th>
|
<?php echo $_SESSION['message']; ?>
|
||||||
<th>Location</th>
|
<span class="close-btn" onclick="this.parentElement.style.display='none'">×</span>
|
||||||
<th>Start Date</th>
|
</div>
|
||||||
<th>End Date</th>
|
<?php unset($_SESSION['message']);
|
||||||
<th>Capacity</th>
|
endif;
|
||||||
<th>Booked</th>
|
if (count($trips) > 0) {
|
||||||
<th>Cost (Member)</th>
|
echo '<input type="text" class="filter-input" placeholder="Filter trips...">';
|
||||||
<th>Status</th>
|
echo '<div class="trips-container" data-aos="fade-up" data-aos-duration="1500" data-aos-offset="50">';
|
||||||
<th>Actions</th>
|
|
||||||
</tr>
|
foreach ($trips as $trip) {
|
||||||
</thead>
|
$available = $trip['vehicle_capacity'] - $trip['places_booked'];
|
||||||
<tbody>';
|
$publishStatus = $trip['published'] == 1 ? 'published' : 'draft';
|
||||||
foreach ($trips as $trip) {
|
$publishStatusBadge = $trip['published'] == 1 ? 'PUBLISHED' : 'DRAFT';
|
||||||
$publishButtonText = $trip['published'] == 1 ? 'Unpublish' : 'Publish';
|
|
||||||
$publishButtonClass = $trip['published'] == 1 ? 'btn-warning' : 'btn-success';
|
// Get trip image - look for assets/images/trips/$trip_id_{number}.jpg
|
||||||
echo '<tr>
|
$tripImagePath = '';
|
||||||
<td><strong>' . htmlspecialchars($trip['trip_name']) . '</strong></td>
|
$tripImagesGlob = glob($rootPath . '/assets/images/trips/' . $trip['trip_id'] . '_*.jpg');
|
||||||
<td>' . htmlspecialchars($trip['location']) . '</td>
|
if (!empty($tripImagesGlob)) {
|
||||||
<td>' . date('M d, Y', strtotime($trip['start_date'])) . '</td>
|
$tripImagePath = str_replace($rootPath, '', $tripImagesGlob[0]);
|
||||||
<td>' . date('M d, Y', strtotime($trip['end_date'])) . '</td>
|
} else {
|
||||||
<td>' . $trip['vehicle_capacity'] . '</td>
|
// Fallback to placeholder icon if no image found
|
||||||
<td><span class="badge bg-info">' . $trip['places_booked'] . ' / ' . $trip['vehicle_capacity'] . '</span></td>
|
$tripImagePath = 'assets/images/placeholder.jpg';
|
||||||
<td>R ' . number_format($trip['cost_members'], 2) . '</td>
|
}
|
||||||
<td>' . ($trip['published'] == 1 ? '<span class="badge bg-success">Published</span>' : '<span class="badge bg-warning">Draft</span>') . '</td>
|
|
||||||
<td>
|
echo '
|
||||||
<a href="manage_trips?trip_id=' . $trip['trip_id'] . '" class="btn btn-sm btn-primary" title="Edit">
|
<div class="destination-item style-three bgc-lighter booking" data-aos="fade-up" data-aos-duration="1500" data-aos-offset="50">
|
||||||
<i class="far fa-edit"></i>
|
<div class="image" style="width:300px;height:250px;">
|
||||||
</a>
|
<img src="' . htmlspecialchars($tripImagePath) . '" alt="' . htmlspecialchars($trip['trip_name']) . '">
|
||||||
<button class="btn btn-sm ' . $publishButtonClass . ' toggle-publish" data-trip-id="' . $trip['trip_id'] . '" title="' . $publishButtonText . '">
|
</div>
|
||||||
<i class="far fa-' . ($trip['published'] == 1 ? 'eye-slash' : 'eye') . '"></i>
|
<div class="content" style="width:100%;">
|
||||||
</button>
|
<div class="destination-header d-flex align-items-start gap-3">
|
||||||
<button class="btn btn-sm btn-danger delete-trip" data-trip-id="' . $trip['trip_id'] . '" title="Delete">
|
<div>
|
||||||
<i class="far fa-trash"></i>
|
<span class="badge bg-dark mb-1">' . strtoupper($publishStatusBadge) . '</span>
|
||||||
</button>
|
<h5 class="mb-0">' . htmlspecialchars($trip['trip_name']) . '</h5>
|
||||||
</td>
|
<small class="text-muted">📍 ' . htmlspecialchars($trip['location']) . '</small>
|
||||||
</tr>';
|
</div>
|
||||||
}
|
</div>
|
||||||
echo '</tbody></table>';
|
<p style="margin: 10px 0;">
|
||||||
echo '</div>';
|
<strong>Dates:</strong> ' . date('M d', strtotime($trip['start_date'])) . ' - ' . date('M d, Y', strtotime($trip['end_date'])) . '<br>
|
||||||
echo '</div>';
|
<strong>Capacity:</strong> ' . $trip['places_booked'] . ' / ' . $trip['vehicle_capacity'] . '<br>
|
||||||
} else {
|
<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) . '
|
||||||
echo '<p>No trips found. <a href="manage_trips">Create one</a></p>';
|
</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>';
|
||||||
|
} else {
|
||||||
|
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())
|
||||||
} else {
|
.then(data => {
|
||||||
button.removeClass('btn-warning').addClass('btn-success');
|
if (data.status === 'success') {
|
||||||
button.find('i').removeClass('fa-eye-slash').addClass('fa-eye');
|
alert(action + ' successful!');
|
||||||
button.attr('title', 'Publish');
|
location.reload();
|
||||||
// Update status badge
|
|
||||||
row.find('td:nth-child(8)').html('<span class="badge bg-warning">Draft</span>');
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
alert('Error: ' + response.message);
|
alert(action + ' failed: ' + data.message);
|
||||||
}
|
}
|
||||||
},
|
})
|
||||||
error: function() {
|
.catch(err => {
|
||||||
alert('Error updating trip status');
|
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() {
|
.catch(err => {
|
||||||
alert('Error deleting trip');
|
console.error('Error:', err);
|
||||||
}
|
alert('Delete failed due to network error.');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -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();
|
|
||||||
46
src/api/track/get_obstable.php
Normal 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
|
||||||
@@ -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,18 +2469,21 @@ 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
$mimeType = finfo_file($finfo, $file['tmp_name']);
|
if (function_exists('finfo_open')) {
|
||||||
finfo_close($finfo);
|
$finfo = finfo_open(FILEINFO_MIME_TYPE);
|
||||||
|
if ($finfo !== false) {
|
||||||
|
$mimeType = finfo_file($finfo, $file['tmp_name']);
|
||||||
|
finfo_close($finfo);
|
||||||
|
|
||||||
if (!in_array($mimeType, $config['mimeTypes'], true)) {
|
if (!in_array($mimeType, $config['mimeTypes'], true)) {
|
||||||
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) =====
|
||||||
|
|||||||
1
src/logs/db_errors.log
Normal 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
|
||||||
@@ -76,8 +76,8 @@ $bannerImages = glob($bannerFolder . '*.{jpg,jpeg,png,webp}', GLOB_BRACE);
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-lg-12">
|
<div class="col-lg-12">
|
||||||
|
|
||||||
<h2>Manage Blog Posts</h2>
|
<h2>Manage Blog Posts</h2>
|
||||||
<?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'">×</span>
|
<span class="close-btn" onclick="this.parentElement.style.display='none'">×</span>
|
||||||
@@ -90,7 +90,7 @@ $bannerImages = glob($bannerFolder . '*.{jpg,jpeg,png,webp}', GLOB_BRACE);
|
|||||||
// 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
|
||||||
echo '
|
echo '
|
||||||
<div class="destination-item style-three bgc-lighter booking" data-aos="fade-up" data-aos-duration="1500" data-aos-offset="50">
|
<div class="destination-item style-three bgc-lighter booking" data-aos="fade-up" data-aos-duration="1500" data-aos-offset="50">
|
||||||
<div class="image" style="width:200px;height:200px;">
|
<div class="image" style="width:200px;height:200px;">
|
||||||
<img src="' . htmlspecialchars($coverImage) . '" alt="' . htmlspecialchars($post["title"]) . '">
|
<img src="' . htmlspecialchars($coverImage) . '" alt="' . htmlspecialchars($post["title"]) . '">
|
||||||
@@ -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,40 +125,40 @@ $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));
|
||||||
|
|
||||||
// Handle publish/unpublish button clicks
|
// Handle publish/unpublish button clicks
|
||||||
document.querySelectorAll('.publish-btn').forEach(btn => {
|
document.querySelectorAll('.publish-btn').forEach(btn => {
|
||||||
btn.addEventListener('click', function() {
|
btn.addEventListener('click', function() {
|
||||||
const blogId = this.dataset.blogId;
|
const blogId = this.dataset.blogId;
|
||||||
const status = this.dataset.status;
|
const status = this.dataset.status;
|
||||||
const action = status === 'published' ? 'unpublish' : 'publish';
|
const action = status === 'published' ? 'unpublish' : 'publish';
|
||||||
const endpoint = status === 'published' ? 'blog_unpublish' : 'publish_blog';
|
const endpoint = status === 'published' ? 'blog_unpublish' : 'publish_blog';
|
||||||
|
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('id', blogId);
|
formData.append('id', blogId);
|
||||||
|
|
||||||
fetch(endpoint, {
|
fetch(endpoint, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: formData
|
body: formData
|
||||||
})
|
})
|
||||||
.then(response => {
|
.then(response => {
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
alert(action.charAt(0).toUpperCase() + action.slice(1) + ' successful!');
|
alert(action.charAt(0).toUpperCase() + action.slice(1) + ' successful!');
|
||||||
location.reload();
|
location.reload();
|
||||||
} else {
|
} else {
|
||||||
alert(action + ' failed.');
|
alert(action + ' failed.');
|
||||||
console.error('Error:', response.statusText);
|
console.error('Error:', response.statusText);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
console.error('Error:', err);
|
console.error('Error:', err);
|
||||||
alert(action + ' failed due to network error.');
|
alert(action + ' failed due to network error.');
|
||||||
});
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
return false;
|
console.error("Autosave failed", response.status, errorText);
|
||||||
|
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;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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');
|
||||||
@@ -19,63 +32,58 @@ $posts = $result->get_result();
|
|||||||
?>
|
?>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.image {
|
.image {
|
||||||
width: 400px;
|
width: 400px;
|
||||||
/* Set your desired width */
|
/* Set your desired width */
|
||||||
height: 350px;
|
height: 350px;
|
||||||
/* Set your desired height */
|
/* Set your desired height */
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
/* Hide any overflow */
|
/* Hide any overflow */
|
||||||
display: block;
|
display: block;
|
||||||
/* Ensure proper block behavior */
|
/* Ensure proper block behavior */
|
||||||
}
|
}
|
||||||
|
|
||||||
.image img {
|
.image img {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
/* Image scales to fill the container */
|
/* Image scales to fill the container */
|
||||||
height: 100%;
|
height: 100%;
|
||||||
/* Image scales to fill the container */
|
/* Image scales to fill the container */
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
/* Fills the container while maintaining aspect ratio */
|
/* Fills the container while maintaining aspect ratio */
|
||||||
object-position: top;
|
object-position: top;
|
||||||
/* Aligns the top of the image with the top of the container */
|
/* Aligns the top of the image with the top of the container */
|
||||||
display: block;
|
display: block;
|
||||||
/* Prevents inline whitespace issues */
|
/* Prevents inline whitespace issues */
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
</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;">
|
||||||
<?php if (isset($_SESSION['message'])): ?>
|
<h2 style="margin: 0;">My Blog Posts</h2>
|
||||||
<div class="alert alert-warning message-box">
|
<a href="blog_create" class="theme-btn create-album-btn">
|
||||||
<?php echo $_SESSION['message']; ?>
|
<i class="far fa-plus"></i> Create New Post
|
||||||
<span class="close-btn" onclick="this.parentElement.style.display='none'">×</span>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<?php unset($_SESSION['message']); ?>
|
<?php if (isset($_SESSION['message'])): ?>
|
||||||
<?php endif; ?>
|
<div class="alert alert-warning message-box">
|
||||||
<a href="blog_create.php">+ New Post</a>
|
<?php echo $_SESSION['message']; ?>
|
||||||
|
<span class="close-btn" onclick="this.parentElement.style.display='none'">×</span>
|
||||||
|
</div>
|
||||||
|
<?php unset($_SESSION['message']);
|
||||||
|
endif;
|
||||||
|
|
||||||
<?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
|
||||||
echo '
|
echo '
|
||||||
<div class="destination-item style-three bgc-lighter booking" data-aos="fade-up" data-aos-duration="1500" data-aos-offset="50">
|
<div class="destination-item style-three bgc-lighter booking" data-aos="fade-up" data-aos-duration="1500" data-aos-offset="50">
|
||||||
<div class="image" style="width:200px;height:200px;"><img src="' . htmlspecialchars($coverImage) . '" alt="' . htmlspecialchars($post["title"]) . '"></div>
|
<div class="image" style="width:200px;height:200px;"><img src="' . htmlspecialchars($coverImage) . '" alt="' . htmlspecialchars($post["title"]) . '"></div>
|
||||||
<div class="content" style="width:100%;">
|
<div class="content" style="width:100%;">
|
||||||
@@ -88,25 +96,25 @@ $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>
|
||||||
</div>';
|
</div>';
|
||||||
endwhile; ?>
|
endwhile; ?>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
</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));
|
||||||
|
|
||||||
@@ -122,22 +130,22 @@ $bannerImages = glob($bannerFolder . '*.{jpg,jpeg,png,webp}', GLOB_BRACE);
|
|||||||
formData.append('id', blogId);
|
formData.append('id', blogId);
|
||||||
|
|
||||||
fetch(endpoint, {
|
fetch(endpoint, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: formData
|
body: formData
|
||||||
})
|
})
|
||||||
.then(response => {
|
.then(response => {
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
alert(action.charAt(0).toUpperCase() + action.slice(1) + ' successful!');
|
alert(action.charAt(0).toUpperCase() + action.slice(1) + ' successful!');
|
||||||
location.reload();
|
location.reload();
|
||||||
} else {
|
} else {
|
||||||
alert(action + ' failed.');
|
alert(action + ' failed.');
|
||||||
console.error('Error:', response.statusText);
|
console.error('Error:', response.statusText);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
console.error('Error:', err);
|
console.error('Error:', err);
|
||||||
alert(action + ' failed due to network error.');
|
alert(action + ' failed due to network error.');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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; ?>
|
||||||
|
|||||||
@@ -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
@@ -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(); ?>
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
76
src/processors/delete_event.php
Normal 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()]);
|
||||||
|
}
|
||||||
@@ -174,28 +174,34 @@ 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
|
||||||
|
$end_year = ($month == 12) ? $year + 2 : $year + 1;
|
||||||
|
$membership_end_date = (new DateTime("$end_year-02-01"))
|
||||||
|
->modify('last day of this month')
|
||||||
|
->format('Y-m-d');
|
||||||
} else {
|
} else {
|
||||||
// Otherwise, this year's February
|
// Prorata for Mar-Nov
|
||||||
$year = $today->format('Y');
|
$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');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle leap year (Feb 29) automatically
|
|
||||||
$membership_end_date = (new DateTime("$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', ?)");
|
||||||
$stmt->bind_param("idssss", $user_id, $payment_amount, $payment_date, $membership_start_date, $membership_end_date, $eft_id);
|
$stmt->bind_param("idssss", $user_id, $payment_amount, $payment_date, $membership_start_date, $membership_end_date, $eft_id);
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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'];
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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)) {
|
||||||
|
|||||||
164
src/processors/track-obstacles.php
Normal 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();
|
||||||
|
?>
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
BIN
uploads/pop/103_SUBS_2025_E._BESTER.pdf
Normal file
BIN
uploads/pop/105_SUBS_2025_D._KLADIS.pdf
Normal file
BIN
uploads/pop/109_SUBS_2025_A._MAHON.pdf
Normal file
|
After Width: | Height: | Size: 42 KiB |
BIN
uploads/pop/122_SUBS_2025_M._BUYS_.pdf
Normal file
BIN
uploads/pop/127_SUBS_2025_J._MATTHEUS.pdf
Normal file
BIN
uploads/pop/129_SUBS_2025_C._DE_JESUS.pdf
Normal file
BIN
uploads/pop/130_SUBS_2025_J._HALL.pdf
Normal file
BIN
uploads/pop/134_SUBS_2025_J._EARLE.pdf
Normal file
BIN
uploads/pop/142_SUBS_2025_N._COETZEE.pdf
Normal file
BIN
uploads/pop/COURSE_07-26_D._KLADIS.pdf
Normal file
BIN
uploads/pop/COURSE_07-26_K._SKEE.pdf
Normal file
BIN
uploads/pop/COURSE_07-26_M._MABASO.pdf
Normal file
BIN
uploads/pop/COURSE_07-26_M._NICHOLLS.pdf
Normal file
BIN
uploads/pop/COURSE_08-23_A._FERENCZY_.pdf
Normal file
BIN
uploads/pop/COURSE_08-23_E._HOLTZHAUSEN.pdf
Normal file
BIN
uploads/pop/COURSE_09-20_I._KOORSEN.pdf
Normal file
BIN
uploads/signatures/signature_103.png
Normal file
|
After Width: | Height: | Size: 8.0 KiB |
BIN
uploads/signatures/signature_105.png
Normal file
|
After Width: | Height: | Size: 7.2 KiB |
BIN
uploads/signatures/signature_109.png
Normal file
|
After Width: | Height: | Size: 6.7 KiB |
BIN
uploads/signatures/signature_122.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
uploads/signatures/signature_123.png
Normal file
|
After Width: | Height: | Size: 6.7 KiB |
BIN
uploads/signatures/signature_126.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
uploads/signatures/signature_127.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
uploads/signatures/signature_129.png
Normal file
|
After Width: | Height: | Size: 8.2 KiB |
BIN
uploads/signatures/signature_130.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
uploads/signatures/signature_131.png
Normal file
|
After Width: | Height: | Size: 5.5 KiB |
BIN
uploads/signatures/signature_134.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
uploads/signatures/signature_136.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
uploads/signatures/signature_137.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
uploads/signatures/signature_142.png
Normal file
|
After Width: | Height: | Size: 6.8 KiB |
BIN
uploads/signatures/signature_143.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
uploads/signatures/signature_144.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
uploads/signatures/signature_147.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
uploads/signatures/signature_152.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
uploads/signatures/signature_153.png
Normal file
|
After Width: | Height: | Size: 5.1 KiB |
BIN
uploads/signatures/signature_161.png
Normal file
|
After Width: | Height: | Size: 6.9 KiB |
BIN
uploads/signatures/signature_93.png
Normal file
|
After Width: | Height: | Size: 7.1 KiB |
BIN
uploads/signatures/signature_97.png
Normal file
|
After Width: | Height: | Size: 8.3 KiB |
BIN
uploads/signatures/signature_98.png
Normal file
|
After Width: | Height: | Size: 5.8 KiB |