9 Commits

Author SHA1 Message Date
twotalesanimation
05f74f1b86 feat: prevent duplicate membership applications and fees
- Add UNIQUE constraint on membership_application.user_id (one app per user)
- Add UNIQUE constraint on membership_fees.user_id (one fee record per user)
- Add validation checks in process_application.php before inserting
- Improve error messages for duplicate submission attempts
- Add migration script to clean up existing duplicates before constraints
- Update checkMembershipApplication to set session message on redirect
- Add comprehensive documentation of duplicate prevention architecture

Individual payments/EFTs are tracked separately in payments table
2025-12-05 09:42:42 +02:00
twotalesanimation
9133b7bbc6 feat: improve campsites and events management UX
- Add map-based location picker with centered pin for campsites (two-step process)
- Hide edit buttons for campsites not owned by current user
- Allow numbers in campsite names (fix validateName function)
- Prepopulate edit form with existing campsite data
- Preserve country/province selection when confirming location
- Add real-time filter functionality to campsites table
- Fix events publish button error handling (use output buffering cleanup)
- Improve AJAX response handling with complete callback

Changes:
- src/pages/bookings/campsites.php: Location mode UI, filter, edit form improvements
- src/config/functions.php: Allow numbers in validateName regex
- src/admin/toggle_event_published.php: Clean output buffers before JSON response
- src/admin/admin_events.php: Use complete callback instead of success/error handlers
2025-12-05 09:20:48 +02:00
twotalesanimation
b52c46b67c feat: add campsites link to members area menu with membership access control
- Replace 'Coming Soon!' with 'Campsites' link in Members Area dropdown
- Add membership verification check to campsites.php
- Redirect non-logged-in users to login page
- Redirect non-members to index page
- Only active members can access campsites feature
2025-12-04 23:01:28 +02:00
twotalesanimation
32651ed433 fix: publish toggle error alert and event visibility
- Add proper error handling to toggle_event_published.php with HTTP status codes
- Add try-catch block for database operations in toggle endpoint
- Update events.php query to only show published events (added published = 1 filter)
- Add updated_at timestamp update when toggling publish status
- Improve error messages for better debugging
2025-12-04 21:56:57 +02:00
twotalesanimation
f522b84fc1 refactor: align events admin pages with trips layout and add publish functionality
- Remove checkbox from manage_events.php form (publish via admin table instead)
- Redesign admin_events.php to match admin_trips.php layout exactly
- Add table-based actions with icon buttons (Edit, Publish/Unpublish, Delete)
- Change button styling to match trips (btn classes with colors)
- Add publish/unpublish toggle button with eye icon
- Create toggle_event_published.php endpoint for publish status switching
- Create delete_event.php endpoint for event deletion
- Add AJAX functionality for instant publish/delete without page reload
- Update .htaccess with new endpoint rewrite rules
- Badge styling updated to match trips (bg-success, bg-warning)
- Consistent sorting and filtering functionality
2025-12-04 21:40:11 +02:00
twotalesanimation
2b136c4b06 feat: add events admin navigation links and URL rewrite rules
- Add 'Manage Events' link to admin dropdown menu in header
- Add URL rewrite rules for admin_events and manage_events pages
- Add process_event endpoint rewrite rule
- Events admin pages now accessible via clean URLs
2025-12-04 20:32:49 +02:00
twotalesanimation
7f0964009a docs: add events admin system documentation 2025-12-04 20:26:17 +02:00
twotalesanimation
5be946f78f feat: create events management admin system
- Add manage_events.php form for creating/editing events
- Add process_event.php endpoint for CRUD operations with image uploads
- Add admin_events.php list view with sorting, filtering, and delete functionality
- Add database migration to add created_by, published, created_at, updated_at columns to events table
- Add event images directory structure
- All features follow same patterns as trip management system
2025-12-04 20:25:48 +02:00
twotalesanimation
cb588d20ee Feature: Campsite management system with map, form, and province/country filtering 2025-12-04 20:15:14 +02:00
31 changed files with 2025 additions and 105 deletions

View File

@@ -37,6 +37,7 @@ RewriteRule ^member_info$ src/pages/memberships/member_info.php [L]
RewriteRule ^bookings$ src/pages/bookings/bookings.php [L] RewriteRule ^bookings$ src/pages/bookings/bookings.php [L]
RewriteRule ^campsites$ src/pages/bookings/campsites.php [L] RewriteRule ^campsites$ src/pages/bookings/campsites.php [L]
RewriteRule ^campsite_booking$ src/pages/bookings/campsite_booking.php [L] RewriteRule ^campsite_booking$ src/pages/bookings/campsite_booking.php [L]
RewriteRule ^add_campsite$ src/pages/add_campsite.php [L]
RewriteRule ^trips$ src/pages/bookings/trips.php [L] RewriteRule ^trips$ src/pages/bookings/trips.php [L]
RewriteRule ^trip-details$ src/pages/bookings/trip-details.php [L] RewriteRule ^trip-details$ src/pages/bookings/trip-details.php [L]
RewriteRule ^course_details$ src/pages/bookings/course_details.php [L] RewriteRule ^course_details$ src/pages/bookings/course_details.php [L]
@@ -76,13 +77,14 @@ RewriteRule ^view_indemnity$ src/pages/other/view_indemnity.php [L]
RewriteRule ^admin_members$ src/admin/admin_members.php [L] RewriteRule ^admin_members$ src/admin/admin_members.php [L]
RewriteRule ^admin_payments$ src/admin/admin_payments.php [L] RewriteRule ^admin_payments$ src/admin/admin_payments.php [L]
RewriteRule ^admin_web_users$ src/admin/admin_web_users.php [L] RewriteRule ^admin_web_users$ src/admin/admin_web_users.php [L]
RewriteRule ^admin_events$ src/admin/admin_events.php [L]
RewriteRule ^admin_course_bookings$ src/admin/admin_course_bookings.php [L] RewriteRule ^admin_course_bookings$ src/admin/admin_course_bookings.php [L]
RewriteRule ^admin_camp_bookings$ src/admin/admin_camp_bookings.php [L] RewriteRule ^admin_camp_bookings$ src/admin/admin_camp_bookings.php [L]
RewriteRule ^admin_trip_bookings$ src/admin/admin_trip_bookings.php [L] RewriteRule ^admin_trip_bookings$ src/admin/admin_trip_bookings.php [L]
RewriteRule ^admin_visitors$ src/admin/admin_visitors.php [L] RewriteRule ^admin_visitors$ src/admin/admin_visitors.php [L]
RewriteRule ^admin_efts$ src/admin/admin_efts.php [L] RewriteRule ^admin_efts$ src/admin/admin_efts.php [L]
RewriteRule ^add_campsite$ src/admin/add_campsite.php [L]
RewriteRule ^admin_trips$ src/admin/admin_trips.php [L] RewriteRule ^admin_trips$ src/admin/admin_trips.php [L]
RewriteRule ^manage_events$ src/admin/manage_events.php [L]
RewriteRule ^manage_trips$ src/admin/manage_trips.php [L] RewriteRule ^manage_trips$ src/admin/manage_trips.php [L]
# === API/AJAX ENDPOINTS === # === API/AJAX ENDPOINTS ===
@@ -114,8 +116,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 ^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 ^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]
</IfModule> </IfModule>

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

View File

Before

Width:  |  Height:  |  Size: 128 KiB

After

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 259 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 259 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 226 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

176
docs/EVENTS_ADMIN_SYSTEM.md Normal file
View File

@@ -0,0 +1,176 @@
# Events Management Admin System
## Overview
A complete admin system for managing events on the 4WDCSA website, following the same patterns as the trip management system.
## Files Created
### 1. `/src/admin/manage_events.php`
**Purpose**: Form for creating and editing events
**Features**:
- Create new events form
- Edit existing events form
- Fields:
- Event Name (required)
- Event Type (required) - e.g., Workshop, Training, Rally
- Location (required)
- Date (required)
- Time (required)
- Feature/Category (required) - e.g., Off-Road Training, Social Event
- Description (required) - Full text description
- Event Image (required for new, optional for updates)
- Promotional Image (optional) - Displayed when users click "View Promo"
- Published Status (checkbox) - Controls visibility on website
**Technical Details**:
- AJAX form submission to `process_event` endpoint
- Image upload with validation
- CSRF token protection
- Responsive Bootstrap grid layout (col-md-6 fields)
- Success/error message display with auto-redirect
### 2. `/src/admin/process_event.php`
**Purpose**: Backend endpoint for handling event CRUD operations
**Endpoints**:
- `POST /process_event` - Create/Update event
- `GET /process_event?action=delete&event_id={id}` - Delete event
**Features**:
- Create new events with image uploads
- Update existing events with optional image replacement
- Delete events and associated image files
- CSRF token validation
- Image type validation (JPEG, PNG, GIF, WebP)
- File organization in `/assets/images/events/`
- Automatic timestamp management (created_at, updated_at)
- User tracking (created_by stores admin user_id)
**Image Handling**:
- Main event image: Stored with unique ID prefix
- Promo image: Stored with `_promo_` prefix
- Both uploaded to `/assets/images/events/`
### 3. `/src/admin/admin_events.php`
**Purpose**: Admin dashboard for managing all events
**Features**:
- List all events with sortable columns
- Real-time search/filter across all columns
- Create new event button
- Edit event link for each row
- Delete event with confirmation dialog
- Status badges (Published/Draft)
- Responsive table with alternating row colors
- Rounded corners on even rows
**Sortable Columns**:
- Event Name
- Type
- Location
- Date
- Status
**Actions**:
- Edit - Redirects to manage_events.php with event_id
- Delete - Removes event and associated files
## Database Schema Changes
### Migration File: `/docs/migrations/001_add_events_tracking_columns.sql`
**Columns Added to events table**:
- `created_by` (int) - References user who created the event
- `published` (tinyint(1)) - Boolean flag for publication status (default 0/false)
- `created_at` (timestamp) - Automatic timestamp when event is created
- `updated_at` (timestamp) - Automatic timestamp updated on modification
**Indexes Added**:
- `idx_date` - For sorting and filtering by date
- `idx_published` - For filtering published/draft events
- `idx_created_by` - For tracking who created events
## Design Patterns
### Follows Trip Management System Architecture
- Same form layout and styling (`.comment-form.bgc-lighter`)
- Same table styling with sortable headers and filters
- Same image upload and validation patterns
- AJAX submission with success/error messaging
- Auto-redirect on successful operation
### Image Organization
```
/assets/images/events/
├── {unique_id}_{original_filename}.jpg (event images)
└── {unique_id}_promo_{original_filename}.jpg (promo images)
```
### Front-end Integration
The existing `/src/pages/events/events.php` displays published events:
- Shows event image, name, location, date, time
- Feature description and full description
- "View Promo" button displays promotional image in modal
## Usage Workflow
### Creating an Event
1. Navigate to `/src/admin/manage_events.php`
2. Fill in all required fields
3. Upload event image
4. Optionally upload promotional image
5. Check "Publish Event" if ready to display
6. Submit form via AJAX
7. Redirected to admin_events.php list view
### Editing an Event
1. Click "Edit" button on admin_events.php
2. Modify any fields
3. Image upload is optional - existing image retained if not changed
4. Update timestamps and user tracking automatic
5. Submit form
6. Redirected back to list view
### Deleting an Event
1. Click "Delete" button on admin_events.php
2. Confirm deletion in dialog
3. Event and associated image files removed from server
4. Page automatically refreshes
### Publishing/Unpublishing
- Toggle "Publish Event" checkbox before saving
- Only published events appear on `/src/pages/events/events.php`
- Draft events hidden from public view
## Security Features
1. **CSRF Token Protection**: All forms include CSRF token validation
2. **Admin-only Access**: `checkAdmin()` function validates user permissions
3. **File Validation**: Image type checking (JPEG, PNG, GIF, WebP)
4. **SQL Injection Prevention**: Prepared statements with parameter binding
5. **XSS Prevention**: `htmlspecialchars()` used for output escaping
## Styling Classes
**Form Container**: `.comment-form.bgc-lighter.z-1.rel.mb-30.rmb-55`
**Action Buttons**: `.btn-edit`, `.btn-delete`
**Status Badges**: `.badge.badge-published`, `.badge.badge-draft`
**Tables**: Uses sortable header styling with visual sort indicators
## Browser Compatibility
- Modern browsers with AJAX/Fetch API support
- JavaScript enabled required for filtering and sorting
- File input accepts image MIME types
## Future Enhancement Opportunities
1. Bulk event operations (bulk delete, publish multiple)
2. Event categories/tags system
3. Event capacity limits with registrations
4. Email notifications for published events
5. Event calendar view
6. Event image gallery (multiple images per event)
7. Recurring events support
8. Event attendee tracking

View File

@@ -0,0 +1,86 @@
# Membership Application Duplicate Prevention
## Overview
Implemented comprehensive validation to prevent users from submitting multiple membership applications or creating multiple membership fee records. Each user can have exactly one application and one membership fee record. Individual payments are tracked separately in the payments/efts table.
## Architecture
```
User (1) ---> Membership Application (1) ---> Membership Fee (1) ---> Multiple Payments/EFTs
```
- **Membership Application**: Stores user details and application information (one per user)
- **Membership Fee**: Stores the total fee amount and dates (one per user, linked to application)
- **Payments/EFTs**: Tracks individual payment transactions for the membership fee (many per fee)
## Changes Made
### 1. Database Level Protection
**File:** `docs/migrations/002_add_unique_constraints_membership.sql`
- Added `UNIQUE` constraint on `membership_application.user_id` - ensures each user can only have one application
- Added `UNIQUE` constraint on `membership_fees.user_id` - ensures each user can only have one membership fee record
- Cleans up any duplicate records before adding constraints
### 2. Application Level Validation
**File:** `src/processors/process_application.php`
Added pre-submission checks:
- Check if user already has a membership application in the database
- Check if user already has a membership fee record
- Return clear error message if either check fails
- Catch database constraint violations and provide user-friendly message
**File:** `src/config/functions.php`
- Improved `checkMembershipApplication()` to set session message before redirecting
- Message displayed: "You have already submitted a membership application."
### 3. Error Handling
If a user somehow bypasses checks:
- Server validates before processing
- Returns HTTP 400 error with JSON response
- User sees clear message directing them to support or check email
- Database constraints prevent data corruption (duplicate key violation)
## User Flow
1. **First Visit to Application Page:**
- `checkMembershipApplication()` checks database
- If no application exists, shows form
- If application exists, redirects to `membership_details.php`
2. **Form Submission:**
- Server checks for existing application
- Server checks for existing membership fee
- If checks pass, inserts application and fee in transaction
- On success, redirects to indemnity page
- On error, returns JSON error response
3. **Payment Process:**
- Individual payment records are created in payments/efts table
- Multiple payments can be made against the single membership_fee record
- Payment status is tracked independently from application
## Testing Recommendations
1. Test creating a membership application - should succeed
2. Try applying again - should be redirected to membership_details
3. Try submitting the form multiple times rapidly - should fail on 2nd attempt
4. Verify payments can be made against the single membership fee record
5. Check database constraints: `SHOW INDEX FROM membership_application;` and `SHOW INDEX FROM membership_fees;`
## Database Constraints
```sql
-- One application per user
ALTER TABLE membership_application
ADD CONSTRAINT uk_membership_application_user_id UNIQUE (user_id);
-- One membership fee record per user
ALTER TABLE membership_fees
ADD CONSTRAINT uk_membership_fees_user_id UNIQUE (user_id);
```
## Backwards Compatibility
The migration script cleans up any existing duplicate records before adding constraints, ensuring no data loss.

View File

@@ -0,0 +1,14 @@
-- Events Table Migration
-- Add missing columns to events table for proper tracking and publishing control
-- Add columns if they don't exist (using ALTER IGNORE for compatibility)
ALTER TABLE `events`
ADD COLUMN `created_by` int DEFAULT NULL AFTER `promo`,
ADD COLUMN `published` tinyint(1) DEFAULT 0 AFTER `created_by`,
ADD COLUMN `created_at` timestamp DEFAULT CURRENT_TIMESTAMP AFTER `published`,
ADD COLUMN `updated_at` timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP AFTER `created_at`;
-- Add indexes for better query performance
ALTER TABLE `events` ADD INDEX `idx_date` (`date`);
ALTER TABLE `events` ADD INDEX `idx_published` (`published`);
ALTER TABLE `events` ADD INDEX `idx_created_by` (`created_by`);

View File

@@ -0,0 +1,37 @@
-- Migration: Add UNIQUE constraints to prevent duplicate membership applications and fees
-- Date: 2025-12-05
-- Purpose: Ensure each user can only have one application and one membership fee record
-- Note: Individual payments are tracked in the payments/efts table, not here
-- Add UNIQUE constraint to membership_application table
-- First, delete any duplicate applications keeping the most recent one
DELETE FROM membership_application
WHERE application_id NOT IN (
SELECT MAX(application_id)
FROM (
SELECT application_id
FROM membership_application
) tmp
GROUP BY user_id
);
-- Add UNIQUE constraint on user_id in membership_application
ALTER TABLE membership_application
ADD CONSTRAINT uk_membership_application_user_id UNIQUE (user_id);
-- Add UNIQUE constraint to membership_fees table
-- First, delete any duplicate fees keeping the most recent one
DELETE FROM membership_fees
WHERE fee_id NOT IN (
SELECT MAX(fee_id)
FROM (
SELECT fee_id
FROM membership_fees
) tmp
GROUP BY user_id
);
-- Add UNIQUE constraint on user_id in membership_fees
ALTER TABLE membership_fees
ADD CONSTRAINT uk_membership_fees_user_id UNIQUE (user_id);

View File

@@ -283,6 +283,7 @@ if ($headerStyle === 'light') {
<ul> <ul>
<li><a href="admin_web_users">Website Users</a></li> <li><a href="admin_web_users">Website Users</a></li>
<li><a href="admin_members">4WDCSA Members</a></li> <li><a href="admin_members">4WDCSA Members</a></li>
<li><a href="admin_events">Manage Events</a></li>
<li><a href="admin_trips">Manage Trips</a></li> <li><a href="admin_trips">Manage Trips</a></li>
<li><a href="admin_trip_bookings">Trip Bookings</a></li> <li><a href="admin_trip_bookings">Trip Bookings</a></li>
<li><a href="admin_course_bookings">Course Bookings</a></li> <li><a href="admin_course_bookings">Course Bookings</a></li>
@@ -295,15 +296,21 @@ if ($headerStyle === 'light') {
</li> </li>
<?php } ?> <?php } ?>
<li><a href="contact">Contact</a></li> <li><a href="contact">Contact</a></li>
<?php if ($is_member) : ?> <?php if ($is_logged_in) : ?>
<li class="dropdown"><a href="#">Members Area</a> <li class="dropdown"><a href="#">Members Area</a>
<ul> <ul>
<li><a href="#">Coming Soon!</a></li> <?php
if (getUserMemberStatus($_SESSION['user_id'])) {
echo "<li><a href=\"campsites\">Campsites Directory</a></li>";
} else {
echo "<li><a href=\"membership\">Campsites Directory</a><i class='fal fa-lock'></i></li>";
}
?>
</ul> </ul>
</li> </li>
<?php endif; ?>
<?php if ($is_logged_in) : ?>
<li class="dropdown"><a href="#">My Account</a> <li class="dropdown"><a href="#">My Account</a>
<ul> <ul>
<li><a href="account_settings">Account Settings</a></li> <li><a href="account_settings">Account Settings</a></li>

361
src/admin/admin_events.php Normal file
View File

@@ -0,0 +1,361 @@
<?php
$headerStyle = 'light';
$rootPath = dirname(dirname(__DIR__));
include_once($rootPath . '/header.php');
checkAdmin();
// Fetch all events
$events_query = "
SELECT
event_id, name, type, location, date, published
FROM events
ORDER BY date DESC
";
$result = $conn->query($events_query);
$events = [];
if ($result && $result->num_rows > 0) {
while ($row = $result->fetch_assoc()) {
$events[] = $row;
}
}
?>
<style>
table {
width: 100%;
border-collapse: separate;
border-spacing: 0;
margin: 10px 0;
}
thead th {
cursor: pointer;
text-align: left;
padding: 10px;
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>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
<script>
$(document).ready(function() {
// Sorting functionality
const table = document.querySelector('table');
if (table) {
const headers = table.querySelectorAll('thead th');
const rows = Array.from(table.querySelectorAll('tbody tr'));
headers.forEach((header, index) => {
header.addEventListener('click', () => {
const sortedRows = rows.sort((a, b) => {
const aText = a.cells[index].textContent.trim().toLowerCase();
const bText = b.cells[index].textContent.trim().toLowerCase();
if (aText < bText) return -1;
if (aText > bText) return 1;
return 0;
});
if (header.classList.contains('asc')) {
header.classList.remove('asc');
header.classList.add('desc');
sortedRows.reverse();
} else {
headers.forEach(h => h.classList.remove('asc', 'desc'));
header.classList.add('asc');
}
const tbody = table.querySelector('tbody');
tbody.innerHTML = '';
sortedRows.forEach(row => tbody.appendChild(row));
});
});
// Filter functionality
const filterInput = document.querySelector('.filter-input');
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>
<?php
$pageTitle = 'Manage Events';
$breadcrumbs = [['Home' => 'index'], [$pageTitle => '']];
require_once($rootPath . '/components/banner.php');
?>
<?php
$pageTitle = 'Manage Events';
$breadcrumbs = [['Home' => 'index'], [$pageTitle => '']];
require_once($rootPath . '/components/banner.php');
?>
<!-- Events Management Area start -->
<section class="events-management-area py-100 rel z-1">
<div class="container">
<div class="row mb-30">
<div class="col-lg-12">
<a href="manage_events" class="theme-btn style-two">+ Create New Event</a>
</div>
</div>
<?php
if (!empty($events)) {
echo '<div class="row">
<div class="col-lg-12">
<div class="form-group mb-20">
<input type="text" class="filter-input" placeholder="Search events...">
</div>
<table>
<thead>
<tr>
<th>Event Name</th>
<th>Type</th>
<th>Location</th>
<th>Date</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody>';
foreach ($events as $event) {
$publishButtonText = $event['published'] == 1 ? 'Unpublish' : 'Publish';
$publishButtonClass = $event['published'] == 1 ? 'btn-warning' : 'btn-success';
echo '<tr>
<td><strong>' . htmlspecialchars($event['name']) . '</strong></td>
<td>' . htmlspecialchars($event['type']) . '</td>
<td>' . htmlspecialchars($event['location']) . '</td>
<td>' . convertDate($event['date']) . '</td>
<td>' . ($event['published'] == 1 ? '<span class="badge bg-success">Published</span>' : '<span class="badge bg-warning">Draft</span>') . '</td>
<td>
<a href="manage_events?event_id=' . $event['event_id'] . '" class="btn btn-sm btn-primary" title="Edit">
<i class="far fa-edit"></i>
</a>
<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>
</section>
<!-- Events Management Area end -->
<?php include_once($rootPath . '/components/insta_footer.php'); ?>

View File

@@ -0,0 +1,46 @@
<?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();

173
src/admin/manage_events.php Normal file
View File

@@ -0,0 +1,173 @@
<?php
$headerStyle = 'light';
$rootPath = dirname(dirname(__DIR__));
include_once($rootPath . '/header.php');
checkAdmin();
$event_id = $_GET['event_id'] ?? null;
$event = null;
// If editing an existing event, fetch its data
if ($event_id) {
$stmt = $conn->prepare("SELECT * 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();
}
$stmt->close();
}
?>
<?php
$pageTitle = $event ? 'Edit Event' : 'Create New Event';
$breadcrumbs = [['Home' => 'index'], ['Admin' => 'admin_events'], [$pageTitle => '']];
require_once($rootPath . '/components/banner.php');
?>
<!-- Event Manager Area start -->
<section class="event-manager-area py-100 rel z-1">
<div class="container">
<div class="row">
<div class="col-lg-12">
<div class="comment-form bgc-lighter z-1 rel mb-30 rmb-55">
<form id="eventForm" enctype="multipart/form-data" method="POST" action="process_event">
<input type="hidden" name="csrf_token" value="<?php echo generateCSRFToken(); ?>">
<?php if ($event): ?>
<input type="hidden" name="event_id" value="<?php echo $event['event_id']; ?>">
<?php endif; ?>
<div class="section-title py-20">
<h2><?php echo $event ? 'Edit Event: ' . htmlspecialchars($event['name']) : 'Create New Event'; ?></h2>
<div id="responseMessage"></div>
</div>
<!-- Event Information -->
<div class="row mt-35">
<div class="col-md-6">
<div class="form-group">
<label for="name">Event Name *</label>
<input type="text" id="name" name="name" class="form-control" value="<?php echo $event ? htmlspecialchars($event['name']) : ''; ?>" required>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="type">Event Type *</label>
<input type="text" id="type" name="type" class="form-control" value="<?php echo $event ? htmlspecialchars($event['type']) : ''; ?>" placeholder="e.g., Workshop, Training, Rally" required>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="location">Location *</label>
<input type="text" id="location" name="location" class="form-control" value="<?php echo $event ? htmlspecialchars($event['location']) : ''; ?>" required>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="date">Date *</label>
<input type="date" id="date" name="date" class="form-control" value="<?php echo $event ? $event['date'] : ''; ?>" required>
</div>
</div>
<!-- Time -->
<div class="col-md-6">
<div class="form-group">
<label for="time">Time *</label>
<input type="time" id="time" name="time" class="form-control" value="<?php echo $event ? $event['time'] : ''; ?>" required>
</div>
</div>
<!-- Feature/Category -->
<div class="col-md-6">
<div class="form-group">
<label for="feature">Feature/Category *</label>
<input type="text" id="feature" name="feature" class="form-control" value="<?php echo $event ? htmlspecialchars($event['feature']) : ''; ?>" placeholder="e.g., Off-Road Training, Social Event" required>
</div>
</div>
<!-- Descriptions -->
<div class="col-md-12">
<div class="form-group">
<label for="description">Description *</label>
<textarea id="description" name="description" class="form-control" rows="6" required><?php echo $event ? htmlspecialchars($event['description']) : ''; ?></textarea>
</div>
</div>
<!-- Image Upload -->
<div class="col-md-12 mt-20">
<div class="form-group">
<label for="image">Event Image *</label>
<input type="file" id="image" name="image" class="form-control" accept="image/*" <?php echo !$event ? 'required' : ''; ?>>
<?php if ($event && $event['image']): ?>
<small class="text-info d-block mt-2">Current image: <img src="<?php echo $event['image']; ?>" alt="Event Image" style="max-width: 200px; margin-top: 10px;"></small>
<?php endif; ?>
</div>
</div>
<!-- Promo Image Upload -->
<div class="col-md-12 mt-20">
<div class="form-group">
<label for="promo">Promotional Image</label>
<input type="file" id="promo" name="promo" class="form-control" accept="image/*">
<small class="text-muted">This image will be displayed when users click "View Promo"</small>
<?php if ($event && $event['promo']): ?>
<small class="text-info d-block mt-2">Current promo: <img src="<?php echo $event['promo']; ?>" alt="Promo Image" style="max-width: 200px; margin-top: 10px;"></small>
<?php endif; ?>
</div>
</div>
<div class="col-md-12 mt-20">
<div class="form-group mb-0">
<button type="submit" class="theme-btn style-two" style="width:100%;">
<?php echo $event ? 'Update Event' : 'Create Event'; ?>
</button>
</div>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</section>
<!-- Event Manager Area end -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
<script>
$(document).ready(function() {
$('#eventForm').on('submit', function(event) {
event.preventDefault();
var formData = new FormData(this);
$.ajax({
url: 'process_event',
type: 'POST',
data: formData,
contentType: false,
processData: false,
dataType: 'json',
success: function(response) {
if (response.status === 'success') {
$('#responseMessage').html('<div class="alert alert-success">' + response.message + '</div>');
setTimeout(function() {
window.location.href = 'admin_events';
}, 2000);
} else {
$('#responseMessage').html('<div class="alert alert-danger">' + response.message + '</div>');
console.error('Server error:', response.message);
}
},
error: function(xhr, status, error) {
console.log('AJAX Error:', error);
console.log('Response:', xhr.responseText);
$('#responseMessage').html('<div class="alert alert-danger">Error creating/updating event: ' + error + '</div>');
}
});
});
});
</script>
<?php include_once($rootPath . '/components/insta_footer.php'); ?>

193
src/admin/process_event.php Normal file
View File

@@ -0,0 +1,193 @@
<?php
$rootPath = dirname(dirname(__DIR__));
include_once($rootPath . '/header.php');
checkAdmin();
header('Content-Type: application/json');
// Handle delete action
if ($_GET['action'] ?? null === 'delete') {
$event_id = $_GET['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();
exit;
}
// Check CSRF token
if (!isset($_POST['csrf_token']) || !verifyCsrfToken($_POST['csrf_token'])) {
echo json_encode(['status' => 'error', 'message' => 'CSRF token validation failed']);
exit;
}
$event_id = $_POST['event_id'] ?? null;
$name = $_POST['name'] ?? null;
$type = $_POST['type'] ?? null;
$location = $_POST['location'] ?? null;
$date = $_POST['date'] ?? null;
$time = $_POST['time'] ?? null;
$feature = $_POST['feature'] ?? null;
$description = $_POST['description'] ?? null;
// Validate required fields
if (!$name || !$type || !$location || !$date || !$time || !$feature || !$description) {
echo json_encode(['status' => 'error', 'message' => 'All required fields must be filled']);
exit;
}
// Handle image upload
$image_path = null;
if (!empty($_FILES['image']['name'])) {
$upload_dir = $rootPath . '/assets/images/events/';
if (!is_dir($upload_dir)) {
mkdir($upload_dir, 0755, true);
}
$file_name = uniqid() . '_' . basename($_FILES['image']['name']);
$target_file = $upload_dir . $file_name;
$file_type = mime_content_type($_FILES['image']['tmp_name']);
// Validate image file
$allowed_types = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
if (!in_array($file_type, $allowed_types)) {
echo json_encode(['status' => 'error', 'message' => 'Invalid image file type. Only JPEG, PNG, GIF, and WebP are allowed']);
exit;
}
if (move_uploaded_file($_FILES['image']['tmp_name'], $target_file)) {
$image_path = 'assets/images/events/' . $file_name;
} else {
echo json_encode(['status' => 'error', 'message' => 'Failed to upload image']);
exit;
}
} else if (!$event_id) {
echo json_encode(['status' => 'error', 'message' => 'Image is required for new events']);
exit;
}
// Handle promo image upload
$promo_path = null;
if (!empty($_FILES['promo']['name'])) {
$upload_dir = $rootPath . '/assets/images/events/';
if (!is_dir($upload_dir)) {
mkdir($upload_dir, 0755, true);
}
$file_name = uniqid() . '_promo_' . basename($_FILES['promo']['name']);
$target_file = $upload_dir . $file_name;
$file_type = mime_content_type($_FILES['promo']['tmp_name']);
// Validate image file
$allowed_types = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
if (!in_array($file_type, $allowed_types)) {
echo json_encode(['status' => 'error', 'message' => 'Invalid promo image file type. Only JPEG, PNG, GIF, and WebP are allowed']);
exit;
}
if (move_uploaded_file($_FILES['promo']['tmp_name'], $target_file)) {
$promo_path = 'assets/images/events/' . $file_name;
}
}
try {
if ($event_id) {
// Update existing event
$update_fields = [
'name' => $name,
'type' => $type,
'location' => $location,
'date' => $date,
'time' => $time,
'feature' => $feature,
'description' => $description,
'updated_at' => date('Y-m-d H:i:s')
];
if ($image_path) {
$update_fields['image'] = $image_path;
}
if ($promo_path) {
$update_fields['promo'] = $promo_path;
}
$set_clause = implode(', ', array_map(function($key) {
return $key . ' = ?';
}, array_keys($update_fields)));
$values = array_values($update_fields);
$values[] = $event_id;
$stmt = $conn->prepare("UPDATE events SET $set_clause WHERE event_id = ?");
// Build type string for bind_param
$type_str = str_repeat('s', count($update_fields)) . 'i';
$stmt->bind_param($type_str, ...$values);
if ($stmt->execute()) {
echo json_encode(['status' => 'success', 'message' => 'Event updated successfully']);
} else {
echo json_encode(['status' => 'error', 'message' => 'Failed to update event: ' . $stmt->error]);
}
} else {
// Create new event
if (!$image_path) {
echo json_encode(['status' => 'error', 'message' => 'Image is required for new events']);
exit;
}
$promo_path = $promo_path ?? 'assets/images/events/default-promo.jpg';
$stmt = $conn->prepare("
INSERT INTO events (name, type, location, date, time, feature, description, image, promo, created_by)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
");
$created_by = $_SESSION['user_id'] ?? 0;
$stmt->bind_param('sssssssssi', $name, $type, $location, $date, $time, $feature, $description, $image_path, $promo_path, $created_by);
if ($stmt->execute()) {
echo json_encode(['status' => 'success', 'message' => 'Event created successfully']);
} else {
echo json_encode(['status' => 'error', 'message' => 'Failed to create event: ' . $stmt->error]);
}
}
$stmt->close();
} catch (Exception $e) {
echo json_encode(['status' => 'error', 'message' => 'An error occurred: ' . $e->getMessage()]);
}

View File

@@ -0,0 +1,75 @@
<?php
// Set JSON header FIRST before any includes that might output
header('Content-Type: application/json');
header('Cache-Control: no-cache, no-store, must-revalidate');
header('Pragma: no-cache');
header('Expires: 0');
// Clean any output buffers before including header
while (ob_get_level() > 0) {
ob_end_clean();
}
$rootPath = dirname(dirname(__DIR__));
include_once($rootPath . '/header.php');
checkAdmin();
// Clean output buffer again in case header.php added content
ob_clean();
$event_id = $_POST['event_id'] ?? null;
if (!$event_id) {
http_response_code(400);
echo json_encode(['status' => 'error', 'message' => 'Event ID is required']);
exit;
}
try {
// Get current published status
$stmt = $conn->prepare("SELECT published FROM events WHERE event_id = ?");
if (!$stmt) {
throw new Exception("Prepare failed: " . $conn->error);
}
$stmt->bind_param("i", $event_id);
$stmt->execute();
$result = $stmt->get_result();
if ($result->num_rows === 0) {
http_response_code(404);
echo json_encode(['status' => 'error', 'message' => 'Event not found']);
$stmt->close();
exit;
}
$event = $result->fetch_assoc();
$new_status = $event['published'] == 1 ? 0 : 1;
$stmt->close();
// Update published status
$update_stmt = $conn->prepare("UPDATE events SET published = ?, updated_at = NOW() WHERE event_id = ?");
if (!$update_stmt) {
throw new Exception("Prepare failed: " . $conn->error);
}
$update_stmt->bind_param("ii", $new_status, $event_id);
if ($update_stmt->execute()) {
ob_clean(); // Clean any buffered output before sending JSON
http_response_code(200);
echo json_encode([
'status' => 'success',
'message' => $new_status == 1 ? 'Event published' : 'Event unpublished',
'published' => $new_status
]);
} else {
throw new Exception("Update failed: " . $update_stmt->error);
}
$update_stmt->close();
} catch (Exception $e) {
ob_clean(); // Clean any buffered output before sending JSON
http_response_code(500);
echo json_encode(['status' => 'error', 'message' => 'Database error: ' . $e->getMessage()]);
}

View File

@@ -6,7 +6,16 @@ include_once('../config/functions.php');
$conn = openDatabaseConnection(); $conn = openDatabaseConnection();
$stmt = $conn->prepare("SELECT $stmt = $conn->prepare("SELECT
c.*, c.id,
c.name,
c.description,
c.website,
c.telephone,
c.latitude,
c.longitude,
c.thumbnail,
c.country,
c.province,
u.first_name, u.first_name,
u.last_name, u.last_name,
u.profile_pic u.profile_pic
@@ -26,6 +35,8 @@ while ($row = $result->fetch_assoc()) {
'latitude' => $row['latitude'], 'latitude' => $row['latitude'],
'longitude' => $row['longitude'], 'longitude' => $row['longitude'],
'thumbnail' => $row['thumbnail'], 'thumbnail' => $row['thumbnail'],
'country' => $row['country'],
'province' => $row['province'],
'user' => [ 'user' => [
'first_name' => $row['first_name'], 'first_name' => $row['first_name'],
'last_name' => $row['last_name'], 'last_name' => $row['last_name'],

View File

@@ -1434,6 +1434,10 @@ function checkMembershipApplication($user_id)
// Check if the record exists and redirect // Check if the record exists and redirect
if ($count > 0) { if ($count > 0) {
// Set a session message before redirecting
if (!isset($_SESSION['message'])) {
$_SESSION['message'] = 'You have already submitted a membership application.';
}
header("Location: membership_details.php"); header("Location: membership_details.php");
exit(); exit();
} }
@@ -2195,8 +2199,8 @@ function validateName($name, $minLength = 2, $maxLength = 100) {
return false; return false;
} }
// Only allow letters, spaces, hyphens, and apostrophes // Allow letters, numbers, spaces, hyphens, and apostrophes
if (!preg_match('/^[a-zA-Z\s\'-]+$/', $name)) { if (!preg_match('/^[a-zA-Z0-9\s\'-]+$/', $name)) {
return false; return false;
} }

118
src/pages/add_campsite.php Normal file
View File

@@ -0,0 +1,118 @@
<?php
$rootPath = dirname(dirname(__DIR__));
require_once($rootPath . '/src/config/env.php');
include_once($rootPath . '/src/config/connection.php');
include_once($rootPath . '/src/config/functions.php');
session_start();
$user_id = $_SESSION['user_id'] ?? null;
// CSRF Token Validation
if (!isset($_POST['csrf_token']) || !validateCSRFToken($_POST['csrf_token'])) {
http_response_code(403);
die('Security token validation failed. Please try again.');
}
// campsites.php
$conn = openDatabaseConnection();
// Get text inputs
$name = validateName($_POST['name'] ?? '') ?: '';
$desc = isset($_POST['description']) ? htmlspecialchars($_POST['description'], ENT_QUOTES, 'UTF-8') : '';
$country = isset($_POST['country']) ? htmlspecialchars($_POST['country'], ENT_QUOTES, 'UTF-8') : '';
$province = isset($_POST['province']) ? htmlspecialchars($_POST['province'], ENT_QUOTES, 'UTF-8') : '';
$lat = isset($_POST['latitude']) ? floatval($_POST['latitude']) : 0.0;
$lng = isset($_POST['longitude']) ? floatval($_POST['longitude']) : 0.0;
$website = isset($_POST['website']) ? filter_var($_POST['website'], FILTER_VALIDATE_URL) : '';
$telephone = validatePhoneNumber($_POST['telephone'] ?? '') ?: '';
if (empty($name)) {
http_response_code(400);
die('Campsite name is required.');
}
// Handle file upload
$thumbnailPath = null;
if (isset($_FILES['thumbnail']) && $_FILES['thumbnail']['error'] !== UPLOAD_ERR_NO_FILE) {
// Validate file using hardened validation function
$validationResult = validateFileUpload($_FILES['thumbnail'], 'profile_picture');
if ($validationResult === false) {
http_response_code(400);
die('Invalid thumbnail image. Only JPG, JPEG, PNG, GIF, and WEBP images under 5MB are allowed.');
}
$uploadDir = $rootPath . "/assets/uploads/campsites/";
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0755, true);
}
if (!is_writable($uploadDir)) {
http_response_code(500);
die('Upload directory is not writable.');
}
$randomFilename = $validationResult['filename'];
$targetFile = $uploadDir . $randomFilename;
if (move_uploaded_file($_FILES["thumbnail"]["tmp_name"], $targetFile)) {
chmod($targetFile, 0644);
$thumbnailPath = "assets/uploads/campsites/" . $randomFilename;
} else {
http_response_code(500);
die('Failed to move uploaded file.');
}
}
$id = isset($_POST['id']) ? intval($_POST['id']) : 0;
if ($id > 0) {
// Verify ownership - check if the campsite belongs to the current user
$ownerCheckStmt = $conn->prepare("SELECT user_id FROM campsites WHERE id = ?");
$ownerCheckStmt->bind_param("i", $id);
$ownerCheckStmt->execute();
$ownerResult = $ownerCheckStmt->get_result();
if ($ownerResult->num_rows === 0) {
http_response_code(404);
die('Campsite not found.');
}
$ownerRow = $ownerResult->fetch_assoc();
if ($ownerRow['user_id'] != $user_id) {
http_response_code(403);
die('You do not have permission to edit this campsite. Only the owner can make changes.');
}
$ownerCheckStmt->close();
// UPDATE
if ($thumbnailPath) {
$stmt = $conn->prepare("UPDATE campsites SET name=?, description=?, country=?, province=?, latitude=?, longitude=?, website=?, telephone=?, thumbnail=? WHERE id=?");
$stmt->bind_param("ssssddsssi", $name, $desc, $country, $province, $lat, $lng, $website, $telephone, $thumbnailPath, $id);
} else {
$stmt = $conn->prepare("UPDATE campsites SET name=?, description=?, country=?, province=?, latitude=?, longitude=?, website=?, telephone=? WHERE id=?");
$stmt->bind_param("ssssddssi", $name, $desc, $country, $province, $lat, $lng, $website, $telephone, $id);
}
// Log the action
auditLog($user_id, 'CAMPSITE_UPDATE', 'campsites', $id, ['name' => $name]);
} else {
// INSERT
$stmt = $conn->prepare("INSERT INTO campsites (name, description, country, province, latitude, longitude, website, telephone, thumbnail, user_id)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
$stmt->bind_param("ssssddsssi", $name, $desc, $country, $province, $lat, $lng, $website, $telephone, $thumbnailPath, $user_id);
// Log the action
auditLog($user_id, 'CAMPSITE_CREATE', 'campsites', 0, ['name' => $name]);
}
if (!$stmt->execute()) {
http_response_code(500);
die('Database error: ' . $stmt->error);
}
$stmt->close();
header("Location: campsites");
?>

View File

@@ -3,6 +3,18 @@ $headerStyle = 'light';
$rootPath = dirname(dirname(dirname(__DIR__))); $rootPath = dirname(dirname(dirname(__DIR__)));
include_once($rootPath . '/header.php'); include_once($rootPath . '/header.php');
// Check if user has active membership
if (!isset($_SESSION['user_id'])) {
header('Location: login');
exit;
}
$is_member = getUserMemberStatus($_SESSION['user_id']);
if (!$is_member) {
header('Location: index');
exit;
}
$conn = openDatabaseConnection(); $conn = openDatabaseConnection();
$stmt = $conn->prepare("SELECT * FROM campsites"); $stmt = $conn->prepare("SELECT * FROM campsites");
$stmt->execute(); $stmt->execute();
@@ -17,104 +29,526 @@ while ($row = $result->fetch_assoc()) {
#map { #map {
height: 600px; height: 600px;
width: 100%; width: 100%;
position: relative;
} }
.gm-style .info-box { /* Center pin overlay */
max-width: 250px; .map-center-pin {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -100%);
z-index: 10;
pointer-events: none;
font-size: 48px;
} }
.info-box img { /* Location mode indicator */
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); .location-mode-indicator {
position: absolute;
top: 20px;
left: 20px;
background: #4CAF50;
color: white;
padding: 12px 20px;
border-radius: 6px;
z-index: 11;
font-weight: 500;
display: none;
}
/* Confirm location button */
.confirm-location-btn {
position: absolute;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
background: #4CAF50;
color: white;
padding: 12px 30px;
border: none;
border-radius: 6px;
cursor: pointer;
font-weight: 500;
z-index: 11;
display: none;
}
.confirm-location-btn:hover {
background: #45a049;
}
.cancel-location-btn {
position: absolute;
bottom: 20px;
left: 20px;
background: #f44336;
color: white;
padding: 12px 30px;
border: none;
border-radius: 6px;
cursor: pointer;
font-weight: 500;
z-index: 11;
display: none;
}
.cancel-location-btn:hover {
background: #da190b;
}
/* Form styling to match manage_trips */
.campsite-form-container {
background: #f9f9f7;
border: 1px solid #d8d8d8;
border-radius: 10px;
padding: 30px;
margin: 20px 0;
display: none;
}
.campsite-form-container h5 {
color: #2c3e50;
font-weight: 600;
margin-bottom: 30px;
font-size: 1.5rem;
}
.campsite-form-container .form-group {
margin-bottom: 20px;
}
.campsite-form-container label {
font-weight: 500;
color: #34495e;
margin-bottom: 8px;
display: block;
}
.campsite-form-container .form-control {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 14px;
}
.campsite-form-container .form-control:focus {
border-color: #4CAF50;
box-shadow: 0 0 0 0.2rem rgba(76, 175, 80, 0.25);
outline: none;
}
.campsite-form-container .form-control select {
cursor: pointer;
}
.campsite-form-container .btn {
border-radius: 6px;
font-weight: 500;
padding: 10px 20px;
}
/* Table styling to match admin trips */
.campsites-table {
width: 100%;
border-collapse: separate;
border-spacing: 0;
margin: 10px 0;
}
.campsites-table thead th {
cursor: pointer;
text-align: left;
padding: 10px;
font-weight: bold;
position: relative;
}
.campsites-table thead th::after {
content: '\25B2';
font-size: 0.8em;
position: absolute;
right: 10px;
opacity: 0;
transition: opacity 0.2s;
}
.campsites-table thead th.asc::after {
content: '\25B2';
opacity: 1;
}
.campsites-table thead th.desc::after {
content: '\25BC';
opacity: 1;
}
.campsites-table tbody tr:nth-child(odd) {
background-color: transparent;
}
.campsites-table tbody tr:nth-child(even) {
background-color: rgb(255, 255, 255);
border-radius: 10px;
}
.campsites-table tbody td {
padding: 10px;
}
.campsites-table tbody tr:nth-child(even) td:first-child {
border-top-left-radius: 10px;
border-bottom-left-radius: 10px;
}
.campsites-table 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: 20px;
border: 1px solid #ddd;
}
.campsite-group {
color: #484848;
background: #f9f9f7;
border: 1px solid #d8d8d8;
border-radius: 10px;
margin-top: 15px;
margin-bottom: 15px;
padding: 10px;
}
</style> </style>
<?php <?php
$pageTitle = 'Campsites'; $pageTitle = 'Campsites Directory';
$breadcrumbs = [['Home' => 'index.php']]; $breadcrumbs = [['Home' => 'index.php']];
require_once($rootPath . '/components/banner.php'); require_once($rootPath . '/components/banner.php');
?> ?>
<!-- Tour List Area start --> <section class="tour-list-page py-100 rel">
<section class="tour-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">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
<h3>Campsites Map</h3>
<button class="theme-btn" id="toggleFormBtn" onclick="startLocationMode()">
<i class="far fa-plus"></i> Add Campsite
</button>
</div>
<p style="color: #666; margin-bottom: 15px;">Click on a marker to view details, or use the "Add Campsite" button to add a new location.</p>
<!-- Map with location mode UI -->
<div style="position: relative; margin-bottom: 20px;">
<div id="map" style="width: 100%; height: 500px;"></div>
<!-- Location Mode Indicator -->
<div class="location-mode-indicator">
📍 Position the map center pin over your campsite location
</div>
<!-- Confirm and Cancel Buttons -->
<button type="button" class="confirm-location-btn" onclick="confirmLocation()">
✓ Confirm Location
</button>
<button type="button" class="cancel-location-btn" onclick="cancelLocationMode()">
✕ Cancel
</button>
</div>
<!-- Collapsible Campsite Form -->
<div class="campsite-form-container" id="campsiteFormContainer">
<h5>Add New Campsite</h5>
<form id="addCampsiteForm" method="POST" action="add_campsite" enctype="multipart/form-data">
<input type="hidden" name="csrf_token" value="<?php echo generateCSRFToken(); ?>">
<input type="hidden" name="latitude" id="latitude">
<input type="hidden" name="longitude" id="longitude">
<div class="row mt-35">
<div class="col-md-6">
<div class="form-group">
<label for="campsite_name">Campsite Name *</label>
<input type="text" id="campsite_name" class="form-control" name="name" required>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="countrySelect">Country *</label>
<select id="countrySelect" class="form-control" name="country" required>
<option value="">-- Select Country --</option>
<option value="South Africa">South Africa</option>
<option value="Botswana">Botswana</option>
<option value="Eswatini">Eswatini</option>
<option value="Lesotho">Lesotho</option>
<option value="Namibia">Namibia</option>
<option value="Zimbabwe">Zimbabwe</option>
<option value="Other">Other</option>
</select>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="provinceSelect">Province *</label>
<select id="provinceSelect" class="form-control" name="province" required>
<option value="">-- Select Province --</option>
<option value="Eastern Cape">Eastern Cape</option>
<option value="Free State">Free State</option>
<option value="Gauteng">Gauteng</option>
<option value="KwaZulu-Natal">KwaZulu-Natal</option>
<option value="Limpopo">Limpopo</option>
<option value="Mpumalanga">Mpumalanga</option>
<option value="Northern Cape">Northern Cape</option>
<option value="North West">North West</option>
<option value="Western Cape">Western Cape</option>
<option value="Other">Other</option>
</select>
</div>
</div>
<div class="col-md-12">
<div class="form-group">
<label for="campsite_description">Description</label>
<textarea id="campsite_description" class="form-control" name="description" rows="3"></textarea>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="campsite_website">Booking URL</label>
<input type="url" id="campsite_website" class="form-control" name="website">
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="campsite_phone">Phone Number</label>
<input type="text" id="campsite_phone" class="form-control" name="telephone">
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="latitude_display">Latitude</label>
<input type="text" id="latitude_display" class="form-control" readonly>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="longitude_display">Longitude</label>
<input type="text" id="longitude_display" class="form-control" readonly>
</div>
</div>
<div class="col-md-12">
<div class="form-group">
<label for="campsite_thumbnail">Thumbnail Image</label>
<input type="file" id="campsite_thumbnail" class="form-control" name="thumbnail" accept="image/*">
</div>
</div>
<div class="col-md-12">
<div class="form-group mb-0">
<button class="theme-btn style-two" type="submit" style="width: 100%; margin-right: 10px;">Save Campsite</button>
<button class="theme-btn" type="button" onclick="toggleCampsiteForm()" style="width: 100%; margin-top: 10px;">Cancel</button>
</div>
</div>
</div>
</form>
</div>
<!-- Campsites Table -->
<div style="margin-top: 40px;">
<h4 style="margin-bottom: 20px;">All Campsites</h4>
<input type="text" class="filter-input" id="campsitesFilter" placeholder="Filter results...">
<div class="table-responsive">
<table class="campsites-table">
<thead class="table-light">
<tr>
<th>Name</th>
<th>Description</th>
<th>Booking Website</th>
<th>Phone</th>
<th>Added By</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="campsitesTableBody">
<!-- Populated by JavaScript -->
</tbody>
</table>
</div>
</div>
<div id="map" style="width: 100%; height: 500px;"></div>
<!-- Add Campsite Modal -->
</div> </div>
</div> </div>
</div> </div>
</section> </section>
<div class="modal fade" id="addCampsiteModal" tabindex="-1">
<div class="modal-dialog">
<form id="addCampsiteForm" method="POST" action="add_campsite" enctype="multipart/form-data">
<input type="hidden" name="csrf_token" value="<?php echo generateCSRFToken(); ?>">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Add Campsite</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<input type="hidden" name="latitude" id="latitude">
<input type="hidden" name="longitude" id="longitude">
<div class="mb-3">
<label class="form-label">Campsite Name</label>
<input type="text" class="form-control" name="name" required>
</div>
<div class="mb-3">
<label class="form-label">Description</label>
<textarea class="form-control" name="description" rows="3"></textarea>
</div>
<div class="mb-3">
<label class="form-label">Booking URL</label>
<input type="url" class="form-control" name="website">
</div>
<div class="mb-3">
<label class="form-label">Phone Number</label>
<input type="text" class="form-control" name="telephone">
</div>
<div class="mb-3">
<label class="form-label">Thumbnail Image</label>
<input type="file" class="form-control" name="thumbnail" accept="image/*">
</div>
</div>
<div class="modal-footer">
<button class="btn btn-primary" type="submit">Save Campsite</button>
<button class="btn btn-secondary" type="button" data-bs-dismiss="modal">Cancel</button>
</div>
</div>
</form>
</div>
</div>
<script> <script>
let map; let map;
let centerPinMarker;
let isLocationMode = false;
const currentUserId = <?php echo $_SESSION['user_id']; ?>;
const campsites = <?php echo json_encode($campsites); ?>; const campsites = <?php echo json_encode($campsites); ?>;
function startLocationMode() {
if (isLocationMode) return;
isLocationMode = true;
// Show location mode UI elements
document.querySelector(".location-mode-indicator").style.display = "block";
document.querySelector(".confirm-location-btn").style.display = "block";
document.querySelector(".cancel-location-btn").style.display = "block";
document.getElementById("toggleFormBtn").disabled = true;
// Create invisible marker at map center
const mapCenter = map.getCenter();
centerPinMarker = new google.maps.Marker({
position: mapCenter,
map: map,
title: "Campsite Location",
draggable: true,
icon: 'http://maps.google.com/mapfiles/ms/icons/red-dot.png'
});
// Update coordinates when marker is dragged
centerPinMarker.addListener('drag', function() {
const position = centerPinMarker.getPosition();
updateCoordinatesDisplay(position.lat(), position.lng());
});
// Set initial coordinates
updateCoordinatesDisplay(mapCenter.lat(), mapCenter.lng());
// Update coordinates when map is moved
const moveListener = map.addListener('center_changed', function() {
const mapCenter = map.getCenter();
centerPinMarker.setPosition(mapCenter);
updateCoordinatesDisplay(mapCenter.lat(), mapCenter.lng());
});
// Store listener for cleanup
window.mapMoveListener = moveListener;
}
function updateCoordinatesDisplay(lat, lng) {
document.getElementById("latitude").value = lat;
document.getElementById("longitude").value = lng;
document.getElementById("latitude_display").value = lat.toFixed(6);
document.getElementById("longitude_display").value = lng.toFixed(6);
}
function confirmLocation() {
if (!isLocationMode) return;
isLocationMode = false;
// Hide location mode UI elements
document.querySelector(".location-mode-indicator").style.display = "none";
document.querySelector(".confirm-location-btn").style.display = "none";
document.querySelector(".cancel-location-btn").style.display = "none";
document.getElementById("toggleFormBtn").disabled = false;
// Remove map move listener
if (window.mapMoveListener) {
google.maps.event.removeListener(window.mapMoveListener);
}
// Remove the center marker
if (centerPinMarker) {
centerPinMarker.setMap(null);
centerPinMarker = null;
}
// Reset form fields and show form (for new campsite only)
resetFormForNewCampsite();
document.getElementById("campsiteFormContainer").style.display = "block";
document.getElementById("campsiteFormContainer").scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
function cancelLocationMode() {
if (!isLocationMode) return;
isLocationMode = false;
// Hide location mode UI elements
document.querySelector(".location-mode-indicator").style.display = "none";
document.querySelector(".confirm-location-btn").style.display = "none";
document.querySelector(".cancel-location-btn").style.display = "none";
document.getElementById("toggleFormBtn").disabled = false;
// Remove map move listener
if (window.mapMoveListener) {
google.maps.event.removeListener(window.mapMoveListener);
}
// Remove the center marker
if (centerPinMarker) {
centerPinMarker.setMap(null);
centerPinMarker = null;
}
}
function toggleCampsiteForm() {
if (isLocationMode) return;
const container = document.getElementById("campsiteFormContainer");
container.style.display = container.style.display === "none" ? "block" : "none";
if (container.style.display === "block") {
container.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
}
function resetFormForNewCampsite() {
// This is called when confirming location for a NEW campsite
// Only clears text fields and removes ID, but keeps country/province selections
document.querySelector("#addCampsiteForm input[name='name']").value = '';
document.querySelector("#addCampsiteForm textarea[name='description']").value = '';
document.querySelector("#addCampsiteForm input[name='website']").value = '';
document.querySelector("#addCampsiteForm input[name='telephone']").value = '';
// Remove the ID input if it exists
let idInput = document.querySelector("#addCampsiteForm input[name='id']");
if (idInput) {
idInput.remove();
}
// Change form heading
document.querySelector("#campsiteFormContainer h5").textContent = "Add New Campsite";
}
function resetForm() {
// This is called when canceling the form - fully resets everything
document.querySelector("#campsiteFormContainer h5").textContent = "Add New Campsite";
// Clear the form completely
document.getElementById("addCampsiteForm").reset();
// Remove the ID input if it exists
let idInput = document.querySelector("#addCampsiteForm input[name='id']");
if (idInput) {
idInput.remove();
}
// Clear coordinate displays
document.getElementById("latitude_display").value = '';
document.getElementById("longitude_display").value = '';
}
function initMap() { function initMap() {
map = new google.maps.Map(document.getElementById("map"), { map = new google.maps.Map(document.getElementById("map"), {
center: { center: {
lat: -28.0, lat: -28.0,
lng: 24.0 lng: 24.0
}, // SA center },
zoom: 6, zoom: 6,
}); });
map.addListener("click", function(e) {
const lat = e.latLng.lat();
const lng = e.latLng.lng();
document.getElementById("latitude").value = lat;
document.getElementById("longitude").value = lng;
const addModal = new bootstrap.Modal(document.getElementById("addCampsiteModal"));
addModal.show();
});
// Load existing campsites from PHP // Load existing campsites from PHP
fetch("get_campsites.php") fetch("get_campsites")
.then(response => response.json()) .then(response => response.json())
.then(data => { .then(data => {
data.forEach(site => { data.forEach(site => {
@@ -133,7 +567,7 @@ while ($row = $result->fetch_assoc()) {
${site.description ? site.description + "<br>" : ""} ${site.description ? site.description + "<br>" : ""}
${site.website ? `<a href="${site.website}" target="_blank">Visit Website</a><br>` : ""} ${site.website ? `<a href="${site.website}" target="_blank">Visit Website</a><br>` : ""}
${site.telephone ? `Phone: ${site.telephone}<br>` : ""} ${site.telephone ? `Phone: ${site.telephone}<br>` : ""}
${site.thumbnail ? `<img src="${site.thumbnail}" style="width: 100%; max-width: 200px; border-radius: 8px; margin-top: 5px;">` : ""} ${site.thumbnail ? `<img src="${site.thumbnail}" style="width: 100%; max-width: 200px; border-radius: 8px; margin-top: 5px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);">` : ""}
${site.user && site.user.first_name ? ` ${site.user && site.user.first_name ? `
<div class="user-info mt-2 d-flex align-items-center"> <div class="user-info mt-2 d-flex align-items-center">
<img src="${site.user.profile_pic}" style="width: 40px; height: 40px; border-radius: 50%; object-fit: cover; margin-right: 10px;"> <img src="${site.user.profile_pic}" style="width: 40px; height: 40px; border-radius: 50%; object-fit: cover; margin-right: 10px;">
@@ -156,40 +590,177 @@ while ($row = $result->fetch_assoc()) {
infowindow.open(map, marker); infowindow.open(map, marker);
}); });
}); });
// Populate the table
populateCampsitesTable(data);
}) })
.catch(err => console.error("Failed to load campsites:", err)); .catch(err => console.error("Failed to load campsites:", err));
} }
function populateCampsitesTable(campsites) {
const tableBody = document.getElementById("campsitesTableBody");
tableBody.innerHTML = ""; // Clear existing rows
if (campsites.length === 0) {
tableBody.innerHTML = `
<tr>
<td colspan="6" class="text-center text-muted" style="padding: 30px;">
No campsites added yet. Click on the map to add one!
</td>
</tr>
`;
return;
}
// Group campsites by country and province
const groupedByCountryAndProvince = {};
campsites.forEach(site => {
const country = site.country || 'Unknown Country';
const province = site.province || 'Unknown Province';
if (!groupedByCountryAndProvince[country]) {
groupedByCountryAndProvince[country] = {};
}
if (!groupedByCountryAndProvince[country][province]) {
groupedByCountryAndProvince[country][province] = [];
}
groupedByCountryAndProvince[country][province].push(site);
});
// Sort countries alphabetically
const sortedCountries = Object.keys(groupedByCountryAndProvince).sort();
// Populate table with grouped data
sortedCountries.forEach(country => {
// Sort provinces alphabetically for this country
const sortedProvinces = Object.keys(groupedByCountryAndProvince[country]).sort();
sortedProvinces.forEach(province => {
// Add province group header
const groupRow = document.createElement("tr");
groupRow.innerHTML = `
<td colspan="6" style="font-weight: 600; padding: 10px 8px; background-color: #f0f0f0;">
<i class="fas fa-globe" style="color: #2196F3; margin-right: 8px;"></i>${country} - ${province}
</td>
`;
tableBody.appendChild(groupRow);
// Add campsite rows for this province
groupedByCountryAndProvince[country][province].forEach(site => {
const row = document.createElement("tr");
const userName = site.user && site.user.first_name
? `${site.user.first_name} ${site.user.last_name}`
: "Unknown";
// Only show edit button if current user is the owner
const editButtonHTML = site.user_id == currentUserId
? `<button class="btn btn-sm btn-warning" onclick='editCampsite(${JSON.stringify(site)})'>Edit</button>`
: '';
row.innerHTML = `
<td><strong>${site.name}</strong></td>
<td>${site.description ? site.description.substring(0, 50) + (site.description.length > 50 ? '...' : '') : '-'}</td>
<td>${site.website ? `<a href="${site.website}" target="_blank" class="link-primary">Visit</a>` : '-'}</td>
<td>${site.telephone || '-'}</td>
<td><small>${userName}</small></td>
<td>
${editButtonHTML}
<a href="https://www.google.com/maps/dir/?api=1&destination=${site.latitude},${site.longitude}" target="_blank" class="btn btn-sm btn-outline-primary">Directions</a>
</td>
`;
tableBody.appendChild(row);
});
});
});
}
function editCampsite(site) { function editCampsite(site) {
// Pre-fill form // Change form heading to indicate editing
document.querySelector("#addCampsiteForm input[name='name']").value = site.name; document.querySelector("#campsiteFormContainer h5").textContent = "Edit Campsite";
document.querySelector("#addCampsiteForm textarea[name='description']").value = site.description || "";
document.querySelector("#addCampsiteForm input[name='website']").value = site.website || ""; // Pre-fill form with a slight delay to ensure DOM is ready
document.querySelector("#addCampsiteForm input[name='telephone']").value = site.telephone || ""; setTimeout(() => {
document.querySelector("#addCampsiteForm input[name='latitude']").value = site.latitude; document.querySelector("#addCampsiteForm input[name='name']").value = site.name;
document.querySelector("#addCampsiteForm input[name='longitude']").value = site.longitude; document.querySelector("#addCampsiteForm textarea[name='description']").value = site.description || "";
document.querySelector("#addCampsiteForm input[name='website']").value = site.website || "";
document.querySelector("#addCampsiteForm input[name='telephone']").value = site.telephone || "";
document.querySelector("#addCampsiteForm input[name='latitude']").value = site.latitude;
document.querySelector("#addCampsiteForm input[name='longitude']").value = site.longitude;
document.getElementById("latitude_display").value = parseFloat(site.latitude).toFixed(6);
document.getElementById("longitude_display").value = parseFloat(site.longitude).toFixed(6);
// Set country and province LAST to ensure they stick
document.querySelector("#addCampsiteForm select[name='country']").value = site.country || '';
document.querySelector("#addCampsiteForm select[name='province']").value = site.province || '';
// Add hidden ID input // Add hidden ID input
let idInput = document.querySelector("#addCampsiteForm input[name='id']"); let idInput = document.querySelector("#addCampsiteForm input[name='id']");
if (!idInput) { if (!idInput) {
idInput = document.createElement("input"); idInput = document.createElement("input");
idInput.type = "hidden"; idInput.type = "hidden";
idInput.name = "id"; idInput.name = "id";
document.querySelector("#addCampsiteForm").appendChild(idInput); document.querySelector("#addCampsiteForm").appendChild(idInput);
} }
idInput.value = site.id; idInput.value = site.id;
}, 0);
// Show the modal // Show the form container
const addModal = new bootstrap.Modal(document.getElementById("addCampsiteModal")); document.getElementById("campsiteFormContainer").style.display = "block";
addModal.show(); document.getElementById("campsiteFormContainer").scrollIntoView({ behavior: 'smooth', block: 'nearest' });
} }
function filterCampsites() {
const filterInput = document.getElementById("campsitesFilter");
const filterValue = filterInput.value.toLowerCase();
const tableBody = document.getElementById("campsitesTableBody");
const rows = tableBody.getElementsByTagName("tr");
let visibleRows = 0;
for (let i = 0; i < rows.length; i++) {
const row = rows[i];
const text = row.textContent.toLowerCase();
// Show rows that match the filter or are group headers
if (text.includes(filterValue) || row.innerHTML.includes('fas fa-globe')) {
row.style.display = "";
if (row.innerHTML.includes('fas fa-globe') === false) {
visibleRows++;
}
} else {
row.style.display = "none";
}
}
// Hide group headers if no campsites match in that group
for (let i = 0; i < rows.length; i++) {
const row = rows[i];
if (row.innerHTML.includes('fas fa-globe')) {
// Check if next visible row is also a header
let hasVisibleChildren = false;
for (let j = i + 1; j < rows.length; j++) {
if (rows[j].style.display !== "none") {
if (!rows[j].innerHTML.includes('fas fa-globe')) {
hasVisibleChildren = true;
}
break;
}
}
row.style.display = hasVisibleChildren ? "" : "none";
}
}
}
// Add filter event listener when page loads
document.addEventListener("DOMContentLoaded", function() {
const filterInput = document.getElementById("campsitesFilter");
if (filterInput) {
filterInput.addEventListener("keyup", filterCampsites);
}
});
</script> </script>
<script src="https://maps.googleapis.com/maps/api/js?key=AIzaSyC-JuvnbUYc8WGjQBFFVZtKiv5_bFJoWLU&callback=initMap" async defer></script> <script src="https://maps.googleapis.com/maps/api/js?key=AIzaSyC-JuvnbUYc8WGjQBFFVZtKiv5_bFJoWLU&callback=initMap" async defer></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<?php include_once(dirname(dirname(dirname(__DIR__))) . '/components/insta_footer.php'); ?> <?php include_once(dirname(dirname(dirname(__DIR__))) . '/components/insta_footer.php'); ?>

View File

@@ -90,8 +90,8 @@ include_once($rootPath . '/header.php');
</div> </div>
<?php <?php
// Query to retrieve upcoming events // Query to retrieve upcoming published events only
$stmt = $conn->prepare("SELECT event_id, date, time, name, image, description, feature, location, type, promo FROM events WHERE date > CURDATE() ORDER BY date ASC"); $stmt = $conn->prepare("SELECT event_id, date, time, name, image, description, feature, location, type, promo FROM events WHERE date > CURDATE() AND published = 1 ORDER BY date ASC");
$stmt->execute(); $stmt->execute();
$result = $stmt->get_result(); $result = $stmt->get_result();

View File

@@ -9,7 +9,7 @@ $eft_id = strtoupper("SUBS " . date("Y") . " " . getLastName($user_id));
$status = 'AWAITING PAYMENT'; $status = 'AWAITING PAYMENT';
$description = 'Membership Fees ' . date("Y") . " " . getLastName($user_id); $description = 'Membership Fees ' . date("Y") . " " . getLastName($user_id);
$payment_amount = 2500; // Assuming a fixed membership fee, adjust as needed $payment_amount = 2600; // Assuming a fixed membership fee, adjust as needed
$payment_date = date('Y-m-d'); $payment_date = date('Y-m-d');
$membership_start_date = date('Y-01-01'); $membership_start_date = date('Y-01-01');
$membership_end_date = date('Y-12-31'); $membership_end_date = date('Y-12-31');

View File

@@ -18,6 +18,40 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
die('Security token validation failed. Please try again.'); die('Security token validation failed. Please try again.');
} }
// Check if user already has a membership application
$check_stmt = $conn->prepare("SELECT COUNT(*) as count FROM membership_application WHERE user_id = ?");
$check_stmt->bind_param("i", $user_id);
$check_stmt->execute();
$check_result = $check_stmt->get_result();
$check_row = $check_result->fetch_assoc();
$check_stmt->close();
if ($check_row['count'] > 0) {
http_response_code(400);
echo json_encode([
'status' => 'error',
'message' => 'You have already submitted a membership application. Please check your email for membership details.'
]);
exit;
}
// Check if user already has a membership fee record
$fee_check_stmt = $conn->prepare("SELECT COUNT(*) as count FROM membership_fees WHERE user_id = ?");
$fee_check_stmt->bind_param("i", $user_id);
$fee_check_stmt->execute();
$fee_result = $fee_check_stmt->get_result();
$fee_row = $fee_result->fetch_assoc();
$fee_check_stmt->close();
if ($fee_row['count'] > 0) {
http_response_code(400);
echo json_encode([
'status' => 'error',
'message' => 'You already have a membership fee record. Please contact support if you need to update your application.'
]);
exit;
}
// Get all the form fields with validation // Get all the form fields with validation
$first_name = validateName($_POST['first_name'] ?? ''); $first_name = validateName($_POST['first_name'] ?? '');
if ($first_name === false) { if ($first_name === false) {
@@ -188,11 +222,20 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// Rollback the transaction in case of error // Rollback the transaction in case of error
$conn->rollback(); $conn->rollback();
// Error response // Check for duplicate key error
$response = [ $errorMessage = $e->getMessage();
'status' => 'error', if (strpos($errorMessage, 'Duplicate') !== false || strpos($errorMessage, '1062') !== false) {
'message' => 'Error: ' . $e->getMessage() $response = [
]; 'status' => 'error',
'message' => 'You have already submitted a membership application. Please check your email for membership details.'
];
} else {
// Error response
$response = [
'status' => 'error',
'message' => 'Error: ' . $errorMessage
];
}
} }
// Return the response in JSON format // Return the response in JSON format