mirror of
https://github.com/myronblair/parkerslingshotrentals
synced 2026-06-30 17:50:31 -05:00
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 <noreply@anthropic.com>
This commit is contained in:
+80
-105
@@ -1,29 +1,14 @@
|
||||
<?php
|
||||
/**
|
||||
* Parker County Slingshot Rentals — Booking Request Handler
|
||||
*/
|
||||
require_once __DIR__ . '/db.php';
|
||||
|
||||
header('Content-Type: application/json');
|
||||
header('Access-Control-Allow-Origin: https://parkerslingshotrentals.com');
|
||||
header('Access-Control-Allow-Methods: POST, OPTIONS');
|
||||
header('Access-Control-Allow-Headers: Content-Type');
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(204); exit; }
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
http_response_code(405);
|
||||
echo json_encode(['success' => 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 = '
|
||||
<div style="max-width:600px;margin:0 auto;font-family:Arial,sans-serif;">
|
||||
<div style="background:#f97316;padding:24px;text-align:center;">
|
||||
<h1 style="color:#fff;margin:0;font-size:22px;">New Booking Request!</h1>
|
||||
<p style="color:rgba(255,255,255,.85);margin:4px 0 0;font-size:14px;">Parker County Slingshot Rentals</p>
|
||||
// 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 = "<div style='max-width:600px;margin:0 auto;font-family:Arial,sans-serif'>
|
||||
<div style='background:#f97316;padding:20px;text-align:center'>
|
||||
<h1 style='color:#fff;margin:0;font-size:20px'>New Booking Request — {$ref}</h1>
|
||||
</div>
|
||||
<div style="padding:28px;background:#fff;border:1px solid #e5e7eb;">
|
||||
<table style="width:100%;border-collapse:collapse;font-size:15px;">
|
||||
<tr><td style="padding:10px 0;color:#6b7280;width:100px;">Name</td>
|
||||
<td style="padding:10px 0;font-weight:600;">' . htmlspecialchars($name) . '</td></tr>
|
||||
<tr><td style="padding:10px 0;color:#6b7280;">Email</td>
|
||||
<td style="padding:10px 0;"><a href="mailto:' . htmlspecialchars($email) . '" style="color:#f97316;">' . htmlspecialchars($email) . '</a></td></tr>
|
||||
<tr><td style="padding:10px 0;color:#6b7280;">Phone</td>
|
||||
<td style="padding:10px 0;">' . (htmlspecialchars($phone) ?: '<em style="color:#9ca3af;">not provided</em>') . '</td></tr>
|
||||
<tr><td style="padding:10px 0;color:#6b7280;">Package</td>
|
||||
<td style="padding:10px 0;font-weight:700;color:#f97316;">' . htmlspecialchars($packageLabel) . '</td></tr>
|
||||
<tr><td style="padding:10px 0;color:#6b7280;">Date</td>
|
||||
<td style="padding:10px 0;font-weight:600;">' . htmlspecialchars($dateFormatted) . '</td></tr>
|
||||
<div style='padding:24px;background:#fff;border:1px solid #e5e7eb'>
|
||||
<table style='width:100%;font-size:15px'>
|
||||
<tr><td style='color:#6b7280;padding:8px 0;width:110px'>Ref</td><td style='padding:8px 0;font-weight:700'>{$ref}</td></tr>
|
||||
<tr><td style='color:#6b7280;padding:8px 0'>Name</td><td style='padding:8px 0'>" . htmlspecialchars($name) . "</td></tr>
|
||||
<tr><td style='color:#6b7280;padding:8px 0'>Email</td><td style='padding:8px 0'>" . htmlspecialchars($email) . "</td></tr>
|
||||
<tr><td style='color:#6b7280;padding:8px 0'>Phone</td><td style='padding:8px 0'>" . htmlspecialchars($phone ?: '—') . "</td></tr>
|
||||
<tr><td style='color:#6b7280;padding:8px 0'>Package</td><td style='padding:8px 0;font-weight:700;color:#f97316'>{$pkgLabel} — {$amountLabel}</td></tr>
|
||||
<tr><td style='color:#6b7280;padding:8px 0'>Date</td><td style='padding:8px 0;font-weight:700'>{$dateLabel}</td></tr>
|
||||
</table>
|
||||
' . ($message ? '<div style="margin-top:16px;padding:16px;background:#fff7ed;border-radius:8px;border-left:4px solid #f97316;"><p style="margin:0;font-size:14px;color:#374151;line-height:1.6;">' . nl2br(htmlspecialchars($message)) . '</p></div>' : '') . '
|
||||
<p style="margin-top:20px;font-size:13px;color:#9ca3af;">Submitted ' . date('F j, Y \a\t g:i A') . ' CT</p>
|
||||
" . ($message ? "<div style='margin-top:12px;padding:12px;background:#fff7ed;border-left:4px solid #f97316'>" . nl2br(htmlspecialchars($message)) . "</div>" : "") . "
|
||||
<p style='margin-top:16px;font-size:13px;color:#9ca3af'>Submitted " . date('F j, Y g:i A') . " CT</p>
|
||||
</div>
|
||||
</div>';
|
||||
</div>";
|
||||
|
||||
// ── SEND CUSTOMER CONFIRMATION ────────────────────────────────────────
|
||||
$confirmHtml = '
|
||||
<div style="max-width:600px;margin:0 auto;font-family:Arial,sans-serif;">
|
||||
<div style="background:#0d0d0d;padding:24px;text-align:center;">
|
||||
<h1 style="color:#f97316;margin:0;font-size:22px;">Parker County Slingshot Rentals</h1>
|
||||
// Customer confirmation email
|
||||
$confirmHtml = "<div style='max-width:600px;margin:0 auto;font-family:Arial,sans-serif'>
|
||||
<div style='background:#0d0d0d;padding:24px;text-align:center'>
|
||||
<h1 style='color:#f97316;margin:0;font-size:20px'>Parker County Slingshot Rentals</h1>
|
||||
</div>
|
||||
<div style="padding:32px;background:#fff;">
|
||||
<h2 style="color:#0d0d0d;margin-top:0;">Booking Request Received!</h2>
|
||||
<p style="color:#374151;line-height:1.6;">Hey ' . htmlspecialchars($name) . ', we got your request and will confirm availability within a few hours.</p>
|
||||
<div style="background:#fff7ed;border:1px solid #fed7aa;border-radius:10px;padding:20px;margin:24px 0;">
|
||||
<h3 style="margin-top:0;color:#f97316;font-size:16px;">Your Request Summary</h3>
|
||||
<p style="margin:4px 0;font-size:14px;color:#374151;"><strong>Package:</strong> ' . htmlspecialchars($packageLabel) . '</p>
|
||||
<p style="margin:4px 0;font-size:14px;color:#374151;"><strong>Requested Date:</strong> ' . htmlspecialchars($dateFormatted) . '</p>
|
||||
<div style='padding:32px;background:#fff'>
|
||||
<h2 style='margin-top:0;color:#0d0d0d'>Booking Request Received!</h2>
|
||||
<p style='color:#374151'>Hey " . htmlspecialchars($name) . ", your request is in. We'll confirm availability and reach out within a few hours.</p>
|
||||
<div style='background:#fff7ed;border:1px solid #fed7aa;border-radius:10px;padding:20px;margin:20px 0'>
|
||||
<p style='margin:0 0 6px;font-size:13px;color:#9ca3af;text-transform:uppercase;letter-spacing:1px'>Booking Reference</p>
|
||||
<p style='margin:0 0 16px;font-size:22px;font-weight:700;color:#f97316'>{$ref}</p>
|
||||
<p style='margin:4px 0;font-size:14px;color:#374151'><strong>Package:</strong> {$pkgLabel}</p>
|
||||
<p style='margin:4px 0;font-size:14px;color:#374151'><strong>Requested Date:</strong> {$dateLabel}</p>
|
||||
<p style='margin:4px 0;font-size:14px;color:#374151'><strong>Total:</strong> {$amountLabel}</p>
|
||||
</div>
|
||||
<p style="color:#374151;line-height:1.6;">We\'ll reach out to you at <strong>' . htmlspecialchars($email) . '</strong>' . ($phone ? ' or <strong>' . htmlspecialchars($phone) . '</strong>' : '') . ' to confirm your ride.</p>
|
||||
<p style="color:#374151;line-height:1.6;">Questions? Call or text us at <strong>(817) 555-0199</strong>.</p>
|
||||
<p style="color:#374151;line-height:1.6;">Ride on,<br><strong>The Parker County Slingshot Team</strong></p>
|
||||
<p style='color:#374151'>Questions? Call or text <strong>(817) 555-0199</strong> or reply to this email.</p>
|
||||
<p style='color:#374151'>Ride on,<br><strong>The Parker County Slingshot Team</strong></p>
|
||||
</div>
|
||||
<div style="background:#f3f4f6;padding:16px;text-align:center;">
|
||||
<p style="margin:0;font-size:12px;color:#9ca3af;">© ' . date('Y') . ' Parker County Slingshot Rentals — Weatherford, TX</p>
|
||||
<div style='background:#f3f4f6;padding:16px;text-align:center'>
|
||||
<p style='margin:0;font-size:12px;color:#9ca3af'>© " . date('Y') . " Parker County Slingshot Rentals — Weatherford, TX</p>
|
||||
</div>
|
||||
</div>';
|
||||
</div>";
|
||||
|
||||
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."]);
|
||||
|
||||
Reference in New Issue
Block a user