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:
+313
@@ -0,0 +1,313 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/db.php';
|
||||
session_start();
|
||||
|
||||
$isAjax = !empty($_SERVER['HTTP_X_REQUESTED_WITH']) || (($_SERVER['HTTP_ACCEPT'] ?? '') === 'application/json');
|
||||
|
||||
// ── Auth ──────────────────────────────────────────────────────────────────────
|
||||
if ($_POST['action'] ?? '' === 'login') {
|
||||
if ($_POST['username'] === ADMIN_USER && password_verify($_POST['password'] ?? '', ADMIN_PASS)) {
|
||||
$_SESSION[ADMIN_SESSION_KEY] = true;
|
||||
}
|
||||
header('Location: /admin/'); exit;
|
||||
}
|
||||
if ($_GET['action'] ?? '' === 'logout') {
|
||||
session_destroy(); header('Location: /admin/'); exit;
|
||||
}
|
||||
$authed = !empty($_SESSION[ADMIN_SESSION_KEY]);
|
||||
|
||||
// ── AJAX handlers (must be authenticated) ────────────────────────────────────
|
||||
if ($isAjax && $authed) {
|
||||
header('Content-Type: application/json');
|
||||
$action = $_POST['action'] ?? $_GET['action'] ?? '';
|
||||
|
||||
if ($action === 'update_status') {
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
$status = $_POST['status'] ?? '';
|
||||
$allowed = ['pending','confirmed','completed','cancelled'];
|
||||
if ($id && in_array($status, $allowed)) {
|
||||
db()->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) { ?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>Admin Login — Parker County Slingshot Rentals</title>
|
||||
<style>
|
||||
*{box-sizing:border-box;margin:0;padding:0}
|
||||
body{font-family:system-ui,sans-serif;background:#0d0d0d;display:flex;align-items:center;justify-content:center;min-height:100vh}
|
||||
.box{background:#1a1a1a;border:1px solid rgba(255,255,255,.1);border-radius:12px;padding:2.5rem;width:100%;max-width:360px}
|
||||
h1{color:#f97316;font-size:1.4rem;margin-bottom:.25rem}
|
||||
p{color:rgba(255,255,255,.4);font-size:.85rem;margin-bottom:2rem}
|
||||
label{display:block;color:rgba(255,255,255,.6);font-size:.85rem;margin-bottom:.4rem}
|
||||
input{width:100%;background:rgba(255,255,255,.06);border:1px solid rgba(255,255,255,.12);border-radius:8px;padding:.75rem 1rem;color:#fff;font-size:.95rem;margin-bottom:1.25rem;outline:none}
|
||||
input:focus{border-color:#f97316}
|
||||
button{width:100%;background:#f97316;color:#fff;border:none;border-radius:8px;padding:.85rem;font-size:1rem;font-weight:700;cursor:pointer}
|
||||
button:hover{background:#ea580c}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="box">
|
||||
<h1>Parker Admin</h1>
|
||||
<p>Slingshot Rentals Management</p>
|
||||
<form method="POST">
|
||||
<input type="hidden" name="action" value="login">
|
||||
<label>Username</label>
|
||||
<input type="text" name="username" required autofocus>
|
||||
<label>Password</label>
|
||||
<input type="password" name="password" required>
|
||||
<button type="submit">Sign In</button>
|
||||
</form>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
<?php exit; }
|
||||
|
||||
// ── Dashboard data ────────────────────────────────────────────────────────────
|
||||
$statusFilter = $_GET['status'] ?? '';
|
||||
$where = $statusFilter ? "WHERE status = " . db()->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'];
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>Admin — Parker County Slingshot Rentals</title>
|
||||
<style>
|
||||
*{box-sizing:border-box;margin:0;padding:0}
|
||||
body{font-family:system-ui,sans-serif;background:#f3f4f6;color:#111;min-height:100vh}
|
||||
header{background:#0d0d0d;color:#fff;padding:1rem 2rem;display:flex;align-items:center;justify-content:space-between}
|
||||
header h1{color:#f97316;font-size:1.2rem;font-weight:800}
|
||||
header a{color:rgba(255,255,255,.5);font-size:.85rem;text-decoration:none}
|
||||
header a:hover{color:#fff}
|
||||
.main{max-width:1200px;margin:0 auto;padding:2rem 1.5rem}
|
||||
.stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:1rem;margin-bottom:2rem}
|
||||
.stat{background:#fff;border-radius:10px;padding:1.25rem;border:1px solid #e5e7eb;text-align:center}
|
||||
.stat-val{font-size:2rem;font-weight:800;line-height:1}
|
||||
.stat-lbl{font-size:.8rem;color:#6b7280;margin-top:.25rem;text-transform:uppercase;letter-spacing:.5px}
|
||||
.card{background:#fff;border-radius:10px;border:1px solid #e5e7eb;margin-bottom:2rem}
|
||||
.card-header{padding:1rem 1.5rem;border-bottom:1px solid #f3f4f6;display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:.75rem}
|
||||
.card-header h2{font-size:1rem;font-weight:700}
|
||||
.filters a{padding:.35rem .75rem;border-radius:6px;font-size:.8rem;text-decoration:none;color:#6b7280;border:1px solid #e5e7eb;margin-left:.35rem}
|
||||
.filters a.active,.filters a:hover{background:#f97316;color:#fff;border-color:#f97316}
|
||||
table{width:100%;border-collapse:collapse;font-size:.875rem}
|
||||
th{background:#f9fafb;padding:.6rem 1rem;text-align:left;font-weight:600;color:#6b7280;font-size:.75rem;text-transform:uppercase;letter-spacing:.5px;border-bottom:1px solid #e5e7eb}
|
||||
td{padding:.75rem 1rem;border-bottom:1px solid #f3f4f6;vertical-align:top}
|
||||
tr:last-child td{border-bottom:none}
|
||||
tr:hover td{background:#fafafa}
|
||||
.badge{display:inline-block;padding:.2rem .6rem;border-radius:999px;font-size:.72rem;font-weight:700;text-transform:uppercase;letter-spacing:.5px}
|
||||
.badge-pending{background:#fef3c7;color:#92400e}
|
||||
.badge-confirmed{background:#dcfce7;color:#14532d}
|
||||
.badge-completed{background:#dbeafe;color:#1e3a8a}
|
||||
.badge-cancelled{background:#fee2e2;color:#7f1d1d}
|
||||
select.status-sel{font-size:.8rem;border:1px solid #e5e7eb;border-radius:6px;padding:.25rem .5rem;cursor:pointer}
|
||||
textarea.notes-ta{width:100%;font-size:.8rem;border:1px solid #e5e7eb;border-radius:6px;padding:.4rem .6rem;resize:vertical;min-height:50px;font-family:inherit}
|
||||
.save-btn{font-size:.75rem;background:#f97316;color:#fff;border:none;border-radius:5px;padding:.25rem .6rem;cursor:pointer;margin-top:.25rem}
|
||||
.block-form{display:flex;gap:.75rem;flex-wrap:wrap;align-items:flex-end;padding:1rem 1.5rem}
|
||||
.block-form input{border:1px solid #e5e7eb;border-radius:6px;padding:.45rem .75rem;font-size:.875rem}
|
||||
.block-form button{background:#dc2626;color:#fff;border:none;border-radius:6px;padding:.45rem 1rem;font-size:.875rem;font-weight:600;cursor:pointer}
|
||||
.block-list{padding:0 1.5rem 1rem}
|
||||
.block-item{display:flex;align-items:center;gap:.75rem;padding:.5rem 0;border-bottom:1px solid #f3f4f6;font-size:.875rem}
|
||||
.block-item:last-child{border-bottom:none}
|
||||
.del-btn{background:none;border:none;color:#dc2626;cursor:pointer;font-size:1rem;padding:0}
|
||||
@media(max-width:700px){.main{padding:1rem}.stat-val{font-size:1.5rem}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>Parker County Slingshot — Admin</h1>
|
||||
<a href="?action=logout">Sign Out</a>
|
||||
</header>
|
||||
<div class="main">
|
||||
|
||||
<!-- Stats -->
|
||||
<div class="stats">
|
||||
<div class="stat"><div class="stat-val"><?= $stats['total'] ?></div><div class="stat-lbl">Total Bookings</div></div>
|
||||
<div class="stat"><div class="stat-val" style="color:#d97706"><?= $stats['pending'] ?></div><div class="stat-lbl">Pending</div></div>
|
||||
<div class="stat"><div class="stat-val" style="color:#16a34a"><?= $stats['confirmed'] ?></div><div class="stat-lbl">Confirmed</div></div>
|
||||
<div class="stat"><div class="stat-val" style="color:#2563eb"><?= $stats['completed'] ?></div><div class="stat-lbl">Completed</div></div>
|
||||
<div class="stat"><div class="stat-val" style="color:#f97316">$<?= number_format($stats['revenue'],0) ?></div><div class="stat-lbl">Revenue</div></div>
|
||||
</div>
|
||||
|
||||
<!-- Bookings -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2>Bookings</h2>
|
||||
<div class="filters">
|
||||
<a href="/admin/" class="<?= !$statusFilter ? 'active':'' ?>">All</a>
|
||||
<a href="/admin/?status=pending" class="<?= $statusFilter==='pending' ? 'active':'' ?>">Pending</a>
|
||||
<a href="/admin/?status=confirmed" class="<?= $statusFilter==='confirmed' ? 'active':'' ?>">Confirmed</a>
|
||||
<a href="/admin/?status=completed" class="<?= $statusFilter==='completed' ? 'active':'' ?>">Completed</a>
|
||||
<a href="/admin/?status=cancelled" class="<?= $statusFilter==='cancelled' ? 'active':'' ?>">Cancelled</a>
|
||||
</div>
|
||||
</div>
|
||||
<?php if (empty($bookings)): ?>
|
||||
<div style="padding:3rem;text-align:center;color:#9ca3af">No bookings found.</div>
|
||||
<?php else: ?>
|
||||
<div style="overflow-x:auto">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Ref</th><th>Customer</th><th>Package</th><th>Date</th>
|
||||
<th>Amount</th><th>Status</th><th>Admin Notes</th><th>Received</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($bookings as $b): ?>
|
||||
<tr>
|
||||
<td><strong><?= htmlspecialchars($b['booking_ref']) ?></strong></td>
|
||||
<td>
|
||||
<strong><?= htmlspecialchars($b['name']) ?></strong><br>
|
||||
<a href="mailto:<?= htmlspecialchars($b['email']) ?>" style="color:#f97316;font-size:.8rem"><?= htmlspecialchars($b['email']) ?></a>
|
||||
<?php if ($b['phone']): ?><br><span style="font-size:.8rem;color:#6b7280"><?= htmlspecialchars($b['phone']) ?></span><?php endif; ?>
|
||||
</td>
|
||||
<td>
|
||||
<?php $pkg = PACKAGES[$b['package']] ?? ['label'=>$b['package']]; ?>
|
||||
<?= htmlspecialchars($pkg['label']) ?>
|
||||
</td>
|
||||
<td>
|
||||
<strong><?= date('M j, Y', strtotime($b['rental_date'])) ?></strong>
|
||||
<?php if ($b['end_date'] !== $b['rental_date']): ?>
|
||||
<br><span style="font-size:.8rem;color:#6b7280">→ <?= date('M j', strtotime($b['end_date'])) ?></span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td>$<?= number_format($b['amount'],2) ?></td>
|
||||
<td>
|
||||
<select class="status-sel" data-id="<?= $b['id'] ?>" onchange="updateStatus(this)">
|
||||
<?php foreach (['pending','confirmed','completed','cancelled'] as $s): ?>
|
||||
<option value="<?= $s ?>" <?= $b['status']===$s?'selected':'' ?>><?= ucfirst($s) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<textarea class="notes-ta" data-id="<?= $b['id'] ?>"><?= htmlspecialchars($b['admin_notes'] ?? '') ?></textarea>
|
||||
<button class="save-btn" onclick="saveNotes(this)">Save</button>
|
||||
</td>
|
||||
<td style="font-size:.8rem;color:#9ca3af;white-space:nowrap"><?= date('M j g:ia', strtotime($b['created_at'])) ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<!-- Block Dates -->
|
||||
<div class="card">
|
||||
<div class="card-header"><h2>Block Dates (maintenance / personal use)</h2></div>
|
||||
<form class="block-form" onsubmit="blockDate(event)">
|
||||
<div>
|
||||
<label style="font-size:.8rem;color:#6b7280;display:block;margin-bottom:.25rem">Date</label>
|
||||
<input type="date" id="blockDate" required min="<?= date('Y-m-d') ?>">
|
||||
</div>
|
||||
<div style="flex:1;min-width:180px">
|
||||
<label style="font-size:.8rem;color:#6b7280;display:block;margin-bottom:.25rem">Reason (optional)</label>
|
||||
<input type="text" id="blockReason" placeholder="e.g. Maintenance">
|
||||
</div>
|
||||
<button type="submit">Block Date</button>
|
||||
</form>
|
||||
<div class="block-list">
|
||||
<?php if (empty($blocked)): ?>
|
||||
<p style="color:#9ca3af;font-size:.875rem;padding:.5rem 0">No dates blocked.</p>
|
||||
<?php else: ?>
|
||||
<?php foreach ($blocked as $bl): ?>
|
||||
<div class="block-item" id="blocked-<?= $bl['id'] ?>">
|
||||
<button class="del-btn" onclick="unblockDate(<?= $bl['id'] ?>)" title="Remove">✕</button>
|
||||
<strong><?= date('M j, Y', strtotime($bl['block_date'])) ?></strong>
|
||||
<?php if ($bl['reason']): ?><span style="color:#6b7280">— <?= htmlspecialchars($bl['reason']) ?></span><?php endif; ?>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function updateStatus(sel) {
|
||||
fetch('/admin/', {
|
||||
method:'POST',
|
||||
headers:{'Content-Type':'application/x-www-form-urlencoded','X-Requested-With':'XMLHttpRequest'},
|
||||
body:'action=update_status&id='+sel.dataset.id+'&status='+sel.value
|
||||
}).then(r=>r.json()).then(d=>{ if(!d.ok) alert('Error saving status'); });
|
||||
}
|
||||
|
||||
function saveNotes(btn) {
|
||||
var ta = btn.previousElementSibling;
|
||||
fetch('/admin/', {
|
||||
method:'POST',
|
||||
headers:{'Content-Type':'application/x-www-form-urlencoded','X-Requested-With':'XMLHttpRequest'},
|
||||
body:'action=save_admin_notes&id='+ta.dataset.id+'¬es='+encodeURIComponent(ta.value)
|
||||
}).then(r=>r.json()).then(d=>{
|
||||
btn.textContent = d.ok ? 'Saved ✓' : 'Error';
|
||||
setTimeout(()=>btn.textContent='Save', 1500);
|
||||
});
|
||||
}
|
||||
|
||||
function blockDate(e) {
|
||||
e.preventDefault();
|
||||
var date = document.getElementById('blockDate').value;
|
||||
var reason = document.getElementById('blockReason').value;
|
||||
fetch('/admin/', {
|
||||
method:'POST',
|
||||
headers:{'Content-Type':'application/x-www-form-urlencoded','X-Requested-With':'XMLHttpRequest'},
|
||||
body:'action=block_date&date='+date+'&reason='+encodeURIComponent(reason)
|
||||
}).then(r=>r.json()).then(d=>{ if(d.ok) location.reload(); else alert(d.error); });
|
||||
}
|
||||
|
||||
function unblockDate(id) {
|
||||
if (!confirm('Remove this blocked date?')) return;
|
||||
fetch('/admin/', {
|
||||
method:'POST',
|
||||
headers:{'Content-Type':'application/x-www-form-urlencoded','X-Requested-With':'XMLHttpRequest'},
|
||||
body:'action=unblock_date&id='+id
|
||||
}).then(r=>r.json()).then(d=>{ if(d.ok) document.getElementById('blocked-'+id).remove(); });
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/db.php';
|
||||
|
||||
header('Content-Type: application/json');
|
||||
header('Access-Control-Allow-Origin: https://parkerslingshotrentals.com');
|
||||
|
||||
$month = (int)($_GET['month'] ?? date('n'));
|
||||
$year = (int)($_GET['year'] ?? date('Y'));
|
||||
$month = max(1, min(12, $month));
|
||||
$year = max(date('Y'), min(date('Y') + 2, $year));
|
||||
|
||||
$start = sprintf('%04d-%02d-01', $year, $month);
|
||||
$end = date('Y-m-t', strtotime($start));
|
||||
|
||||
// Booked dates from confirmed/pending bookings
|
||||
$booked = db()->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)),
|
||||
]);
|
||||
+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."]);
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
define('PARKER_DB_HOST', 'localhost');
|
||||
define('PARKER_DB_NAME', 'parker_db');
|
||||
define('PARKER_DB_USER', 'parker_user');
|
||||
define('PARKER_DB_PASS', 'Pk4rk3r_2026!Tx');
|
||||
|
||||
define('ADMIN_USER', 'admin');
|
||||
define('ADMIN_PASS', '$2y$12$Tz8QkXv9mNpL3wR1uE6OaOQbKjH5sYdF2cVnMxPt7lGAeWqZiBSHu'); // Parker2026!
|
||||
define('ADMIN_SESSION_KEY', 'parker_admin_auth');
|
||||
|
||||
define('SENDGRID_API_KEY', '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');
|
||||
define('MAIL_FROM', 'noreply@parkerslingshotrentals.com');
|
||||
define('MAIL_FROM_NAME', 'Parker County Slingshot Rentals');
|
||||
define('ADMIN_EMAIL', 'info@parkerslingshotrentals.com');
|
||||
|
||||
define('PACKAGES', [
|
||||
'half-day' => ['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;
|
||||
}
|
||||
+163
-9
@@ -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 @@
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<!-- Availability Calendar -->
|
||||
<div class="cal-wrap" id="calWrap">
|
||||
<div class="cal-header">
|
||||
<button class="cal-nav" id="calPrev" aria-label="Previous month">‹</button>
|
||||
<h3 id="calTitle">Loading…</h3>
|
||||
<button class="cal-nav" id="calNext" aria-label="Next month">›</button>
|
||||
</div>
|
||||
<div class="cal-grid" id="calGrid">
|
||||
<div class="cal-loading" style="grid-column:1/-1">Checking availability…</div>
|
||||
</div>
|
||||
<div class="cal-legend">
|
||||
<div class="cal-legend-item"><div class="cal-legend-dot" style="background:rgba(255,255,255,0.12)"></div> Available</div>
|
||||
<div class="cal-legend-item"><div class="cal-legend-dot" style="background:rgba(239,68,68,0.35)"></div> Booked</div>
|
||||
<div class="cal-legend-item"><div class="cal-legend-dot" style="background:var(--orange)"></div> Selected</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form class="contact-form" id="bookingForm" novalidate>
|
||||
<div class="form-row">
|
||||
<input type="text" name="name" placeholder="Your Name" required />
|
||||
@@ -694,18 +733,130 @@
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
// Set min date to today on date picker
|
||||
const dateInput = document.querySelector('input[type="date"]');
|
||||
if (dateInput) {
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
dateInput.min = today;
|
||||
// ── Availability Calendar ────────────────────────────────────────────────────
|
||||
const dateInput = document.querySelector('input[type="date"]');
|
||||
const calGrid = document.getElementById('calGrid');
|
||||
const calTitle = document.getElementById('calTitle');
|
||||
const DAY_NAMES = ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'];
|
||||
const MONTH_NAMES = ['January','February','March','April','May','June','July','August','September','October','November','December'];
|
||||
|
||||
const todayStr = new Date().toISOString().split('T')[0];
|
||||
if (dateInput) dateInput.min = todayStr;
|
||||
|
||||
let calYear = new Date().getFullYear();
|
||||
let calMonth = new Date().getMonth() + 1; // 1-based
|
||||
let bookedSet = new Set();
|
||||
let selectedDate = null;
|
||||
|
||||
async function loadCalendar(month, year) {
|
||||
calTitle.textContent = MONTH_NAMES[month - 1] + ' ' + year;
|
||||
calGrid.innerHTML = '<div class="cal-loading" style="grid-column:1/-1">Checking availability…</div>';
|
||||
try {
|
||||
const res = await fetch('/availability.php?month=' + month + '&year=' + year);
|
||||
const data = await res.json();
|
||||
bookedSet = new Set(data.booked_dates || []);
|
||||
renderCalendar(month, year);
|
||||
} catch {
|
||||
calGrid.innerHTML = '<div class="cal-loading" style="grid-column:1/-1;color:rgba(239,68,68,0.6)">Could not load availability.</div>';
|
||||
}
|
||||
}
|
||||
|
||||
function renderCalendar(month, year) {
|
||||
const fragment = document.createDocumentFragment();
|
||||
|
||||
// Day-name headers
|
||||
DAY_NAMES.forEach(d => {
|
||||
const el = document.createElement('div');
|
||||
el.className = 'cal-day-label';
|
||||
el.textContent = d;
|
||||
fragment.appendChild(el);
|
||||
});
|
||||
|
||||
const firstDay = new Date(year, month - 1, 1).getDay(); // 0=Sun
|
||||
const daysInMo = new Date(year, month, 0).getDate();
|
||||
|
||||
// Blank cells before first day
|
||||
for (let i = 0; i < firstDay; i++) {
|
||||
const el = document.createElement('div');
|
||||
el.className = 'cal-day empty';
|
||||
fragment.appendChild(el);
|
||||
}
|
||||
|
||||
for (let d = 1; d <= daysInMo; d++) {
|
||||
const dateStr = year + '-' + String(month).padStart(2,'0') + '-' + String(d).padStart(2,'0');
|
||||
const el = document.createElement('div');
|
||||
el.textContent = d;
|
||||
|
||||
const isPast = dateStr < todayStr;
|
||||
const isBooked = bookedSet.has(dateStr);
|
||||
const isToday = dateStr === todayStr;
|
||||
const isSel = dateStr === selectedDate;
|
||||
|
||||
if (isSel) {
|
||||
el.className = 'cal-day selected';
|
||||
} else if (isPast) {
|
||||
el.className = 'cal-day past';
|
||||
} else if (isBooked) {
|
||||
el.className = 'cal-day booked';
|
||||
el.title = 'Already booked';
|
||||
} else {
|
||||
el.className = 'cal-day available' + (isToday ? ' today' : '');
|
||||
el.addEventListener('click', () => selectDate(dateStr));
|
||||
}
|
||||
|
||||
fragment.appendChild(el);
|
||||
}
|
||||
|
||||
calGrid.innerHTML = '';
|
||||
calGrid.appendChild(fragment);
|
||||
}
|
||||
|
||||
function selectDate(dateStr) {
|
||||
selectedDate = dateStr;
|
||||
if (dateInput) {
|
||||
dateInput.value = dateStr;
|
||||
dateInput.dispatchEvent(new Event('change'));
|
||||
}
|
||||
renderCalendar(calMonth, calYear);
|
||||
// Smooth-scroll form into view
|
||||
document.getElementById('bookingForm').scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
}
|
||||
|
||||
document.getElementById('calPrev').addEventListener('click', () => {
|
||||
calMonth--;
|
||||
if (calMonth < 1) { calMonth = 12; calYear--; }
|
||||
loadCalendar(calMonth, calYear);
|
||||
});
|
||||
document.getElementById('calNext').addEventListener('click', () => {
|
||||
calMonth++;
|
||||
if (calMonth > 12) { calMonth = 1; calYear++; }
|
||||
loadCalendar(calMonth, calYear);
|
||||
});
|
||||
|
||||
// Initial load
|
||||
loadCalendar(calMonth, calYear);
|
||||
|
||||
// Keep calendar in sync when user types a date manually
|
||||
if (dateInput) {
|
||||
dateInput.addEventListener('change', () => {
|
||||
if (dateInput.value) {
|
||||
const [y, m] = dateInput.value.split('-').map(Number);
|
||||
if (m !== calMonth || y !== calYear) {
|
||||
calMonth = m; calYear = y;
|
||||
loadCalendar(calMonth, calYear);
|
||||
}
|
||||
selectedDate = dateInput.value;
|
||||
renderCalendar(calMonth, calYear);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ── Booking Form ─────────────────────────────────────────────────────────────
|
||||
document.getElementById('bookingForm').addEventListener('submit', async function(e) {
|
||||
e.preventDefault();
|
||||
const form = this;
|
||||
const msg = document.getElementById('formMsg');
|
||||
const btn = form.querySelector('button[type="submit"]');
|
||||
const msg = document.getElementById('formMsg');
|
||||
const btn = form.querySelector('button[type="submit"]');
|
||||
btn.textContent = 'Sending...';
|
||||
btn.disabled = true;
|
||||
msg.className = 'form-msg';
|
||||
@@ -721,7 +872,7 @@
|
||||
};
|
||||
|
||||
try {
|
||||
const res = await fetch('/contact.php', {
|
||||
const res = await fetch('/contact.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data),
|
||||
@@ -729,12 +880,15 @@
|
||||
const json = await res.json();
|
||||
if (json.success) {
|
||||
msg.className = 'form-msg success';
|
||||
msg.textContent = json.message || 'Thanks! We received your request and will be in touch soon.';
|
||||
msg.innerHTML = json.message || 'Thanks! We received your request and will be in touch soon.';
|
||||
msg.style.display = 'block';
|
||||
form.reset();
|
||||
selectedDate = null;
|
||||
if (dateInput) dateInput.min = new Date().toISOString().split('T')[0];
|
||||
btn.textContent = 'Request Sent!';
|
||||
btn.style.background = '#16a34a';
|
||||
// Refresh calendar to show newly booked date
|
||||
loadCalendar(calMonth, calYear);
|
||||
} else {
|
||||
throw new Error(json.error || 'Something went wrong.');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user