Compare commits
3 Commits
d5feaacddf
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9653443c09 | ||
|
|
782d343243 | ||
|
|
c618fd4506 |
@@ -30,6 +30,7 @@ RewriteRule ^membership$ src/pages/memberships/membership.php [L]
|
||||
RewriteRule ^membership_details$ src/pages/memberships/membership_details.php [L]
|
||||
RewriteRule ^membership_application$ src/pages/memberships/membership_application.php [L]
|
||||
RewriteRule ^membership_payment$ src/pages/memberships/membership_payment.php [L]
|
||||
RewriteRule ^renewal_payment$ src/pages/memberships/renewal_payment.php [L]
|
||||
RewriteRule ^renew_membership$ src/pages/memberships/renew_membership.php [L]
|
||||
RewriteRule ^member_info$ src/pages/memberships/member_info.php [L]
|
||||
|
||||
@@ -68,6 +69,7 @@ RewriteRule ^instapage$ src/pages/events/instapage.php [L]
|
||||
|
||||
# === OTHER PAGES ===
|
||||
RewriteRule ^about$ src/pages/other/about.php [L]
|
||||
RewriteRule ^base4$ src/pages/other/base4.php [L]
|
||||
RewriteRule ^contact$ src/pages/other/contact.php [L]
|
||||
RewriteRule ^privacy_policy$ src/pages/other/privacy_policy.php [L]
|
||||
RewriteRule ^track-map$ src/pages/track-map.php [L]
|
||||
|
||||
BIN
assets/images/base4/01.jpeg
Normal file
BIN
assets/images/base4/01.jpeg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 199 KiB |
@@ -260,7 +260,7 @@ if ($headerStyle === 'light') {
|
||||
<ul class="navigation clearfix">
|
||||
<li><a href="index">Home</a></li>
|
||||
<li><a href="about">About</a></li>
|
||||
<li><a href="track-map">Track Map</a></li>
|
||||
<li><a href="base4">BASE 4</a></li>
|
||||
<li><a href="trips">Trips</a>
|
||||
<?php if ($headerStyle === 'dark'): ?>
|
||||
<ul>
|
||||
|
||||
41
index.php
41
index.php
@@ -27,17 +27,38 @@ if ($showRenewModal) {
|
||||
} else {
|
||||
$showRenewModal = false;
|
||||
}
|
||||
|
||||
$user_id = $_SESSION['user_id'];
|
||||
|
||||
// Ensure we have a DB connection
|
||||
if (!isset($conn) || $conn === null) {
|
||||
$showRenewModal = false;
|
||||
} else {
|
||||
$stmt = $conn->prepare("SELECT payment_status FROM membership_fees WHERE user_id = ? LIMIT 1");
|
||||
$stmt->bind_param("i", $user_id);
|
||||
$stmt->execute();
|
||||
// store_result so we can check num_rows
|
||||
$stmt->store_result();
|
||||
|
||||
// If there's no membership_fees record for this user, don't show the renew modal
|
||||
if ($stmt->num_rows === 0) {
|
||||
$showRenewModal = false;
|
||||
} else {
|
||||
$stmt->bind_result($payment_status);
|
||||
$stmt->fetch();
|
||||
$stmt->close();
|
||||
|
||||
if ($payment_status === 'PENDING RENEWAL') {
|
||||
$showRenewModal = false;
|
||||
}
|
||||
if (isMembershipExpiringSoon($user_id)) {
|
||||
$showRenewModal = true;
|
||||
} else {
|
||||
$showRenewModal = false;
|
||||
}
|
||||
}
|
||||
|
||||
$stmt->close();
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($_SESSION['user_id']) && isset($conn) && $conn !== null) {
|
||||
@@ -726,7 +747,7 @@ if (countUpcomingTrips() > 0) { ?>
|
||||
</div> -->
|
||||
<div class="modal-body">
|
||||
Your membership will be expiring soon. Click below to renew now.
|
||||
<a style="width:100%; display:block;" href="renew_membership" class="theme-btn style-two style-three mt-3">Renew Now</a>
|
||||
<a style="width:100%; display:block;" href="renewal_payment" class="theme-btn style-two style-three mt-3">Renew Now</a>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" style="width:100%; display:block;" class="theme-btn" data-bs-dismiss="modal">Remind Me Later</button>
|
||||
@@ -784,6 +805,7 @@ if (countUpcomingTrips() > 0) { ?>
|
||||
background-color: rgba(0, 0, 0, 0.6);
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
animation: fadeIn 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
@@ -805,6 +827,10 @@ if (countUpcomingTrips() > 0) { ?>
|
||||
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
|
||||
animation: slideDown 0.3s ease-out;
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
/* Limit height so the modal never exceeds the viewport and allow internal scrolling */
|
||||
max-height: calc(100vh - 80px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
@keyframes slideDown {
|
||||
@@ -890,8 +916,17 @@ if (countUpcomingTrips() > 0) { ?>
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.updates-modal {
|
||||
/* Align to top on small screens so content's top (and close button) is visible */
|
||||
align-items: flex-start;
|
||||
padding-top: 18px;
|
||||
}
|
||||
|
||||
.updates-modal-content {
|
||||
padding: 30px 20px;
|
||||
padding: 20px;
|
||||
max-width: 92%;
|
||||
width: 92%;
|
||||
max-height: calc(100vh - 36px);
|
||||
}
|
||||
|
||||
.updates-modal-header h2 {
|
||||
|
||||
@@ -29,6 +29,106 @@ function openDatabaseConnection()
|
||||
return $conn;
|
||||
}
|
||||
|
||||
//function to determine whether membership_end_date is within 3 months from current date where user_id = ?, if so return true, else false
|
||||
function isMembershipExpiringSoon($user_id)
|
||||
{
|
||||
$conn = openDatabaseConnection();
|
||||
if ($conn === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$stmt = $conn->prepare("SELECT membership_end_date FROM membership_fees WHERE user_id = ? LIMIT 1");
|
||||
if (!$stmt) {
|
||||
$conn->close();
|
||||
return false;
|
||||
}
|
||||
|
||||
$stmt->bind_param('i', $user_id);
|
||||
$stmt->execute();
|
||||
$result = $stmt->get_result();
|
||||
|
||||
if ($result->num_rows === 0) {
|
||||
$stmt->close();
|
||||
$conn->close();
|
||||
return false;
|
||||
}
|
||||
|
||||
$row = $result->fetch_assoc();
|
||||
$membership_end_date = new DateTime($row['membership_end_date']);
|
||||
$current_date = new DateTime();
|
||||
$interval = $current_date->diff($membership_end_date);
|
||||
|
||||
$stmt->close();
|
||||
$conn->close();
|
||||
|
||||
return ($interval->days <= 90 && $membership_end_date > $current_date);
|
||||
}
|
||||
|
||||
function normalizeName($name) {
|
||||
$name = strtolower($name);
|
||||
$name = preg_replace("/[^a-z\s]/", "", $name); // remove punctuation
|
||||
$name = preg_replace("/\s+/", " ", $name); // normalize spaces
|
||||
return trim($name);
|
||||
}
|
||||
|
||||
// function to checkif first name + last name matches names in an array. names may have slight variations or spelling mistakes.
|
||||
function validateHonoraryMemberName($first_name, $last_name)
|
||||
{
|
||||
$honorary_names = [
|
||||
"robin hood",
|
||||
"marc rademaker",
|
||||
"clive robinson",
|
||||
"joern kuebler",
|
||||
"maurice compton",
|
||||
"jenny cole",
|
||||
"geoff joubert",
|
||||
"alan exton",
|
||||
"dave bell",
|
||||
"karl hoffman",
|
||||
"gerald obrian"
|
||||
// Add more honorary member names as needed
|
||||
];
|
||||
|
||||
$full_name = normalizeName($first_name . ' ' . $last_name);
|
||||
$full_name = strtolower($full_name);
|
||||
foreach ($honorary_names as $name) {
|
||||
similar_text($full_name, strtolower(normalizeName($name)), $percent);
|
||||
if ($percent >= 80) { // 80% similarity threshold
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
//get membership_type from membership_applications table for user_id = ?
|
||||
function getMembershipType($user_id)
|
||||
{
|
||||
$conn = openDatabaseConnection();
|
||||
if ($conn === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$stmt = $conn->prepare("SELECT membership_type FROM membership_applications WHERE user_id = ? ORDER BY id DESC LIMIT 1");
|
||||
if (!$stmt) {
|
||||
$conn->close();
|
||||
return null;
|
||||
}
|
||||
|
||||
$stmt->bind_param('i', $user_id);
|
||||
$stmt->execute();
|
||||
$stmt->bind_result($membership_type);
|
||||
if ($stmt->fetch()) {
|
||||
$stmt->close();
|
||||
$conn->close();
|
||||
return $membership_type;
|
||||
} else {
|
||||
$stmt->close();
|
||||
$conn->close();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function progress_log($message, $context = null)
|
||||
{
|
||||
try {
|
||||
|
||||
@@ -7,7 +7,7 @@ checkUserSession();
|
||||
// Assuming you have the user ID stored in the session
|
||||
if (isset($_SESSION['user_id'])) {
|
||||
$user_id = $_SESSION['user_id'];
|
||||
}else{
|
||||
} else {
|
||||
header('Location: login.php');
|
||||
exit(); // Stop further script execution
|
||||
}
|
||||
@@ -25,7 +25,7 @@ $user = $result->fetch_assoc();
|
||||
$pageTitle = 'Membership Application';
|
||||
$breadcrumbs = [['Home' => 'index.php'], ['Membership' => 'membership.php']];
|
||||
require_once($rootPath . '/components/banner.php');
|
||||
?>
|
||||
?>
|
||||
|
||||
|
||||
|
||||
@@ -39,6 +39,31 @@ $user = $result->fetch_assoc();
|
||||
<div class="section-title">
|
||||
<div id="responseMessage"></div> <!-- Message display area -->
|
||||
</div>
|
||||
<!-- Membership Type -->
|
||||
<h3>Membership Type</h3>
|
||||
<div class="row mt-35">
|
||||
<div class="col-md-12">
|
||||
<div class="form-group mb-3">
|
||||
<div class="form-check">
|
||||
<input type="checkbox" id="country_membership" name="country_membership" value="1">
|
||||
<label style="margin-left:20px;" for="country_membership">Country Membership - if you reside more than 150km from BASE4 and qualify for country membership.</label>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<div class="form-check form-check-inline">
|
||||
<input type="radio" name="membership_type" id="membership_full" value="full" checked>
|
||||
<label style="margin-left:20px;" for="membership_full">Full Membership</label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline">
|
||||
<input type="radio" name="membership_type" id="membership_single" value="single">
|
||||
<label style="margin-left:20px;" for="membership_single">Single Membership</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<!-- Personal Details Section -->
|
||||
<h3>Main Member</h3>
|
||||
<div class="row mt-35">
|
||||
@@ -88,6 +113,7 @@ $user = $result->fetch_assoc();
|
||||
</div>
|
||||
|
||||
<!-- Spouse / Partner Details Section -->
|
||||
<div id="spouseSection">
|
||||
<h3>Spouse / Life Partner / Other Details</h3>
|
||||
<div class="row mt-35">
|
||||
<div class="col-md-6">
|
||||
@@ -135,7 +161,11 @@ $user = $result->fetch_assoc();
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div> <!-- end spouse row -->
|
||||
<!-- </div> end spouseSection -->
|
||||
|
||||
<!-- Children Section -->
|
||||
<div id="childrenSection">
|
||||
<h3>Children's Names</h3>
|
||||
<div class="row mt-35">
|
||||
<div class="col-md-6">
|
||||
@@ -176,6 +206,7 @@ $user = $result->fetch_assoc();
|
||||
</div>
|
||||
<!-- Repeat for other children if needed -->
|
||||
</div>
|
||||
</div> <!-- end childrenSection -->
|
||||
|
||||
<!-- Address Section -->
|
||||
<h3>Address</h3>
|
||||
@@ -282,3 +313,43 @@ $user = $result->fetch_assoc();
|
||||
|
||||
|
||||
<?php include_once(dirname(dirname(dirname(__DIR__))) . '/components/insta_footer.php'); ?>
|
||||
|
||||
<script>
|
||||
// Toggle spouse and children sections when 'Single Membership' is selected
|
||||
(function() {
|
||||
function setSectionState(isSingle) {
|
||||
var spouse = document.getElementById('spouseSection');
|
||||
var children = document.getElementById('childrenSection');
|
||||
[spouse, children].forEach(function(sec) {
|
||||
if (!sec) return;
|
||||
var inputs = sec.querySelectorAll('input, select, textarea, button');
|
||||
if (isSingle) {
|
||||
sec.style.display = 'none';
|
||||
inputs.forEach(function(i) {
|
||||
i.disabled = true;
|
||||
});
|
||||
} else {
|
||||
sec.style.display = '';
|
||||
inputs.forEach(function(i) {
|
||||
i.disabled = false;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
var full = document.getElementById('membership_full');
|
||||
var single = document.getElementById('membership_single');
|
||||
|
||||
// initialize state
|
||||
setSectionState(single && single.checked);
|
||||
|
||||
if (full) full.addEventListener('change', function() {
|
||||
if (this.checked) setSectionState(false);
|
||||
});
|
||||
if (single) single.addEventListener('change', function() {
|
||||
if (this.checked) setSectionState(true);
|
||||
});
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
@@ -189,7 +189,7 @@ if (empty($application['id_number'])) {
|
||||
<td><?php echo htmlspecialchars($membership['payment_amount']); ?></td>
|
||||
<td><?php echo htmlspecialchars($membership['payment_id']); ?></td>
|
||||
<?php if ($membership['payment_status'] == "AWAITING PAYMENT" || $membership['payment_status'] == "PENDING RENEWAL") { ?>
|
||||
<td><a href='<?= $payment_link; ?>' class='theme-btn style-two style-three' style='padding: 0px 14px;'><span data-hover='PAY NOW'>PENDING RENEWAL</span></a></td>
|
||||
<td><a href='<?= $payment_link; ?>' class='theme-btn style-two style-three' style='padding: 0px 14px;'><span data-hover='<?= $membership['payment_status'] ?>'><?= $membership['payment_status'] ?></span></a></td>
|
||||
<?php } else { ?>
|
||||
<td><?php echo htmlspecialchars($membership['payment_status']); ?></td>
|
||||
<?php } ?>
|
||||
@@ -221,7 +221,7 @@ if (empty($application['id_number'])) {
|
||||
|
||||
if (strtotime($today) >= strtotime($threeMonthsBefore)) {
|
||||
echo '
|
||||
<a href="renew_membership" class="theme-btn style-two bgc-secondary" style="width:100%; margin-top: 20px; background-color: #63ab45; padding: 10px 20px; color: white; text-decoration: none; border-radius: 25px;">
|
||||
<a href="renewal_payment" class="theme-btn style-two bgc-secondary" style="width:100%; margin-top: 20px; background-color: #63ab45; padding: 10px 20px; color: white; text-decoration: none; border-radius: 25px;">
|
||||
<span data-hover="Renew Membership">Renew Membership</span>
|
||||
<i class="fal fa-arrow-right"></i>
|
||||
</a>';
|
||||
|
||||
@@ -14,6 +14,37 @@ if (isset($_SESSION['user_id'])) {
|
||||
|
||||
$full_name = getFullName($user_id);
|
||||
|
||||
if (isset($_POST['membership_type'])) {
|
||||
$membership_type = $_POST['membership_type'];
|
||||
echo $membership_type;
|
||||
//update membership_type in membership_application
|
||||
$stmt = $conn->prepare("UPDATE membership_application SET membership_type = ? WHERE user_id = ? ");
|
||||
$stmt->bind_param("si", $membership_type, $user_id);
|
||||
$stmt->execute();
|
||||
$stmt->close();
|
||||
} else {
|
||||
//get user membership type from membership_applications
|
||||
$stmt = $conn->prepare("SELECT membership_type FROM membership_application WHERE user_id = ? ");
|
||||
$stmt->bind_param("i", $user_id);
|
||||
$stmt->execute();
|
||||
$stmt->bind_result($membership_type);
|
||||
$stmt->fetch();
|
||||
$stmt->close();
|
||||
}
|
||||
|
||||
//check memberhsip_applications for user_id, if 0 rows, redirect to membership_application.php
|
||||
$stmt = $conn->prepare("SELECT COUNT(*) AS cnt FROM membership_application WHERE user_id = ? LIMIT 1");
|
||||
$stmt->bind_param("i", $user_id);
|
||||
|
||||
$stmt->execute();
|
||||
$stmt->bind_result($application_count);
|
||||
$stmt->fetch();
|
||||
$stmt->close();
|
||||
if ($application_count == 0) {
|
||||
header("Location: membership_application.php");
|
||||
exit();
|
||||
}
|
||||
|
||||
//if membership_fees payment_status is PENDING RENEWAL, redirect to membership_details.php
|
||||
$stmt = $conn->prepare("SELECT payment_status FROM membership_fees WHERE user_id = ? LIMIT 1");
|
||||
$stmt->bind_param("i", $user_id);
|
||||
@@ -27,8 +58,25 @@ if ($payment_status === 'PENDING RENEWAL') {
|
||||
exit();
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
if ($membership_type === 'country') {
|
||||
$payment_amount = getPriceByDescription('country_membership');
|
||||
} elseif ($membership_type === 'single') {
|
||||
$payment_amount = getPriceByDescription('single');
|
||||
} else {
|
||||
$payment_amount = getPriceByDescription('membership_fees');
|
||||
}
|
||||
|
||||
if ($membership_type === 'honorary') {
|
||||
// Honorary members do not pay fees, redirect to membership details
|
||||
header("Location: membership_details.php");
|
||||
exit();
|
||||
|
||||
}
|
||||
|
||||
$payment_id = generatePaymentRef('SUBS', null, $user_id);
|
||||
$payment_amount = getPriceByDescription('membership_fees');
|
||||
$payment_date = date('Y-m-d');
|
||||
$renewal_period_end = getMembershipEndDate($user_id);
|
||||
// Hardcode membership start date to 2026-03-01 per request
|
||||
|
||||
182
src/pages/memberships/renewal_payment.php
Normal file
182
src/pages/memberships/renewal_payment.php
Normal file
@@ -0,0 +1,182 @@
|
||||
<?php
|
||||
$headerStyle = 'light';
|
||||
$rootPath = dirname(dirname(dirname(__DIR__)));
|
||||
include_once($rootPath . '/header.php');
|
||||
// Assuming you have the user ID stored in the session
|
||||
if (isset($_SESSION['user_id'])) {
|
||||
$user_id = $_SESSION['user_id'];
|
||||
} else {
|
||||
header('Location: login.php');
|
||||
exit(); // Stop further script execution
|
||||
}
|
||||
|
||||
// Initialize variables
|
||||
$payment_amount = null;
|
||||
$membership_start_date = null;
|
||||
$membership_end_date = null;
|
||||
|
||||
$continue_processing = isMembershipExpiringSoon($user_id);
|
||||
if (!$continue_processing) {
|
||||
header("Location: membership_details.php");
|
||||
exit();
|
||||
}
|
||||
|
||||
// Determine current membership type (default) and available renewal prices
|
||||
$membership_type = getMembershipType($user_id);
|
||||
if ($membership_type === 'honorary') {
|
||||
// Honorary members do not renew
|
||||
header("Location: membership_details.php");
|
||||
exit();
|
||||
}
|
||||
|
||||
// Fetch prices for all types so we can show dynamic updates client-side
|
||||
$price_full = getPriceByDescription('membership_fees');
|
||||
$price_single = getPriceByDescription('single');
|
||||
$price_country = getPriceByDescription('country_membership');
|
||||
|
||||
// Set the initially displayed renewal amount based on current membership type
|
||||
switch ($membership_type) {
|
||||
case 'country':
|
||||
$current_renewal_amount = $price_country;
|
||||
break;
|
||||
case 'single':
|
||||
$current_renewal_amount = $price_single;
|
||||
break;
|
||||
default:
|
||||
$current_renewal_amount = $price_full;
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Get the user_id from the session
|
||||
$user_id = isset($_SESSION['user_id']) ? $_SESSION['user_id'] : null;
|
||||
|
||||
if ($user_id) {
|
||||
// Prepare the SQL query to fetch data
|
||||
$query = "SELECT payment_amount, membership_start_date, membership_end_date, payment_id
|
||||
FROM membership_fees
|
||||
WHERE user_id = ?";
|
||||
|
||||
if ($stmt = $conn->prepare($query)) {
|
||||
// Bind the user_id parameter to the query
|
||||
$stmt->bind_param("i", $user_id);
|
||||
|
||||
// Execute the query
|
||||
$stmt->execute();
|
||||
|
||||
// Bind the results to variables
|
||||
$stmt->bind_result($payment_amount, $membership_start_date, $membership_end_date, $eft_id);
|
||||
|
||||
// Fetch the data
|
||||
if ($stmt->fetch()) {
|
||||
// Values are now assigned to $payment_amount, $membership_start_date, and $membership_end_date
|
||||
} else {
|
||||
// Handle case where no records are found
|
||||
$error_message = "No records found for the given user ID.";
|
||||
}
|
||||
|
||||
// Close the statement
|
||||
$stmt->close();
|
||||
} else {
|
||||
// Handle query preparation failure
|
||||
$error_message = "Query preparation failed: " . $conn->error;
|
||||
}
|
||||
} else {
|
||||
// Handle case where user_id is not found in session
|
||||
$error_message = "User ID not found in session.";
|
||||
}
|
||||
?>
|
||||
|
||||
<?php
|
||||
$pageTitle = 'Membership Renewal';
|
||||
$breadcrumbs = [['Home' => 'index.php']];
|
||||
require_once($rootPath . '/components/banner.php');
|
||||
?>
|
||||
<!-- Contact Form Area start -->
|
||||
<section class="about-us-area py-100 rpb-90 rel z-1">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-lg-6">
|
||||
<div class="section-title mb-25">
|
||||
<span class="h2 mb-15">Membership Renewal:</span>
|
||||
<?php echo
|
||||
'<h5>Membership Expiration Date: ' . $membership_end_date . '</h5>'; ?>
|
||||
</div>
|
||||
|
||||
<h5>Renewal Amount:</h5>
|
||||
|
||||
<form method="post" action="renew_membership" id="renewForm">
|
||||
<input type="hidden" name="csrf_token" value="<?php echo generateCSRFToken(); ?>">
|
||||
|
||||
<div class="mb-3">
|
||||
<div class="form-check">
|
||||
<input type="radio" name="membership_type" id="radio_full" value="full" <?php echo ($membership_type === 'full' || $membership_type === 'member' || $membership_type === null) ? 'checked' : ''; ?>>
|
||||
<label class="form-check-label" for="radio_full">Family Membership</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input type="radio" name="membership_type" id="radio_single" value="single" <?php echo ($membership_type === 'single') ? 'checked' : ''; ?>>
|
||||
<label class="form-check-label" for="radio_single">Single Membership</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input type="radio" name="membership_type" id="radio_country" value="country" <?php echo ($membership_type === 'country') ? 'checked' : ''; ?>>
|
||||
<label class="form-check-label" for="radio_country">Country Membership</label>
|
||||
<small>You need to reside more than 150km from BASE4 to qualify.</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h5>Amount:</h5>
|
||||
|
||||
<h2>R <span id="renewAmount"><?php echo number_format($current_renewal_amount, 2); ?></span></h2>
|
||||
|
||||
<button type="submit" class="theme-btn style-two style-three" style="width:100%;">
|
||||
<span data-hover="Renew Membership">Renew Membership</span>
|
||||
<i class="fal fa-arrow-right"></i>
|
||||
</button>
|
||||
<div class="text-center mt-2">
|
||||
<p>You will be redirected to iKhokha's Secure payment gateway.</p>
|
||||
</div>
|
||||
<img src="assets/images/logos/ikhokha.png" alt="Secure Payment Badges" style="max-width: 200px; display: block; margin: 10px auto 0;">
|
||||
</form>
|
||||
|
||||
<script>
|
||||
(function(){
|
||||
// Prices from server
|
||||
var prices = {
|
||||
full: <?php echo json_encode((float)$price_full); ?>,
|
||||
single: <?php echo json_encode((float)$price_single); ?>,
|
||||
country: <?php echo json_encode((float)$price_country); ?>
|
||||
};
|
||||
|
||||
function updateAmount(type){
|
||||
var amt = prices[type] !== undefined ? prices[type] : prices.full;
|
||||
document.getElementById('renewAmount').textContent = amt.toFixed(2);
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function(){
|
||||
var radios = document.querySelectorAll('input[name="membership_type"]');
|
||||
radios.forEach(function(r){
|
||||
r.addEventListener('change', function(){
|
||||
updateAmount(this.value);
|
||||
});
|
||||
});
|
||||
// initialize
|
||||
var checked = document.querySelector('input[name="membership_type"]:checked');
|
||||
if(checked) updateAmount(checked.value);
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="col-lg-6" data-aos="fade-right" data-aos-duration="1500" data-aos-offset="50">
|
||||
<div class="about-us-image">
|
||||
<img src="assets/images/logos/weblogo.png" alt="About">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<?php include_once(dirname(dirname(dirname(__DIR__))) . '/components/insta_footer.php'); ?>
|
||||
@@ -159,13 +159,18 @@ require_once($rootPath . '/components/banner.php');
|
||||
<h2>4WDCSA Committee and Other Office Bearers</h2>
|
||||
<div>
|
||||
<h3>Committee</h3>
|
||||
<li>Chairman - John Runciman</li>
|
||||
<li>Chairman - Peter Hutchison</li>
|
||||
<li>Vice Chairman - Davin Webster</li>
|
||||
<li>National Liaison - Peter Hutchison</li>
|
||||
<li>Treasurer - Doug Timm</li>
|
||||
<li>Outings - John Runciman</li>
|
||||
<li>Events - Noelene Runciman</li>
|
||||
<li>Driver Training - John Runciman</li>
|
||||
<li>Events - Noelene Koertzen</li>
|
||||
<li>Driver Training - VACANT</li>
|
||||
<li>Digital Media - Christopher Pinto</li>
|
||||
<li>Marketing - Janet Erasmus</li>
|
||||
<li>Outdoor - Carla Holtzhausen</li>
|
||||
<li>Clubhouse - Tree Stiebel</li>
|
||||
<li>Maintenance - Kit Muirhead</li>
|
||||
|
||||
|
||||
</div>
|
||||
<div class="pt-30 pb-20">
|
||||
@@ -238,7 +243,7 @@ require_once($rootPath . '/components/banner.php');
|
||||
<div class="cta-item" style="background-image: url(assets/images/trips/1_01.jpg);">
|
||||
<span class="category">Extended Trips</span>
|
||||
<h2>Come and Explore Africa and beyond</h2>
|
||||
<a href="<?= url('trips') ?>" class="theme-btn style-two bgc-secondary">
|
||||
<a href="trips" class="theme-btn style-two bgc-secondary">
|
||||
<span data-hover="Explore Tours">Explore Trips</span>
|
||||
<i class="fal fa-arrow-right"></i>
|
||||
</a>
|
||||
@@ -248,7 +253,7 @@ require_once($rootPath . '/components/banner.php');
|
||||
<div class="cta-item" style="background-image: url(assets/images/courses/driver_training.png);">
|
||||
<span class="category">Driver Training</span>
|
||||
<h2>Level up your 4x4 Driving Skills</h2>
|
||||
<a href="<?= url('driver_training') ?>" class="theme-btn style-two">
|
||||
<a href="driver_training" class="theme-btn style-two">
|
||||
<span data-hover="Explore Tours">Explore Training</span>
|
||||
<i class="fal fa-arrow-right"></i>
|
||||
</a>
|
||||
@@ -258,7 +263,7 @@ require_once($rootPath . '/components/banner.php');
|
||||
<div class="cta-item" style="background-image: url(assets/images/base4/camping.jpg);">
|
||||
<span class="category">Events</span>
|
||||
<h2>See whats cooking at BASE4!</h2>
|
||||
<a href="<?= url('events') ?>" class="theme-btn style-two bgc-secondary">
|
||||
<a href="events" class="theme-btn style-two bgc-secondary">
|
||||
<span data-hover="Explore Tours">Explore Events</span>
|
||||
<i class="fal fa-arrow-right"></i>
|
||||
</a>
|
||||
|
||||
827
src/pages/other/base4.php
Normal file
827
src/pages/other/base4.php
Normal file
@@ -0,0 +1,827 @@
|
||||
<?php
|
||||
$headerStyle = 'light';
|
||||
$rootPath = dirname(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>
|
||||
.gallery-slider-active {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
/* spacing between images */
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.gallery-three-item {
|
||||
width: 520px;
|
||||
height: 300px;
|
||||
overflow: hidden;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
background: #f9f9f9;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.gallery-three-item .image {
|
||||
flex-grow: 1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.gallery-three-item img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
/* ensures aspect ratio while filling container */
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
<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: 50px;
|
||||
padding: 20px;
|
||||
border-radius: 20px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
#map {
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
width: 100% !important;
|
||||
height: 500px !important;
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.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 Track';
|
||||
$breadcrumbs = [['Home' => 'index.php']];
|
||||
require_once($rootPath . '/components/banner.php');
|
||||
?>
|
||||
|
||||
<!-- Features Area start -->
|
||||
<section class="features-area pt-100 pb-45 rel z-1">
|
||||
<div class="container">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-xl-7">
|
||||
<div class=" mb-55" data-aos="fade-left" data-aos-duration="1500"
|
||||
data-aos-offset="50">
|
||||
<div class="section-title mb-20">
|
||||
<h2><b>BASE 4:</b> The home of 4WDCSA.</h2>
|
||||
<p>Nestled near the Hennops river, in Doornradje, Centurion, BASE4 is the ultimate weekend getaway for 4x4 enthusiasts and outdoor lovers. This vibrant hub offers an array of exciting activities, including a challenging 4x4 test track, relaxing camping spots, and a clubhouse with food and refreshments. Take a dip in the swimming pool, fire up the braai, or unwind our brand new clubhouse. Whether you're here for adventure or relaxation, BASE4 provides the perfect setting for all your off-road and outdoor adventures. Join the Four Wheel Drive Club of Southern Africa and be part of the thrill!</p>
|
||||
<div class="image">
|
||||
<img style="border-radius:10px;" src="assets/images/base4/01.jpeg" alt="Hotel">
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="menu-btns py-10">
|
||||
<a href="membership" class="theme-btn style-two bgc-secondary">
|
||||
<span data-hover="Become a Member">Become a Member</span>
|
||||
<i class="fal fa-arrow-right"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xl-5" data-aos="fade-right" data-aos-duration="1500" data-aos-offset="50">
|
||||
<div class="row pb-25">
|
||||
<div class="section-title text-center counter-text-wrap mb-70" data-aos="fade-up"
|
||||
data-aos-duration="1500" data-aos-offset="50">
|
||||
<h2><b>BASE4</b><br>Non Member Fees:</h2>
|
||||
<div class="pt-20 pb-20">
|
||||
<h3>Day visitors*:</h3>
|
||||
<h4>R 50.00 per vehicle</h4>
|
||||
</div>
|
||||
<div class="pt-20 pb-20">
|
||||
<h3>Day visit & Track Pass*:</h3>
|
||||
<h4>R 150.00 per vehicle</h4>
|
||||
</div>
|
||||
<div class="pt-20 pb-20">
|
||||
<h3>Camping:</h3>
|
||||
<h4>R 250.00 per vehicle</h4>
|
||||
<p>Single night camping. Includes access to the track.</p>
|
||||
</div>
|
||||
<div class="pt-20 pb-20">
|
||||
<h3>BASE4 Weekend Pass:</h3>
|
||||
<h4>R 400.00 per vehicle</h4>
|
||||
<p>Camping from Friday till Sunday. Includes access to the track</p>
|
||||
</div>
|
||||
<p style="font-size:0.8rem;">
|
||||
*Day visitor charge not applicable on Open Days. Non-members require a 4WDCSA member to accompany them on the track at all times. Indemnity waiver must be signed at the clubhouse upon entry.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<!-- Features Area end -->
|
||||
|
||||
<!-- Hotel Area start -->
|
||||
<section class="hotel-area bgc-black py-100 rel z-1">
|
||||
<div class="container-fluid">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-12">
|
||||
<div class="section-title text-white text-center counter-text-wrap mb-70" data-aos="fade-up"
|
||||
data-aos-duration="1500" data-aos-offset="50">
|
||||
<h2>BASE4 Open Days</h2>
|
||||
<p style="max-width: 60%; margin: auto;">Whether you're a member or just curious, everyone's welcome at our monthly open events. Come camp with us, enjoy guest speakers, take your rig for a spin on the 4x4 track, or just relax by the swimming pool. Food and refreshments are available all weekend, plus braai fires ready to go—just bring your tongs! It’s the perfect way to experience the spirit of the club and connect with fellow adventurers. </p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gallery-slider-active">
|
||||
<?php
|
||||
$folder = $rootPath . '/assets/images/opendays/';
|
||||
$images = glob($folder . '*.{jpg,jpeg,png,gif}', GLOB_BRACE);
|
||||
// Convert absolute paths to web-relative paths
|
||||
$images = array_map(function ($path) use ($rootPath) {
|
||||
return str_replace($rootPath, '', $path);
|
||||
}, $images);
|
||||
|
||||
// Shuffle and pick first 5
|
||||
shuffle($images);
|
||||
$selected = array_slice($images, 0, 10);
|
||||
|
||||
foreach ($selected as $image) {
|
||||
echo '<div class="gallery-three-item" data-aos="fade-up" data-aos-duration="1500" data-aos-offset="50">
|
||||
<div class="image">
|
||||
<img src="' . $image . '" alt="Gallery">
|
||||
</div>
|
||||
|
||||
</div>';
|
||||
}
|
||||
?>
|
||||
</div>
|
||||
</div>
|
||||
<!-- <div class="hotel-more-btn text-center mt-40">
|
||||
<a href="destination2.html" class="theme-btn style-four">
|
||||
<span data-hover="Explore More Hotel">Explore More Hotel</span>
|
||||
<i class="fal fa-arrow-right"></i>
|
||||
</a>
|
||||
</div> -->
|
||||
</div>
|
||||
</section>
|
||||
<!-- Hotel Area end -->
|
||||
<!-- Track Map Section -->
|
||||
<section class="track-map-section">
|
||||
<div class="container">
|
||||
<div class="track-info-box">
|
||||
<div class="section-title text-center counter-text-wrap mb-70" data-aos="fade-up"
|
||||
data-aos-duration="1500" data-aos-offset="50">
|
||||
<h2>BASE4 4x4 Training Track</h2>
|
||||
<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>
|
||||
</div>
|
||||
<?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(); ?>
|
||||
@@ -1,660 +0,0 @@
|
||||
<?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(); ?>
|
||||
@@ -4,10 +4,11 @@ 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");
|
||||
require_once($rootPath . "/src/helpers/notification_helper.php");
|
||||
|
||||
$user_id = isset($_SESSION['user_id']) ? $_SESSION['user_id'] : null;
|
||||
$payment_id = generatePaymentRef('SUBS', null, $user_id);
|
||||
$status = 'AWAITING PAYMENT';
|
||||
|
||||
// If current month is December, attribute the membership year to the next year
|
||||
$currentYear = intval(date('Y'));
|
||||
$month = intval(date('n'));
|
||||
@@ -92,6 +93,20 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
die('Invalid email format.');
|
||||
}
|
||||
|
||||
//MEMBERSHIP TYPE
|
||||
$country_membership = isset($_POST['country_membership']) ? 1 : 0;
|
||||
$membership_type = in_array($_POST['membership_type'] ?? '', ['full', 'single']) ? $_POST['membership_type'] : 'full';
|
||||
$honorary_member = validateHonoraryMemberName($first_name, $last_name);
|
||||
if ($honorary_member) {
|
||||
$membership_type = 'honorary';
|
||||
} elseif ($country_membership) {
|
||||
$membership_type = 'country';
|
||||
} else {
|
||||
$membership_type = $membership_type;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Spouse or Partner details (optional)
|
||||
$spouse_first_name = !empty($_POST['spouse_first_name']) ? validateName($_POST['spouse_first_name']) : null;
|
||||
$spouse_last_name = !empty($_POST['spouse_last_name']) ? validateName($_POST['spouse_last_name']) : null;
|
||||
@@ -136,8 +151,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
spouse_first_name, spouse_last_name, spouse_id_number, spouse_dob, spouse_occupation, spouse_tel_cell, spouse_email,
|
||||
child_name1, child_dob1, child_name2, child_dob2, child_name3, child_dob3,
|
||||
physical_address, postal_address, interests_hobbies, vehicle_make, vehicle_model, vehicle_year, vehicle_registration,
|
||||
secondary_vehicle_make, secondary_vehicle_model, secondary_vehicle_year, secondary_vehicle_registration
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
|
||||
secondary_vehicle_make, secondary_vehicle_model, secondary_vehicle_year, secondary_vehicle_registration, membership_type
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
|
||||
|
||||
// Check if preparation was successful
|
||||
if (!$stmt) {
|
||||
@@ -145,7 +160,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
}
|
||||
|
||||
$stmt->bind_param(
|
||||
"isssssssssssssssssssssssssssssss",
|
||||
"issssssssssssssssssssssssssssssss",
|
||||
$user_id,
|
||||
$first_name,
|
||||
$last_name,
|
||||
@@ -177,7 +192,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$secondary_vehicle_make,
|
||||
$secondary_vehicle_model,
|
||||
$secondary_vehicle_year,
|
||||
$secondary_vehicle_registration
|
||||
$secondary_vehicle_registration,
|
||||
$membership_type
|
||||
);
|
||||
|
||||
if ($stmt->execute()) {
|
||||
@@ -187,10 +203,27 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$year = (int)$today->format('Y');
|
||||
$payment_date = $today->format('Y-m-d');
|
||||
$membership_start_date = $payment_date;
|
||||
$status = 'AWAITING PAYMENT';
|
||||
if ($membership_type === 'honorary') {
|
||||
// Honorary members do not pay fees, set amount to 0 and end date far in future
|
||||
$payment_amount = 0.00;
|
||||
$status = 'PAID';
|
||||
} elseif ($membership_type === 'country') {
|
||||
$payment_amount = getPriceByDescription('country_membership');
|
||||
$prorata_amount = calculateProrata(getPriceByDescription('country_prorata'));
|
||||
} elseif ($membership_type === 'single') {
|
||||
$payment_amount = getPriceByDescription('single');
|
||||
$prorata_amount = calculateProrata(getPriceByDescription('single_prorata'));
|
||||
} else {
|
||||
$payment_amount = getPriceByDescription('membership_fees');
|
||||
$prorata_amount = calculateProrata(getPriceByDescription('pro_rata'));
|
||||
}
|
||||
|
||||
|
||||
|
||||
if ($month == 12 || $month == 1 || $month == 2) {
|
||||
// December, January, February: charge full fee, valid till end of next Feb
|
||||
$payment_amount = getPriceByDescription('membership_fees');
|
||||
$payment_amount = $payment_amount;
|
||||
// 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"))
|
||||
@@ -198,7 +231,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
->format('Y-m-d');
|
||||
} else {
|
||||
// Prorata for Mar-Nov
|
||||
$payment_amount = calculateProrata(getPriceByDescription('pro_rata'));
|
||||
$payment_amount = $prorata_amount;
|
||||
// End of next Feb if after Feb, else this Feb
|
||||
if ($month > 2) {
|
||||
$end_year = $year + 1;
|
||||
@@ -209,10 +242,14 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
->modify('last day of this month')
|
||||
->format('Y-m-d');
|
||||
}
|
||||
if ($membership_type === 'honorary') {
|
||||
// Honorary members do not pay fees, set amount to 0 and end date far in future
|
||||
$membership_end_date = '2099-12-31';
|
||||
}
|
||||
|
||||
$stmt = $conn->prepare("INSERT INTO membership_fees (user_id, payment_amount, payment_date, membership_start_date, membership_end_date, renewal_period_end, payment_status, payment_id)
|
||||
VALUES (?, ?, ?, ?, ?, ?, 'AWAITING PAYMENT', ?)");
|
||||
$stmt->bind_param("idsssss", $user_id, $payment_amount, $payment_date, $membership_start_date, $membership_end_date, $membership_end_date, $payment_id);
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)");
|
||||
$stmt->bind_param("idssssss", $user_id, $payment_amount, $payment_date, $membership_start_date, $membership_end_date, $membership_end_date, $status, $payment_id);
|
||||
|
||||
if ($stmt->execute()) {
|
||||
// Commit the transaction
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 9.7 KiB After Width: | Height: | Size: 4.9 KiB |
Reference in New Issue
Block a user