Compare commits
14 Commits
feature/ca
...
feature/ph
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
325e2b4707 | ||
|
|
233305cac2 | ||
|
|
5736757f19 | ||
|
|
ad460ef85a | ||
|
|
e6d298c506 | ||
|
|
98ef03c7af | ||
|
|
05f74f1b86 | ||
|
|
9133b7bbc6 | ||
|
|
b52c46b67c | ||
|
|
32651ed433 | ||
|
|
f522b84fc1 | ||
|
|
2b136c4b06 | ||
|
|
7f0964009a | ||
|
|
5be946f78f |
16
.htaccess
@@ -51,6 +51,12 @@ RewriteRule ^payment_confirmation$ src/pages/shop/payment_confirmation.php [L]
|
|||||||
RewriteRule ^confirm$ src/pages/shop/confirm.php [L]
|
RewriteRule ^confirm$ src/pages/shop/confirm.php [L]
|
||||||
RewriteRule ^confirm2$ src/pages/shop/confirm2.php [L]
|
RewriteRule ^confirm2$ src/pages/shop/confirm2.php [L]
|
||||||
|
|
||||||
|
# === GALLERY PAGES ===
|
||||||
|
RewriteRule ^gallery$ src/pages/gallery/gallery.php [L]
|
||||||
|
RewriteRule ^create_album$ src/pages/gallery/create_album.php [L]
|
||||||
|
RewriteRule ^edit_album$ src/pages/gallery/create_album.php [L]
|
||||||
|
RewriteRule ^view_album$ src/pages/gallery/view_album.php [L]
|
||||||
|
|
||||||
# === EVENTS & BLOG PAGES ===
|
# === EVENTS & BLOG PAGES ===
|
||||||
RewriteRule ^events$ src/pages/events/events.php [L]
|
RewriteRule ^events$ src/pages/events/events.php [L]
|
||||||
RewriteRule ^blog$ src/pages/events/blog.php [L]
|
RewriteRule ^blog$ src/pages/events/blog.php [L]
|
||||||
@@ -77,12 +83,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 ^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 +122,16 @@ 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]
|
||||||
|
RewriteRule ^save_album$ src/processors/save_album.php [L]
|
||||||
|
RewriteRule ^update_album$ src/processors/update_album.php [L]
|
||||||
|
RewriteRule ^delete_album$ src/processors/delete_album.php [L]
|
||||||
|
RewriteRule ^delete_photo$ src/processors/delete_photo.php [L]
|
||||||
|
RewriteRule ^get_album_photos$ src/processors/get_album_photos.php [L]
|
||||||
|
|
||||||
</IfModule>
|
</IfModule>
|
||||||
|
|
||||||
|
|||||||
BIN
assets/images/trips/9_01.jpg
Normal file
|
After Width: | Height: | Size: 124 KiB |
BIN
assets/images/trips/9_02.jpg
Normal file
|
After Width: | Height: | Size: 62 KiB |
BIN
assets/images/trips/9_03.jpg
Normal file
|
After Width: | Height: | Size: 119 KiB |
BIN
assets/images/trips/9_04.jpg
Normal file
|
After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 128 KiB After Width: | Height: | Size: 128 KiB |
BIN
assets/uploads/campsites/5a72387fdd1f6fc891e406c55b4b4723.jpg
Normal file
|
After Width: | Height: | Size: 259 KiB |
BIN
assets/uploads/campsites/785baf57034bf35bb3dc7954ca5789b7.jpg
Normal file
|
After Width: | Height: | Size: 259 KiB |
BIN
assets/uploads/campsites/aa2e5d1f0a9a81823b915d203ffadab2.jpg
Normal file
|
After Width: | Height: | Size: 168 KiB |
BIN
assets/uploads/gallery/1/cover_69329485037df.jpg
Normal file
|
After Width: | Height: | Size: 457 KiB |
BIN
assets/uploads/gallery/1/photo_693290ec06c76.jpg
Normal file
|
After Width: | Height: | Size: 663 KiB |
BIN
assets/uploads/gallery/1/photo_693290ec0b2d2.jpg
Normal file
|
After Width: | Height: | Size: 457 KiB |
BIN
assets/uploads/gallery/1/photo_693290ec0c36d.jpg
Normal file
|
After Width: | Height: | Size: 687 KiB |
BIN
assets/uploads/gallery/1/photo_693290ec0d366.jpg
Normal file
|
After Width: | Height: | Size: 254 KiB |
BIN
assets/uploads/gallery/1/photo_693290ec0dd32.jpg
Normal file
|
After Width: | Height: | Size: 280 KiB |
BIN
assets/uploads/gallery/1/photo_693290ec111a0.jpg
Normal file
|
After Width: | Height: | Size: 282 KiB |
BIN
assets/uploads/gallery/1/photo_693290ec11b36.jpg
Normal file
|
After Width: | Height: | Size: 79 KiB |
BIN
assets/uploads/gallery/1/photo_693290ec12280.jpg
Normal file
|
After Width: | Height: | Size: 302 KiB |
BIN
assets/uploads/gallery/1/photo_693290ec12de9.jpg
Normal file
|
After Width: | Height: | Size: 364 KiB |
BIN
assets/uploads/gallery/1/photo_693290ec13987.jpg
Normal file
|
After Width: | Height: | Size: 378 KiB |
BIN
assets/uploads/gallery/1/photo_693290ec1438d.jpg
Normal file
|
After Width: | Height: | Size: 76 KiB |
BIN
assets/uploads/gallery/1/photo_693290ec1487c.jpg
Normal file
|
After Width: | Height: | Size: 75 KiB |
BIN
assets/uploads/gallery/1/photo_693290ec14db9.jpg
Normal file
|
After Width: | Height: | Size: 171 KiB |
BIN
assets/uploads/gallery/1/photo_693290ec1567e.jpg
Normal file
|
After Width: | Height: | Size: 607 KiB |
BIN
assets/uploads/gallery/1/photo_693290ec16451.jpg
Normal file
|
After Width: | Height: | Size: 413 KiB |
BIN
assets/uploads/gallery/1/photo_693290ec16cc3.jpg
Normal file
|
After Width: | Height: | Size: 166 KiB |
BIN
assets/uploads/gallery/2/photo_69329220bd980.jpg
Normal file
|
After Width: | Height: | Size: 155 KiB |
BIN
assets/uploads/gallery/2/photo_69329220bdfbf.jpg
Normal file
|
After Width: | Height: | Size: 264 KiB |
BIN
assets/uploads/gallery/2/photo_69329220be409.jpg
Normal file
|
After Width: | Height: | Size: 237 KiB |
BIN
assets/uploads/gallery/2/photo_69329220be7d6.jpg
Normal file
|
After Width: | Height: | Size: 234 KiB |
BIN
assets/uploads/gallery/2/photo_69329220beb72.jpg
Normal file
|
After Width: | Height: | Size: 209 KiB |
BIN
assets/uploads/gallery/2/photo_69329220bef18.jpg
Normal file
|
After Width: | Height: | Size: 293 KiB |
BIN
assets/uploads/gallery/2/photo_69329220bf360.jpg
Normal file
|
After Width: | Height: | Size: 279 KiB |
BIN
assets/uploads/gallery/2/photo_69329220bf725.jpg
Normal file
|
After Width: | Height: | Size: 164 KiB |
BIN
assets/uploads/gallery/2/photo_69329220bfa53.jpg
Normal file
|
After Width: | Height: | Size: 177 KiB |
BIN
assets/uploads/gallery/2/photo_69329220bfe62.jpg
Normal file
|
After Width: | Height: | Size: 457 KiB |
BIN
assets/uploads/gallery/2/photo_69329220c0308.jpg
Normal file
|
After Width: | Height: | Size: 83 KiB |
BIN
assets/uploads/gallery/2/photo_69329220c06cb.jpg
Normal file
|
After Width: | Height: | Size: 560 KiB |
BIN
assets/uploads/gallery/2/photo_69329220c0d2f.jpg
Normal file
|
After Width: | Height: | Size: 514 KiB |
BIN
assets/uploads/gallery/2/photo_69329220c1293.jpg
Normal file
|
After Width: | Height: | Size: 304 KiB |
BIN
assets/uploads/gallery/2/photo_69329220c16c9.jpg
Normal file
|
After Width: | Height: | Size: 301 KiB |
BIN
assets/uploads/gallery/2/photo_69329220c1b9d.jpg
Normal file
|
After Width: | Height: | Size: 592 KiB |
BIN
assets/uploads/gallery/2/photo_69329220c2163.jpg
Normal file
|
After Width: | Height: | Size: 73 KiB |
BIN
assets/uploads/gallery/2/photo_69329220c24bb.jpg
Normal file
|
After Width: | Height: | Size: 397 KiB |
BIN
assets/uploads/gallery/2/photo_69329220c2a12.jpg
Normal file
|
After Width: | Height: | Size: 571 KiB |
BIN
assets/uploads/gallery/2/photo_69329220c3088.jpg
Normal file
|
After Width: | Height: | Size: 283 KiB |
BIN
assets/uploads/gallery/2/photo_6932937db759f.jpg
Normal file
|
After Width: | Height: | Size: 229 KiB |
BIN
assets/uploads/gallery/2/photo_6932937db7ab2.jpg
Normal file
|
After Width: | Height: | Size: 240 KiB |
BIN
assets/uploads/gallery/2/photo_6932937db7e76.jpg
Normal file
|
After Width: | Height: | Size: 229 KiB |
BIN
assets/uploads/gallery/2/photo_6932937db8219.jpg
Normal file
|
After Width: | Height: | Size: 197 KiB |
BIN
assets/uploads/gallery/2/photo_6932937db8606.jpg
Normal file
|
After Width: | Height: | Size: 329 KiB |
BIN
assets/uploads/gallery/2/photo_6932937db8b5c.jpg
Normal file
|
After Width: | Height: | Size: 593 KiB |
BIN
assets/uploads/gallery/2/photo_6932937db9160.jpg
Normal file
|
After Width: | Height: | Size: 114 KiB |
BIN
assets/uploads/gallery/2/photo_6932937db94ca.jpg
Normal file
|
After Width: | Height: | Size: 258 KiB |
BIN
assets/uploads/gallery/2/photo_6932937db990b.jpg
Normal file
|
After Width: | Height: | Size: 274 KiB |
BIN
assets/uploads/gallery/2/photo_6932937db9d6f.jpg
Normal file
|
After Width: | Height: | Size: 301 KiB |
BIN
assets/uploads/gallery/2/photo_6932937dba1a9.jpg
Normal file
|
After Width: | Height: | Size: 290 KiB |
BIN
assets/uploads/gallery/2/photo_6932937dba5fd.jpg
Normal file
|
After Width: | Height: | Size: 314 KiB |
BIN
assets/uploads/gallery/2/photo_6932937dbaa1e.jpg
Normal file
|
After Width: | Height: | Size: 184 KiB |
BIN
assets/uploads/gallery/2/photo_6932937dbadb0.jpg
Normal file
|
After Width: | Height: | Size: 304 KiB |
BIN
assets/uploads/gallery/2/photo_6932937dbb1b7.jpg
Normal file
|
After Width: | Height: | Size: 200 KiB |
BIN
assets/uploads/gallery/2/photo_6932937dbb55b.jpg
Normal file
|
After Width: | Height: | Size: 300 KiB |
BIN
assets/uploads/gallery/2/photo_6932937dbb98a.jpg
Normal file
|
After Width: | Height: | Size: 284 KiB |
BIN
assets/uploads/gallery/2/photo_6932937dbbd81.jpg
Normal file
|
After Width: | Height: | Size: 246 KiB |
BIN
assets/uploads/gallery/2/photo_6932937dbc193.jpg
Normal file
|
After Width: | Height: | Size: 384 KiB |
BIN
assets/uploads/gallery/2/photo_6932937dbc77b.jpg
Normal file
|
After Width: | Height: | Size: 775 KiB |
BIN
assets/uploads/gallery/2/photo_693293a12cdad.jpg
Normal file
|
After Width: | Height: | Size: 791 KiB |
BIN
assets/uploads/gallery/2/photo_693293a12d4d1.jpg
Normal file
|
After Width: | Height: | Size: 205 KiB |
BIN
assets/uploads/gallery/2/photo_693293a12d83b.jpg
Normal file
|
After Width: | Height: | Size: 219 KiB |
BIN
assets/uploads/gallery/2/photo_693293a12db4b.jpg
Normal file
|
After Width: | Height: | Size: 125 KiB |
BIN
assets/uploads/gallery/2/photo_693293a12dde9.jpg
Normal file
|
After Width: | Height: | Size: 175 KiB |
BIN
assets/uploads/gallery/2/photo_693293a12e0c2.jpg
Normal file
|
After Width: | Height: | Size: 62 KiB |
BIN
assets/uploads/gallery/2/photo_693293a12e34b.jpg
Normal file
|
After Width: | Height: | Size: 134 KiB |
BIN
assets/uploads/gallery/2/photo_693293a12e648.jpg
Normal file
|
After Width: | Height: | Size: 138 KiB |
BIN
assets/uploads/gallery/2/photo_693293a12e94f.jpg
Normal file
|
After Width: | Height: | Size: 212 KiB |
BIN
assets/uploads/gallery/2/photo_693293a12efee.jpg
Normal file
|
After Width: | Height: | Size: 217 KiB |
BIN
assets/uploads/gallery/2/photo_693293a12f85d.jpg
Normal file
|
After Width: | Height: | Size: 167 KiB |
BIN
assets/uploads/gallery/2/photo_693293a12fff8.jpg
Normal file
|
After Width: | Height: | Size: 229 KiB |
BIN
assets/uploads/gallery/2/photo_693293a130855.jpg
Normal file
|
After Width: | Height: | Size: 244 KiB |
BIN
assets/uploads/gallery/2/photo_693293a131266.jpg
Normal file
|
After Width: | Height: | Size: 413 KiB |
BIN
assets/uploads/gallery/2/photo_693293a131d52.jpg
Normal file
|
After Width: | Height: | Size: 219 KiB |
BIN
assets/uploads/gallery/2/photo_693293a1322e8.jpg
Normal file
|
After Width: | Height: | Size: 113 KiB |
BIN
assets/uploads/gallery/2/photo_693293a13282f.jpg
Normal file
|
After Width: | Height: | Size: 337 KiB |
BIN
assets/uploads/gallery/2/photo_693293a132fe2.jpg
Normal file
|
After Width: | Height: | Size: 317 KiB |
BIN
assets/uploads/gallery/2/photo_693293a133667.jpg
Normal file
|
After Width: | Height: | Size: 158 KiB |
BIN
assets/uploads/gallery/2/photo_693293a133bd0.jpg
Normal file
|
After Width: | Height: | Size: 264 KiB |
176
docs/EVENTS_ADMIN_SYSTEM.md
Normal 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
|
||||||
86
docs/MEMBERSHIP_DUPLICATE_PREVENTION.md
Normal 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.
|
||||||
494
docs/PHOTO_GALLERY_IMPLEMENTATION.md
Normal file
@@ -0,0 +1,494 @@
|
|||||||
|
# Photo Gallery Feature - Complete Implementation
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
The Photo Gallery feature allows 4WDCSA members to create, manage, and view photo albums with a carousel interface for browsing and a lightbox viewer for detailed photo viewing.
|
||||||
|
|
||||||
|
## Database Schema
|
||||||
|
|
||||||
|
### photo_albums table
|
||||||
|
```sql
|
||||||
|
- album_id (INT, PK, AUTO_INCREMENT)
|
||||||
|
- user_id (INT, FK to users)
|
||||||
|
- title (VARCHAR 255, NOT NULL)
|
||||||
|
- description (TEXT, nullable)
|
||||||
|
- cover_image (VARCHAR 500, nullable - stores file path)
|
||||||
|
- created_at (TIMESTAMP)
|
||||||
|
- updated_at (TIMESTAMP)
|
||||||
|
- UNIQUE INDEX on user_id (one album per user for now, can be modified)
|
||||||
|
- INDEX on created_at for sorting
|
||||||
|
```
|
||||||
|
|
||||||
|
### photos table
|
||||||
|
```sql
|
||||||
|
- photo_id (INT, PK, AUTO_INCREMENT)
|
||||||
|
- album_id (INT, FK to photo_albums, CASCADE DELETE)
|
||||||
|
- file_path (VARCHAR 500, NOT NULL)
|
||||||
|
- caption (VARCHAR 500, nullable)
|
||||||
|
- display_order (INT, default 0)
|
||||||
|
- created_at (TIMESTAMP)
|
||||||
|
- INDEX on album_id for quick lookups
|
||||||
|
- INDEX on display_order for sorting
|
||||||
|
```
|
||||||
|
|
||||||
|
## File Structure
|
||||||
|
|
||||||
|
### Pages (Public-Facing)
|
||||||
|
- `src/pages/gallery/gallery.php` - Main carousel view of all albums
|
||||||
|
- `src/pages/gallery/view_album.php` - Detailed album view with photo grid and lightbox
|
||||||
|
- `src/pages/gallery/create_album.php` - Form to create new albums and upload initial photos
|
||||||
|
|
||||||
|
### Processors (Backend Logic)
|
||||||
|
- `src/processors/save_album.php` - Creates new album and handles initial photo uploads
|
||||||
|
- `src/processors/update_album.php` - Updates album metadata and handles additional photo uploads
|
||||||
|
- `src/processors/delete_album.php` - Deletes entire album with all photos and files
|
||||||
|
- `src/processors/delete_photo.php` - Deletes individual photos from album
|
||||||
|
- `src/processors/get_album_photos.php` - API endpoint returning album photos as JSON
|
||||||
|
|
||||||
|
### Styling
|
||||||
|
All styling is embedded in each PHP file using `<style>` tags for consistency with existing pattern.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
### Gallery View (gallery.php)
|
||||||
|
**Purpose**: Display all photo albums in a carousel format
|
||||||
|
|
||||||
|
**Features**:
|
||||||
|
- Bootstrap carousel with Previous/Next buttons
|
||||||
|
- Album cards showing:
|
||||||
|
- Cover image
|
||||||
|
- Album title
|
||||||
|
- Description
|
||||||
|
- Creator avatar and name
|
||||||
|
- Photo count
|
||||||
|
- "View Album" button
|
||||||
|
- "Create Album" button (visible to all members)
|
||||||
|
- Empty state message for members with no albums
|
||||||
|
- Responsive design for mobile/tablet/desktop
|
||||||
|
|
||||||
|
**Access Control**:
|
||||||
|
- Members-only (redirects non-members to membership page)
|
||||||
|
- Verified membership required
|
||||||
|
|
||||||
|
### Album Detail View (view_album.php)
|
||||||
|
**Purpose**: Display all photos from a single album with lightbox viewer
|
||||||
|
|
||||||
|
**Features**:
|
||||||
|
- Album header with:
|
||||||
|
- Creator information (avatar, name)
|
||||||
|
- Album title and description
|
||||||
|
- Photo count
|
||||||
|
- "Edit Album" button (visible only to album owner)
|
||||||
|
- Responsive photo grid layout
|
||||||
|
- Click any photo to open lightbox viewer
|
||||||
|
- Lightbox features:
|
||||||
|
- Full-screen image display
|
||||||
|
- Previous/Next navigation buttons
|
||||||
|
- Caption display
|
||||||
|
- Keyboard navigation:
|
||||||
|
- Arrow Left: Previous photo
|
||||||
|
- Arrow Right: Next photo
|
||||||
|
- Escape: Close lightbox
|
||||||
|
- Close button (X)
|
||||||
|
- Empty state message with "Add Photos" link for album owner
|
||||||
|
- "Back to Gallery" button at bottom
|
||||||
|
|
||||||
|
**Access Control**:
|
||||||
|
- Public albums visible to all members
|
||||||
|
- Edit button visible only to album owner
|
||||||
|
|
||||||
|
### Create/Edit Album (create_album.php)
|
||||||
|
**Purpose**: Create new albums or edit existing albums with photo uploads
|
||||||
|
|
||||||
|
**Features**:
|
||||||
|
- Album title input (required, validates with validateName())
|
||||||
|
- Description textarea (optional, max 500 characters)
|
||||||
|
- Drag-and-drop file upload area
|
||||||
|
- File selection click-to-upload
|
||||||
|
- Selected files list showing filename and size
|
||||||
|
- Photo grid showing existing photos (edit mode only)
|
||||||
|
- Delete button on each existing photo
|
||||||
|
- Delete album button (edit mode only)
|
||||||
|
- Submit and Cancel buttons
|
||||||
|
|
||||||
|
**File Upload Validation**:
|
||||||
|
- Allowed formats: JPG, PNG, GIF, WEBP
|
||||||
|
- Max file size: 5MB per image
|
||||||
|
- Validates MIME type and file size
|
||||||
|
- Generates unique filenames with uniqid()
|
||||||
|
- Stores in `/assets/uploads/gallery/{album_id}/`
|
||||||
|
|
||||||
|
**Form Behavior**:
|
||||||
|
- Create mode: Only allows setting album metadata and initial photos
|
||||||
|
- Edit mode: Shows existing photos, allows adding new photos, allows editing metadata
|
||||||
|
- First uploaded photo becomes cover image (auto-selected)
|
||||||
|
- Photos can be deleted before submission (edit mode)
|
||||||
|
- Form prepopulation on edit (title, description, existing photos)
|
||||||
|
|
||||||
|
**Access Control**:
|
||||||
|
- Members-only
|
||||||
|
- Edit form checks album ownership before allowing edits
|
||||||
|
- Redirect to gallery if not owner of album being edited
|
||||||
|
|
||||||
|
## API Endpoints
|
||||||
|
|
||||||
|
### GET /get_album_photos
|
||||||
|
Returns JSON array of photos for an album
|
||||||
|
|
||||||
|
**Parameters**:
|
||||||
|
- `id`: Album ID (GET parameter)
|
||||||
|
|
||||||
|
**Response**:
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"photo_id": 1,
|
||||||
|
"file_path": "/assets/uploads/gallery/1/photo_abc123.jpg",
|
||||||
|
"caption": "Sample photo",
|
||||||
|
"display_order": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Access Control**: Members-only, owner of album only
|
||||||
|
|
||||||
|
### POST /delete_photo
|
||||||
|
Deletes a photo and updates album cover if needed
|
||||||
|
|
||||||
|
**Parameters**:
|
||||||
|
- `photo_id`: Photo ID (POST)
|
||||||
|
- `csrf_token`: CSRF token (POST)
|
||||||
|
|
||||||
|
**Response**:
|
||||||
|
```json
|
||||||
|
{ "success": true }
|
||||||
|
```
|
||||||
|
|
||||||
|
**Side Effects**:
|
||||||
|
- Deletes photo file from disk
|
||||||
|
- Removes photo from database
|
||||||
|
- Updates album cover image if deleted photo was cover
|
||||||
|
- Sets cover to first remaining photo or NULL if no photos left
|
||||||
|
|
||||||
|
**Access Control**: Members-only, owner of album only
|
||||||
|
|
||||||
|
## Workflow Examples
|
||||||
|
|
||||||
|
### Creating an Album
|
||||||
|
1. User clicks "Create Album" button on gallery page
|
||||||
|
2. Navigates to create_album.php
|
||||||
|
3. Enters album title (required) and description (optional)
|
||||||
|
4. Drags and drops or selects multiple photo files
|
||||||
|
5. Clicks "Create Album" button
|
||||||
|
6. save_album.php:
|
||||||
|
- Creates album directory: `/assets/uploads/gallery/{album_id}/`
|
||||||
|
- Validates each photo (mime type, file size)
|
||||||
|
- Moves photos to album directory with unique names
|
||||||
|
- Sets first photo as cover_image
|
||||||
|
- Inserts album record and photo records in transaction
|
||||||
|
- Redirects to view_album page for newly created album
|
||||||
|
|
||||||
|
### Editing an Album
|
||||||
|
1. User clicks "Edit Album" button on album view page
|
||||||
|
2. Navigates to create_album.php?id={album_id}
|
||||||
|
3. Form prepopulates with current album data
|
||||||
|
4. Existing photos displayed in grid with delete buttons
|
||||||
|
5. Can add more photos by uploading new files
|
||||||
|
6. Clicks "Update Album" button
|
||||||
|
7. update_album.php:
|
||||||
|
- Verifies ownership
|
||||||
|
- Updates album metadata
|
||||||
|
- Validates and uploads any new photos
|
||||||
|
- Appends new photos to existing ones (doesn't overwrite)
|
||||||
|
- Redirects back to view_album
|
||||||
|
|
||||||
|
### Deleting a Photo
|
||||||
|
1. User clicks delete (X) button on photo in edit form
|
||||||
|
2. JavaScript shows confirmation dialog
|
||||||
|
3. Sends POST to delete_photo with photo_id and csrf_token
|
||||||
|
4. delete_photo.php:
|
||||||
|
- Verifies ownership through album
|
||||||
|
- Deletes file from disk
|
||||||
|
- Removes from database
|
||||||
|
- Updates cover_image if needed
|
||||||
|
- Returns JSON success response
|
||||||
|
5. Page reloads to show updated photo list
|
||||||
|
|
||||||
|
### Deleting an Album
|
||||||
|
1. User clicks "Delete Album" button in edit form
|
||||||
|
2. JavaScript shows confirmation dialog
|
||||||
|
3. Navigates to delete_album.php?id={album_id}
|
||||||
|
4. delete_album.php:
|
||||||
|
- Verifies ownership
|
||||||
|
- Deletes all photo files from disk
|
||||||
|
- Deletes all photo records from database
|
||||||
|
- Deletes album directory
|
||||||
|
- Deletes album record
|
||||||
|
- Redirects to gallery page
|
||||||
|
|
||||||
|
## URL Routing (.htaccess)
|
||||||
|
|
||||||
|
```
|
||||||
|
/gallery → src/pages/gallery/gallery.php
|
||||||
|
/create_album → src/pages/gallery/create_album.php
|
||||||
|
/edit_album → src/pages/gallery/create_album.php (id parameter determines mode)
|
||||||
|
/view_album → src/pages/gallery/view_album.php
|
||||||
|
/save_album → src/processors/save_album.php (POST only)
|
||||||
|
/update_album → src/processors/update_album.php (POST only)
|
||||||
|
/delete_album → src/processors/delete_album.php (GET with id parameter)
|
||||||
|
/delete_photo → src/processors/delete_photo.php (POST only)
|
||||||
|
/get_album_photos → src/processors/get_album_photos.php (GET with id parameter)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security Features
|
||||||
|
|
||||||
|
### Authentication
|
||||||
|
- All pages check for `$_SESSION['user_id']`
|
||||||
|
- Non-members redirected to membership page
|
||||||
|
- Non-authenticated users redirected to login
|
||||||
|
|
||||||
|
### Authorization
|
||||||
|
- Album ownership verified before allowing edits
|
||||||
|
- Album ownership verified before allowing deletes
|
||||||
|
- Only album owner can edit or delete photos
|
||||||
|
- Only album owner can see edit buttons
|
||||||
|
|
||||||
|
### Data Validation
|
||||||
|
- Album title validated with validateName() function
|
||||||
|
- Description length limited to 500 characters
|
||||||
|
- File uploads validated for:
|
||||||
|
- MIME type (only image formats allowed)
|
||||||
|
- File size (max 5MB)
|
||||||
|
- File extension check
|
||||||
|
- Filename sanitized with uniqid() to prevent conflicts
|
||||||
|
|
||||||
|
### CSRF Protection
|
||||||
|
- All forms include csrf_token
|
||||||
|
- Processors validate CSRF token before processing
|
||||||
|
- POST-only operations protected
|
||||||
|
|
||||||
|
### Transaction Safety
|
||||||
|
- Album creation uses transaction
|
||||||
|
- Creates directory
|
||||||
|
- Inserts album record
|
||||||
|
- Inserts photo records
|
||||||
|
- Commits all or rolls back all on error
|
||||||
|
- Handles cleanup on failure:
|
||||||
|
- Deletes partial uploads
|
||||||
|
- Removes album directory
|
||||||
|
- Removes album record from database
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
### Validation Errors
|
||||||
|
- File too large: "File too large: {filename}"
|
||||||
|
- Invalid file type: "Invalid file type: {filename}"
|
||||||
|
- Missing album title: "Album title is required and must be valid"
|
||||||
|
- Description too long: "Description must be 500 characters or less"
|
||||||
|
|
||||||
|
### Permission Errors
|
||||||
|
- Not authenticated: Redirects to login
|
||||||
|
- Not a member: Redirects to membership page
|
||||||
|
- Not album owner: Returns 403 Forbidden with error message
|
||||||
|
- Album not found: Returns 404 with redirect to gallery
|
||||||
|
|
||||||
|
### Upload Errors
|
||||||
|
- Directory creation failure: "Failed to create album directory"
|
||||||
|
- File move failure: "Failed to upload: {filename}"
|
||||||
|
- Database insert failure: HTTP 400 with error message
|
||||||
|
|
||||||
|
### Recovery
|
||||||
|
- All upload errors trigger transaction rollback
|
||||||
|
- Partial files cleaned up on failure
|
||||||
|
- Album record deleted if transaction fails
|
||||||
|
- Directory removed if transaction fails
|
||||||
|
|
||||||
|
## Testing Checklist
|
||||||
|
|
||||||
|
### Album Creation
|
||||||
|
- [ ] Create album with title only
|
||||||
|
- [ ] Create album with title and description
|
||||||
|
- [ ] Upload single photo to new album
|
||||||
|
- [ ] Upload multiple photos to new album
|
||||||
|
- [ ] Verify first photo becomes cover
|
||||||
|
- [ ] Verify files stored in correct directory
|
||||||
|
- [ ] Verify album appears in carousel
|
||||||
|
|
||||||
|
### Album Editing
|
||||||
|
- [ ] Edit album title
|
||||||
|
- [ ] Edit album description
|
||||||
|
- [ ] Add photos to existing album
|
||||||
|
- [ ] Add many photos at once (10+)
|
||||||
|
- [ ] Delete photos from album
|
||||||
|
- [ ] Delete last photo (cover updates to NULL)
|
||||||
|
- [ ] Delete album cover, verify new cover assigned
|
||||||
|
- [ ] Verify edit unavailable for non-owner
|
||||||
|
|
||||||
|
### Album Viewing
|
||||||
|
- [ ] View album as owner
|
||||||
|
- [ ] View album as other member
|
||||||
|
- [ ] View album with many photos
|
||||||
|
- [ ] Photo grid responsive on mobile
|
||||||
|
- [ ] Photo grid responsive on tablet
|
||||||
|
- [ ] All photos display correct captions
|
||||||
|
|
||||||
|
### Lightbox
|
||||||
|
- [ ] Open lightbox from first photo
|
||||||
|
- [ ] Open lightbox from middle photo
|
||||||
|
- [ ] Open lightbox from last photo
|
||||||
|
- [ ] Next button navigates forward
|
||||||
|
- [ ] Previous button navigates backward
|
||||||
|
- [ ] Next button wraps to first photo from last
|
||||||
|
- [ ] Previous button wraps to last photo from first
|
||||||
|
- [ ] Arrow key navigation works
|
||||||
|
- [ ] Escape key closes lightbox
|
||||||
|
- [ ] Click X button closes lightbox
|
||||||
|
- [ ] Photo caption displays correctly
|
||||||
|
|
||||||
|
### Gallery Page
|
||||||
|
- [ ] Carousel displays all albums
|
||||||
|
- [ ] Previous/Next buttons work
|
||||||
|
- [ ] Album cards show cover image
|
||||||
|
- [ ] Album cards show correct photo count
|
||||||
|
- [ ] Create Album button visible
|
||||||
|
- [ ] Create Album button navigates correctly
|
||||||
|
- [ ] Edit button visible only to owner
|
||||||
|
- [ ] Empty gallery state shows correct message
|
||||||
|
- [ ] Empty gallery has Create Album link
|
||||||
|
|
||||||
|
### Access Control
|
||||||
|
- [ ] Non-members cannot access gallery
|
||||||
|
- [ ] Non-members cannot create albums
|
||||||
|
- [ ] Non-members cannot edit albums
|
||||||
|
- [ ] Users cannot edit others' albums
|
||||||
|
- [ ] Users cannot delete others' albums
|
||||||
|
- [ ] Users cannot delete others' photos
|
||||||
|
|
||||||
|
### File Uploads
|
||||||
|
- [ ] JPG files accepted
|
||||||
|
- [ ] PNG files accepted
|
||||||
|
- [ ] GIF files accepted
|
||||||
|
- [ ] WEBP files accepted
|
||||||
|
- [ ] BMP files rejected
|
||||||
|
- [ ] ZIP files rejected
|
||||||
|
- [ ] Files over 5MB rejected
|
||||||
|
- [ ] Files exactly 5MB accepted
|
||||||
|
- [ ] Drag and drop upload works
|
||||||
|
- [ ] Click-to-upload works
|
||||||
|
|
||||||
|
### Database
|
||||||
|
- [ ] Albums table has correct structure
|
||||||
|
- [ ] Photos table has correct structure
|
||||||
|
- [ ] Foreign keys work correctly
|
||||||
|
- [ ] Cascade delete removes photos when album deleted
|
||||||
|
- [ ] Unique constraint prevents duplicate user ownership
|
||||||
|
- [ ] Indexes created for performance
|
||||||
|
|
||||||
|
### Navigation
|
||||||
|
- [ ] Gallery link appears in Members Area menu
|
||||||
|
- [ ] Gallery link visible only for logged-in users
|
||||||
|
- [ ] Gallery link locked (with icon) for non-members
|
||||||
|
- [ ] "View Album" button navigates to album detail
|
||||||
|
- [ ] "Edit Album" button navigates to edit form
|
||||||
|
- [ ] "Back to Gallery" button returns to gallery
|
||||||
|
- [ ] "Add Photos" link in empty album goes to edit form
|
||||||
|
|
||||||
|
## Future Enhancements
|
||||||
|
|
||||||
|
### Possible Features
|
||||||
|
1. Multiple albums per user (modify UNIQUE constraint)
|
||||||
|
2. Album visibility settings (private/members-only/public)
|
||||||
|
3. Album categories/tags
|
||||||
|
4. Photo ordering/reordering in album
|
||||||
|
5. Photo batch operations (delete multiple, move between albums)
|
||||||
|
6. Album sharing/collaboration
|
||||||
|
7. Photo comments/ratings
|
||||||
|
8. Admin gallery management
|
||||||
|
9. Automatic image optimization/compression
|
||||||
|
10. Photo metadata preservation (EXIF)
|
||||||
|
11. Album archives/export
|
||||||
|
12. Photo search across all albums
|
||||||
|
|
||||||
|
### Schema Changes Required
|
||||||
|
- Remove UNIQUE constraint on user_id to allow multiple albums per user
|
||||||
|
- Add visibility enum field to photo_albums
|
||||||
|
- Add category_id FK to photo_albums
|
||||||
|
- Add user_id to photos for future permission model
|
||||||
|
- Add updated_at timestamps to photos for tracking
|
||||||
|
|
||||||
|
## Deployment Notes
|
||||||
|
|
||||||
|
### Pre-Deployment
|
||||||
|
1. Run migration 003 to create tables
|
||||||
|
2. Create `/assets/uploads/gallery/` directory with proper permissions
|
||||||
|
3. Ensure PHP can write to upload directory (755 or 777)
|
||||||
|
4. Test file upload with valid and invalid files
|
||||||
|
|
||||||
|
### Post-Deployment
|
||||||
|
1. Verify gallery link appears in header menu
|
||||||
|
2. Test creating first album in production
|
||||||
|
3. Test file uploads with various image formats
|
||||||
|
4. Monitor disk space usage for uploads
|
||||||
|
5. Set up automated cleanup for orphaned files (if needed)
|
||||||
|
|
||||||
|
### Permissions
|
||||||
|
```
|
||||||
|
/assets/uploads/gallery/
|
||||||
|
Permissions: 755 (rwxr-xr-x)
|
||||||
|
Owner: web server user
|
||||||
|
|
||||||
|
Individual album directories:
|
||||||
|
Permissions: 755 (rwxr-xr-x)
|
||||||
|
Created automatically by application
|
||||||
|
|
||||||
|
Photo files:
|
||||||
|
Permissions: 644 (rw-r--r--)
|
||||||
|
Created automatically by application
|
||||||
|
```
|
||||||
|
|
||||||
|
### Backups
|
||||||
|
- Include `/assets/uploads/gallery/` in backup routine
|
||||||
|
- Include `photo_albums` and `photos` tables in database backups
|
||||||
|
- Consider separate backup for large image files
|
||||||
|
|
||||||
|
## Known Limitations
|
||||||
|
|
||||||
|
1. **One album per user**: Current schema design with UNIQUE constraint on user_id allows only one album per user. Can be modified if multiple albums per user needed.
|
||||||
|
|
||||||
|
2. **No album visibility control**: All member albums are visible to all members. Could add privacy settings in future.
|
||||||
|
|
||||||
|
3. **No photo ordering UI**: Photos ordered by display_order but no UI to reorder them. Captions show filename by default.
|
||||||
|
|
||||||
|
4. **No album categories**: All albums mixed in one carousel. Could add filtering/categories.
|
||||||
|
|
||||||
|
5. **Image optimization**: No automatic compression/optimization. Large images stored as-is.
|
||||||
|
|
||||||
|
6. **No EXIF data**: Photo metadata stripped during upload. Could preserve orientation/metadata.
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Photos not uploading
|
||||||
|
- Check `/assets/uploads/gallery/` exists and is writable
|
||||||
|
- Verify file sizes under 5MB
|
||||||
|
- Confirm image MIME types are jpeg, png, gif, or webp
|
||||||
|
- Check PHP error logs for upload errors
|
||||||
|
|
||||||
|
### Album cover not updating
|
||||||
|
- Verify cover_image field in database
|
||||||
|
- Check if photo file path stored correctly
|
||||||
|
- Confirm image file exists on disk
|
||||||
|
|
||||||
|
### Lightbox not opening
|
||||||
|
- Check browser console for JavaScript errors
|
||||||
|
- Verify image paths are accessible
|
||||||
|
- Confirm file URLs accessible directly
|
||||||
|
|
||||||
|
### Permission denied errors
|
||||||
|
- Check album owner verification logic
|
||||||
|
- Verify CSRF tokens being passed correctly
|
||||||
|
- Confirm user_id matches in session
|
||||||
|
|
||||||
|
### Memory issues with large uploads
|
||||||
|
- Reduce PHP memory_limit if needed
|
||||||
|
- Split large batches of photos into smaller uploads
|
||||||
|
- Consider image optimization/compression
|
||||||
|
|
||||||
14
docs/migrations/001_add_events_tracking_columns.sql
Normal 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`);
|
||||||
37
docs/migrations/002_add_unique_constraints_membership.sql
Normal 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);
|
||||||
|
|
||||||
30
docs/migrations/003_create_photo_gallery_tables.sql
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
-- Migration: Create photo gallery tables for member photo albums
|
||||||
|
-- Date: 2025-12-05
|
||||||
|
-- Purpose: Allow members to create albums and upload photos to share with the community
|
||||||
|
|
||||||
|
-- Create photo_albums table
|
||||||
|
CREATE TABLE IF NOT EXISTS photo_albums (
|
||||||
|
album_id INT PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
user_id INT NOT NULL,
|
||||||
|
title VARCHAR(255) NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
cover_image VARCHAR(255),
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE CASCADE,
|
||||||
|
INDEX idx_user_id (user_id),
|
||||||
|
INDEX idx_created_at (created_at)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||||
|
|
||||||
|
-- Create photos table
|
||||||
|
CREATE TABLE IF NOT EXISTS photos (
|
||||||
|
photo_id INT PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
album_id INT NOT NULL,
|
||||||
|
file_path VARCHAR(255) NOT NULL,
|
||||||
|
caption VARCHAR(255),
|
||||||
|
display_order INT DEFAULT 0,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (album_id) REFERENCES photo_albums(album_id) ON DELETE CASCADE,
|
||||||
|
INDEX idx_album_id (album_id),
|
||||||
|
INDEX idx_display_order (display_order)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||||
17
header.php
@@ -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,23 @@ 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>";
|
||||||
|
echo "<li><a href=\"gallery\">Photo Gallery</a></li>";
|
||||||
|
} else {
|
||||||
|
echo "<li><a href=\"membership\">Campsites Directory</a><i class='fal fa-lock'></i></li>";
|
||||||
|
echo "<li><a href=\"membership\">Photo Gallery</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
@@ -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'); ?>
|
||||||
46
src/admin/delete_event.php
Normal 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
@@ -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
@@ -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()]);
|
||||||
|
}
|
||||||
75
src/admin/toggle_event_published.php
Normal 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()]);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,14 +29,72 @@ 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 */
|
/* Form styling to match manage_trips */
|
||||||
@@ -159,7 +229,7 @@ while ($row = $result->fetch_assoc()) {
|
|||||||
</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');
|
||||||
?>
|
?>
|
||||||
@@ -170,11 +240,29 @@ require_once($rootPath . '/components/banner.php');
|
|||||||
<div class="col-lg-12">
|
<div class="col-lg-12">
|
||||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
|
||||||
<h3>Campsites Map</h3>
|
<h3>Campsites Map</h3>
|
||||||
<button class="theme-btn" id="toggleFormBtn" onclick="toggleCampsiteForm()">
|
<button class="theme-btn" id="toggleFormBtn" onclick="startLocationMode()">
|
||||||
<i class="far fa-plus"></i> Add Campsite
|
<i class="far fa-plus"></i> Add Campsite
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<p style="color: #666; margin-bottom: 15px;">Click on the map to add a new campsite, or click on a marker to view details.</p>
|
<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 -->
|
<!-- Collapsible Campsite Form -->
|
||||||
<div class="campsite-form-container" id="campsiteFormContainer">
|
<div class="campsite-form-container" id="campsiteFormContainer">
|
||||||
@@ -270,8 +358,6 @@ require_once($rootPath . '/components/banner.php');
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="map" style="width: 100%; height: 500px;"></div>
|
|
||||||
|
|
||||||
<!-- Campsites Table -->
|
<!-- Campsites Table -->
|
||||||
<div style="margin-top: 40px;">
|
<div style="margin-top: 40px;">
|
||||||
<h4 style="margin-bottom: 20px;">All Campsites</h4>
|
<h4 style="margin-bottom: 20px;">All Campsites</h4>
|
||||||
@@ -282,7 +368,7 @@ require_once($rootPath . '/components/banner.php');
|
|||||||
<tr>
|
<tr>
|
||||||
<th>Name</th>
|
<th>Name</th>
|
||||||
<th>Description</th>
|
<th>Description</th>
|
||||||
<th>Website</th>
|
<th>Booking Website</th>
|
||||||
<th>Phone</th>
|
<th>Phone</th>
|
||||||
<th>Added By</th>
|
<th>Added By</th>
|
||||||
<th>Actions</th>
|
<th>Actions</th>
|
||||||
@@ -302,9 +388,113 @@ require_once($rootPath . '/components/banner.php');
|
|||||||
|
|
||||||
<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() {
|
function toggleCampsiteForm() {
|
||||||
|
if (isLocationMode) return;
|
||||||
|
|
||||||
const container = document.getElementById("campsiteFormContainer");
|
const container = document.getElementById("campsiteFormContainer");
|
||||||
container.style.display = container.style.display === "none" ? "block" : "none";
|
container.style.display = container.style.display === "none" ? "block" : "none";
|
||||||
if (container.style.display === "block") {
|
if (container.style.display === "block") {
|
||||||
@@ -312,14 +502,40 @@ require_once($rootPath . '/components/banner.php');
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetForm() {
|
function resetFormForNewCampsite() {
|
||||||
// Clear the form
|
// This is called when confirming location for a NEW campsite
|
||||||
document.getElementById("addCampsiteForm").reset();
|
// 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
|
// Remove the ID input if it exists
|
||||||
let idInput = document.querySelector("#addCampsiteForm input[name='id']");
|
let idInput = document.querySelector("#addCampsiteForm input[name='id']");
|
||||||
if (idInput) {
|
if (idInput) {
|
||||||
idInput.remove();
|
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() {
|
||||||
@@ -327,25 +543,10 @@ require_once($rootPath . '/components/banner.php');
|
|||||||
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();
|
|
||||||
|
|
||||||
resetForm();
|
|
||||||
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);
|
|
||||||
|
|
||||||
// Show the form container
|
|
||||||
document.getElementById("campsiteFormContainer").style.display = "block";
|
|
||||||
document.getElementById("campsiteFormContainer").scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
|
||||||
});
|
|
||||||
|
|
||||||
// Load existing campsites from PHP
|
// Load existing campsites from PHP
|
||||||
fetch("get_campsites")
|
fetch("get_campsites")
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
@@ -366,7 +567,7 @@ require_once($rootPath . '/components/banner.php');
|
|||||||
${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;">
|
||||||
@@ -451,6 +652,11 @@ require_once($rootPath . '/components/banner.php');
|
|||||||
? `${site.user.first_name} ${site.user.last_name}`
|
? `${site.user.first_name} ${site.user.last_name}`
|
||||||
: "Unknown";
|
: "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 = `
|
row.innerHTML = `
|
||||||
<td><strong>${site.name}</strong></td>
|
<td><strong>${site.name}</strong></td>
|
||||||
<td>${site.description ? site.description.substring(0, 50) + (site.description.length > 50 ? '...' : '') : '-'}</td>
|
<td>${site.description ? site.description.substring(0, 50) + (site.description.length > 50 ? '...' : '') : '-'}</td>
|
||||||
@@ -458,7 +664,7 @@ require_once($rootPath . '/components/banner.php');
|
|||||||
<td>${site.telephone || '-'}</td>
|
<td>${site.telephone || '-'}</td>
|
||||||
<td><small>${userName}</small></td>
|
<td><small>${userName}</small></td>
|
||||||
<td>
|
<td>
|
||||||
<button class="btn btn-sm btn-warning" onclick='editCampsite(${JSON.stringify(site)})'>Edit</button>
|
${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>
|
<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>
|
</td>
|
||||||
`;
|
`;
|
||||||
@@ -469,32 +675,89 @@ require_once($rootPath . '/components/banner.php');
|
|||||||
}
|
}
|
||||||
|
|
||||||
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 select[name='country']").value = site.country || '';
|
|
||||||
document.querySelector("#addCampsiteForm select[name='province']").value = site.province || '';
|
|
||||||
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);
|
|
||||||
|
|
||||||
// Add hidden ID input
|
// Pre-fill form with a slight delay to ensure DOM is ready
|
||||||
let idInput = document.querySelector("#addCampsiteForm input[name='id']");
|
setTimeout(() => {
|
||||||
if (!idInput) {
|
document.querySelector("#addCampsiteForm input[name='name']").value = site.name;
|
||||||
idInput = document.createElement("input");
|
document.querySelector("#addCampsiteForm textarea[name='description']").value = site.description || "";
|
||||||
idInput.type = "hidden";
|
document.querySelector("#addCampsiteForm input[name='website']").value = site.website || "";
|
||||||
idInput.name = "id";
|
document.querySelector("#addCampsiteForm input[name='telephone']").value = site.telephone || "";
|
||||||
document.querySelector("#addCampsiteForm").appendChild(idInput);
|
document.querySelector("#addCampsiteForm input[name='latitude']").value = site.latitude;
|
||||||
}
|
document.querySelector("#addCampsiteForm input[name='longitude']").value = site.longitude;
|
||||||
idInput.value = site.id;
|
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
|
||||||
|
let idInput = document.querySelector("#addCampsiteForm input[name='id']");
|
||||||
|
if (!idInput) {
|
||||||
|
idInput = document.createElement("input");
|
||||||
|
idInput.type = "hidden";
|
||||||
|
idInput.name = "id";
|
||||||
|
document.querySelector("#addCampsiteForm").appendChild(idInput);
|
||||||
|
}
|
||||||
|
idInput.value = site.id;
|
||||||
|
}, 0);
|
||||||
|
|
||||||
// Show the form container
|
// Show the form container
|
||||||
document.getElementById("campsiteFormContainer").style.display = "block";
|
document.getElementById("campsiteFormContainer").style.display = "block";
|
||||||
document.getElementById("campsiteFormContainer").scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
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>
|
||||||
|
|||||||