From 2ecf8f04c4969ebc622be36e3b5b1ba3a0a34161 Mon Sep 17 00:00:00 2001 From: Myron Blair Date: Fri, 22 May 2026 13:39:20 +0000 Subject: [PATCH] Add availability calendar, admin portal, and booking backend - db.php: shared config, PDO, SendGrid, package definitions - availability.php: GET endpoint returning booked/blocked dates by month - contact.php: booking handler with DB record, availability check, SendGrid emails - admin/index.php: full admin portal (login, bookings table, status/notes AJAX, block dates) - index.html: interactive availability calendar with click-to-select, wires to /contact.php - .htaccess: block direct access to db.php Co-Authored-By: Claude Sonnet 4.6 --- .htaccess | 4 + admin/index.php | 313 +++++++++++++++++++++++++++++++++++++++++++++++ availability.php | 46 +++++++ contact.php | 185 ++++++++++++---------------- db.php | 56 +++++++++ index.html | 172 ++++++++++++++++++++++++-- 6 files changed, 662 insertions(+), 114 deletions(-) create mode 100644 .htaccess create mode 100644 admin/index.php create mode 100644 availability.php create mode 100644 db.php diff --git a/.htaccess b/.htaccess new file mode 100644 index 0000000..4801c0e --- /dev/null +++ b/.htaccess @@ -0,0 +1,4 @@ + + Order deny,allow + Deny from all + diff --git a/admin/index.php b/admin/index.php new file mode 100644 index 0000000..d8b05e4 --- /dev/null +++ b/admin/index.php @@ -0,0 +1,313 @@ +prepare("UPDATE bookings SET status=? WHERE id=?")->execute([$status, $id]); + echo json_encode(['ok'=>true]); + } else { + echo json_encode(['error'=>'Invalid']); + } + exit; + } + if ($action === 'save_admin_notes') { + $id = (int)($_POST['id'] ?? 0); + $notes = substr(trim($_POST['notes'] ?? ''), 0, 1000); + db()->prepare("UPDATE bookings SET admin_notes=? WHERE id=?")->execute([$notes, $id]); + echo json_encode(['ok'=>true]); + exit; + } + if ($action === 'block_date') { + $date = $_POST['date'] ?? ''; + $reason = substr($_POST['reason'] ?? '', 0, 200); + if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $date)) { + db()->prepare("INSERT IGNORE INTO blocked_dates (block_date, reason) VALUES (?,?)")->execute([$date, $reason]); + echo json_encode(['ok'=>true]); + } else { echo json_encode(['error'=>'Invalid date']); } + exit; + } + if ($action === 'unblock_date') { + $id = (int)($_POST['id'] ?? 0); + db()->prepare("DELETE FROM blocked_dates WHERE id=?")->execute([$id]); + echo json_encode(['ok'=>true]); + exit; + } + exit; +} + +// ── Login page ──────────────────────────────────────────────────────────────── +if (!$authed) { ?> + + + + +Admin Login — Parker County Slingshot Rentals + + + +
+

Parker Admin

+

Slingshot Rentals Management

+
+ + + + + + +
+
+ + +quote($statusFilter) : ''; +$bookings = db()->query("SELECT * FROM bookings {$where} ORDER BY rental_date ASC, created_at DESC")->fetchAll(); +$blocked = db()->query("SELECT * FROM blocked_dates ORDER BY block_date ASC")->fetchAll(); + +$stats = db()->query(" + SELECT + COUNT(*) AS total, + SUM(status='pending') AS pending, + SUM(status='confirmed') AS confirmed, + SUM(status='completed') AS completed, + SUM(status='cancelled') AS cancelled, + SUM(CASE WHEN status IN ('confirmed','completed') THEN amount ELSE 0 END) AS revenue + FROM bookings +")->fetch(); + +$statusColors = ['pending'=>'#d97706','confirmed'=>'#16a34a','completed'=>'#2563eb','cancelled'=>'#dc2626']; +?> + + + + +Admin — Parker County Slingshot Rentals + + + +
+

Parker County Slingshot — Admin

+ Sign Out +
+
+ + +
+
Total Bookings
+
Pending
+
Confirmed
+
Completed
+
$
Revenue
+
+ + +
+
+

Bookings

+ +
+ +
No bookings found.
+ +
+ + + + + + + + + + + + + + + + + + + + + +
RefCustomerPackageDateAmountStatusAdmin NotesReceived
+
+ +
+
+ $b['package']]; ?> + + + + +
+ +
$ + + + + +
+
+ +
+ + +
+

Block Dates (maintenance / personal use)

+
+
+ + +
+
+ + +
+ +
+
+ +

No dates blocked.

+ + +
+ + + +
+ + +
+
+
+ + + + diff --git a/availability.php b/availability.php new file mode 100644 index 0000000..c8f6515 --- /dev/null +++ b/availability.php @@ -0,0 +1,46 @@ +prepare( + "SELECT rental_date, end_date FROM bookings + WHERE status IN ('pending','confirmed') + AND rental_date <= ? AND end_date >= ?" +); +$booked->execute([$end, $start]); + +$bookedDays = []; +foreach ($booked->fetchAll() as $row) { + $d = new DateTime($row['rental_date']); + $e = new DateTime($row['end_date']); + while ($d <= $e) { + $bookedDays[] = $d->format('Y-m-d'); + $d->modify('+1 day'); + } +} + +// Admin-blocked dates +$blocked = db()->prepare( + "SELECT block_date FROM blocked_dates WHERE block_date BETWEEN ? AND ?" +); +$blocked->execute([$start, $end]); +foreach ($blocked->fetchAll() as $row) { + $bookedDays[] = $row['block_date']; +} + +echo json_encode([ + 'month' => $month, + 'year' => $year, + 'booked_dates' => array_values(array_unique($bookedDays)), +]); diff --git a/contact.php b/contact.php index 98309b7..fb44354 100644 --- a/contact.php +++ b/contact.php @@ -1,29 +1,14 @@ false, 'error' => 'Method not allowed']); - exit; -} +if ($_SERVER['REQUEST_METHOD'] !== 'POST') { http_response_code(405); echo json_encode(['success'=>false,'error'=>'Method not allowed']); exit; } -// ── CONFIG ──────────────────────────────────────────────────────────── -define('SENDGRID_API_KEY', 'SG.YOUR_KEY_HERE'); // <-- replace with your SendGrid API key -define('MAIL_FROM', 'noreply@parkerslingshotrentals.com'); -define('MAIL_FROM_NAME', 'Parker County Slingshot Rentals'); -define('ADMIN_EMAIL', 'info@parkerslingshotrentals.com'); // where booking alerts go -// ───────────────────────────────────────────────────────────────────── - -$input = json_decode(file_get_contents('php://input'), true); -if (!$input) { $input = $_POST; } +$input = json_decode(file_get_contents('php://input'), true) ?: $_POST; $name = trim(strip_tags($input['name'] ?? '')); $email = trim(strip_tags($input['email'] ?? '')); @@ -32,108 +17,98 @@ $package = trim(strip_tags($input['package'] ?? '')); $date = trim(strip_tags($input['date'] ?? '')); $message = trim(strip_tags($input['message'] ?? '')); -// Basic validation if (!$name || !$email || !$package || !$date) { http_response_code(400); - echo json_encode(['success' => false, 'error' => 'Name, email, package, and date are required.']); - exit; + echo json_encode(['success'=>false,'error'=>'Name, email, package, and date are required.']); exit; } if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { http_response_code(400); - echo json_encode(['success' => false, 'error' => 'Invalid email address.']); - exit; + echo json_encode(['success'=>false,'error'=>'Invalid email address.']); exit; +} +if (!isset(PACKAGES[$package])) { + http_response_code(400); + echo json_encode(['success'=>false,'error'=>'Invalid package.']); exit; +} +if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $date) || strtotime($date) < strtotime('today')) { + http_response_code(400); + echo json_encode(['success'=>false,'error'=>'Invalid or past date.']); exit; } -$packages = [ - 'half-day' => 'Half Day (4 hrs) — $99', - 'full-day' => 'Full Day (8 hrs) — $169', - 'weekend' => 'Weekend (48 hrs) — $299', -]; -$packageLabel = $packages[$package] ?? ucfirst($package); -$dateFormatted = date('F j, Y', strtotime($date)); +$pkg = PACKAGES[$package]; +$rentalDate = $date; +$endDate = date('Y-m-d', strtotime($date . ' +' . $pkg['days'] . ' days')); -// ── SEND ADMIN ALERT ────────────────────────────────────────────────── -$adminHtml = ' -
-
-

New Booking Request!

-

Parker County Slingshot Rentals

+// Check availability +$conflict = db()->prepare( + "SELECT id FROM bookings + WHERE status IN ('pending','confirmed') + AND rental_date <= ? AND end_date >= ?" +); +$conflict->execute([$endDate, $rentalDate]); +if ($conflict->fetch()) { + echo json_encode(['success'=>false,'error'=>'Sorry, that date is already booked. Please choose another date.']); exit; +} +$blockedCheck = db()->prepare("SELECT id FROM blocked_dates WHERE block_date BETWEEN ? AND ?"); +$blockedCheck->execute([$rentalDate, $endDate]); +if ($blockedCheck->fetch()) { + echo json_encode(['success'=>false,'error'=>'That date is unavailable. Please choose another date.']); exit; +} + +// Create booking +$ref = generateRef(); +$stmt = db()->prepare( + "INSERT INTO bookings (booking_ref, name, email, phone, package, rental_date, end_date, amount, notes) + VALUES (?,?,?,?,?,?,?,?,?)" +); +$stmt->execute([$ref, $name, $email, $phone, $package, $rentalDate, $endDate, $pkg['amount'], $message]); + +$dateLabel = date('F j, Y', strtotime($rentalDate)); +$pkgLabel = $pkg['label']; +$amountLabel = '$' . number_format($pkg['amount'], 2); + +// Admin email +$adminHtml = "
+
+

New Booking Request — {$ref}

-
- - - - - - - - - - - +
+
Name' . htmlspecialchars($name) . '
Email' . htmlspecialchars($email) . '
Phone' . (htmlspecialchars($phone) ?: 'not provided') . '
Package' . htmlspecialchars($packageLabel) . '
Date' . htmlspecialchars($dateFormatted) . '
+ + + + + +
Ref{$ref}
Name" . htmlspecialchars($name) . "
Email" . htmlspecialchars($email) . "
Phone" . htmlspecialchars($phone ?: '—') . "
Package{$pkgLabel} — {$amountLabel}
Date{$dateLabel}
- ' . ($message ? '

' . nl2br(htmlspecialchars($message)) . '

' : '') . ' -

Submitted ' . date('F j, Y \a\t g:i A') . ' CT

+ " . ($message ? "
" . nl2br(htmlspecialchars($message)) . "
" : "") . " +

Submitted " . date('F j, Y g:i A') . " CT

-
'; +
"; -// ── SEND CUSTOMER CONFIRMATION ──────────────────────────────────────── -$confirmHtml = ' -
-
-

Parker County Slingshot Rentals

+// Customer confirmation email +$confirmHtml = "
+
+

Parker County Slingshot Rentals

-
-

Booking Request Received!

-

Hey ' . htmlspecialchars($name) . ', we got your request and will confirm availability within a few hours.

-
-

Your Request Summary

-

Package: ' . htmlspecialchars($packageLabel) . '

-

Requested Date: ' . htmlspecialchars($dateFormatted) . '

+
+

Booking Request Received!

+

Hey " . htmlspecialchars($name) . ", your request is in. We'll confirm availability and reach out within a few hours.

+
+

Booking Reference

+

{$ref}

+

Package: {$pkgLabel}

+

Requested Date: {$dateLabel}

+

Total: {$amountLabel}

-

We\'ll reach out to you at ' . htmlspecialchars($email) . '' . ($phone ? ' or ' . htmlspecialchars($phone) . '' : '') . ' to confirm your ride.

-

Questions? Call or text us at (817) 555-0199.

-

Ride on,
The Parker County Slingshot Team

+

Questions? Call or text (817) 555-0199 or reply to this email.

+

Ride on,
The Parker County Slingshot Team

-
-

© ' . date('Y') . ' Parker County Slingshot Rentals — Weatherford, TX

+
+

© " . date('Y') . " Parker County Slingshot Rentals — Weatherford, TX

-
'; +
"; -function sendgridSend(string $toEmail, string $toName, string $subject, string $html): bool { - $payload = json_encode([ - 'personalizations' => [['to' => [['email' => $toEmail, 'name' => $toName]]]], - 'from' => ['email' => MAIL_FROM, 'name' => MAIL_FROM_NAME], - 'subject' => $subject, - 'content' => [['type' => 'text/html', 'value' => $html]], - ]); +sendEmail(ADMIN_EMAIL, 'Parker Slingshot Admin', "New Booking {$ref}: {$name} — {$pkgLabel} on {$dateLabel}", $adminHtml); +sendEmail($email, $name, "Booking Request {$ref} — Parker County Slingshot Rentals", $confirmHtml); - $ch = curl_init('https://api.sendgrid.com/v3/mail/send'); - curl_setopt_array($ch, [ - CURLOPT_RETURNTRANSFER => true, - CURLOPT_POST => true, - CURLOPT_POSTFIELDS => $payload, - CURLOPT_HTTPHEADER => [ - 'Authorization: Bearer ' . SENDGRID_API_KEY, - 'Content-Type: application/json', - ], - CURLOPT_TIMEOUT => 20, - CURLOPT_SSL_VERIFYPEER => false, - ]); - $response = curl_exec($ch); - $code = curl_getinfo($ch, CURLINFO_HTTP_CODE); - curl_close($ch); - return $code === 202; -} - -$apiKey = SENDGRID_API_KEY; -if ($apiKey && strpos($apiKey, 'YOUR_KEY') === false) { - sendgridSend(ADMIN_EMAIL, 'Parker Slingshot Admin', - "New Booking Request: {$name} — {$packageLabel} on {$dateFormatted}", $adminHtml); - sendgridSend($email, $name, - "Booking Request Confirmed — Parker County Slingshot", $confirmHtml); -} else { - error_log('[Parker Slingshot] SENDGRID_API_KEY not configured'); -} - -echo json_encode(['success' => true, 'message' => 'Booking request received! We\'ll be in touch shortly.']); +echo json_encode(['success'=>true,'ref'=>$ref,'message'=>"Booking request received! Your reference is {$ref}. We'll be in touch shortly."]); diff --git a/db.php b/db.php new file mode 100644 index 0000000..cfddbf3 --- /dev/null +++ b/db.php @@ -0,0 +1,56 @@ + ['label' => 'Half Day (4 hrs)', 'amount' => 99.00, 'days' => 0], + 'full-day' => ['label' => 'Full Day (8 hrs)', 'amount' => 169.00, 'days' => 0], + 'weekend' => ['label' => 'Weekend (48 hrs)', 'amount' => 299.00, 'days' => 1], +]); + +function db(): PDO { + static $pdo; + if (!$pdo) { + $pdo = new PDO( + 'mysql:host=' . PARKER_DB_HOST . ';dbname=' . PARKER_DB_NAME . ';charset=utf8mb4', + PARKER_DB_USER, PARKER_DB_PASS, + [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC] + ); + } + return $pdo; +} + +function generateRef(): string { + return 'PSR-' . strtoupper(substr(uniqid(), -6)); +} + +function sendEmail(string $to, string $toName, string $subject, string $html): bool { + if (!SENDGRID_API_KEY || strpos(SENDGRID_API_KEY, 'YOUR_KEY') !== false) return false; + $payload = json_encode([ + 'personalizations' => [['to' => [['email' => $to, 'name' => $toName]]]], + 'from' => ['email' => MAIL_FROM, 'name' => MAIL_FROM_NAME], + 'subject' => $subject, + 'content' => [['type' => 'text/html', 'value' => $html]], + ]); + $ch = curl_init('https://api.sendgrid.com/v3/mail/send'); + curl_setopt_array($ch, [ + CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, + CURLOPT_POSTFIELDS => $payload, + CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . SENDGRID_API_KEY, 'Content-Type: application/json'], + CURLOPT_TIMEOUT => 15, CURLOPT_SSL_VERIFYPEER => false, + ]); + $code = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_exec($ch); curl_close($ch); + return $code === 202; +} diff --git a/index.html b/index.html index 100224c..9b903a6 100644 --- a/index.html +++ b/index.html @@ -385,6 +385,28 @@ .form-msg.success { background: rgba(34,197,94,0.15); border: 1px solid rgba(34,197,94,0.3); color: #4ade80; display: block; } .form-msg.error { background: rgba(239,68,68,0.15); border: 1px solid rgba(239,68,68,0.3); color: #f87171; display: block; } + /* AVAILABILITY CALENDAR */ + .cal-wrap { background: rgba(255,255,255,0.04); border: 1px solid rgba(255,255,255,0.08); border-radius: 12px; padding: 1.25rem; margin-bottom: 1.25rem; } + .cal-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 1rem; } + .cal-header h3 { font-family: 'Barlow Condensed', sans-serif; font-size: 1.2rem; font-weight: 700; letter-spacing: 0.5px; } + .cal-nav { background: rgba(255,255,255,0.07); border: none; color: white; border-radius: 6px; width: 32px; height: 32px; cursor: pointer; font-size: 1rem; display: flex; align-items: center; justify-content: center; transition: background 0.2s; } + .cal-nav:hover { background: var(--orange); } + .cal-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 3px; } + .cal-day-label { text-align: center; font-size: 0.65rem; font-weight: 600; letter-spacing: 1px; color: rgba(255,255,255,0.35); padding: 0.25rem 0; text-transform: uppercase; } + .cal-day { text-align: center; border-radius: 6px; padding: 0.4rem 0.25rem; font-size: 0.8rem; font-weight: 500; min-height: 32px; display: flex; align-items: center; justify-content: center; transition: background 0.15s, color 0.15s; } + .cal-day.empty { background: transparent; } + .cal-day.past { color: rgba(255,255,255,0.2); cursor: default; } + .cal-day.booked { background: rgba(239,68,68,0.18); color: rgba(239,68,68,0.6); cursor: not-allowed; position: relative; } + .cal-day.booked::after { content: ''; position: absolute; bottom: 3px; left: 50%; transform: translateX(-50%); width: 4px; height: 4px; border-radius: 50%; background: rgba(239,68,68,0.5); } + .cal-day.available { background: rgba(255,255,255,0.05); color: rgba(255,255,255,0.85); cursor: pointer; } + .cal-day.available:hover { background: rgba(249,115,22,0.25); color: var(--orange); } + .cal-day.today { border: 1px solid rgba(249,115,22,0.5); } + .cal-day.selected { background: var(--orange) !important; color: white !important; font-weight: 700; } + .cal-legend { display: flex; gap: 1rem; margin-top: 0.75rem; flex-wrap: wrap; } + .cal-legend-item { display: flex; align-items: center; gap: 0.4rem; font-size: 0.72rem; color: rgba(255,255,255,0.45); } + .cal-legend-dot { width: 10px; height: 10px; border-radius: 3px; flex-shrink: 0; } + .cal-loading { text-align: center; padding: 1.5rem; color: rgba(255,255,255,0.3); font-size: 0.85rem; } + /* FOOTER */ footer { background: #080808; @@ -656,6 +678,23 @@
+ +
+
+ +

Loading…

+ +
+
+
Checking availability…
+
+
+
Available
+
Booked
+
Selected
+
+
+
@@ -694,18 +733,130 @@