Initial commit — Parker County Slingshot Rentals booking site

Full booking system with Square card-on-file, 10-step booking flow,
pre-departure checklist, and Mailjet email integration.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-25 18:31:12 +00:00
commit 3e18d71378
13 changed files with 4334 additions and 0 deletions
+11
View File
@@ -0,0 +1,11 @@
# Credentials (server-only — never commit real values)
db.php
config.php
# Uploads / user files
uploads/
*.log
# OS / editor
.DS_Store
*.swp
+6
View File
@@ -0,0 +1,6 @@
DirectoryIndex index.html index.php
<Files db.php>
Order deny,allow
Deny from all
</Files>
+5
View File
@@ -0,0 +1,5 @@
<IfModule LiteSpeed>
CacheEnable off
</IfModule>
Header always set Cache-Control "no-store, no-cache, must-revalidate"
Header always set Pragma "no-cache"
+2093
View File
File diff suppressed because it is too large Load Diff
+36
View File
@@ -0,0 +1,36 @@
<?php
// admin/login.php
require_once __DIR__ . '/../config.php';
if(isAdminLoggedIn()){header('Location:/admin/index.php');exit;}
$error='';
if($_SERVER['REQUEST_METHOD']==='POST'){
$email=trim($_POST['email']??'');
$pass=trim($_POST['password']??'');
$admin=db()->prepare("SELECT * FROM pcs_admins WHERE email=?");
$admin->execute([$email]);
$a=$admin->fetch();
if($a && password_verify($pass,$a['password_hash'])){
$_SESSION['pcs_admin']=['id'=>$a['id'],'email'=>$a['email'],'name'=>$a['name']];
header('Location:/admin/index.php');exit;
}
$error='Invalid credentials.';
}
?><!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</title>
<link href="https://fonts.googleapis.com/css2?family=Bebas+Neue&family=Barlow+Condensed:wght@600;700&family=Barlow:wght@400;500&display=swap" rel="stylesheet">
<style>*{box-sizing:border-box;margin:0;padding:0}body{background:#080808;font-family:'Barlow',sans-serif;min-height:100vh;display:flex;align-items:center;justify-content:center;padding:20px;background-image:radial-gradient(ellipse at 30% 50%,rgba(255,45,0,.06),transparent 50%)}
.box{background:#141414;border:1px solid rgba(255,45,0,.2);padding:44px;width:100%;max-width:420px;position:relative}
.box::before{content:'';position:absolute;top:0;left:0;right:0;height:3px;background:linear-gradient(90deg,transparent,#ff2d00,transparent)}
.logo{font-family:'Bebas Neue',sans-serif;font-size:32px;color:#ff2d00;letter-spacing:3px;text-align:center;margin-bottom:4px}
.sub{font-family:'Barlow Condensed',sans-serif;font-size:13px;color:#555;letter-spacing:2px;text-transform:uppercase;text-align:center;margin-bottom:32px}
label{display:block;font-family:'Barlow Condensed',sans-serif;font-size:12px;font-weight:700;color:#666;letter-spacing:1.5px;text-transform:uppercase;margin-bottom:7px}
input{width:100%;background:#0d0d0d;border:1.5px solid rgba(255,255,255,.08);color:#f0f0f0;padding:13px 15px;font-family:'Barlow',sans-serif;font-size:15px;outline:none;margin-bottom:18px;transition:border-color .2s}
input:focus{border-color:#ff2d00}.btn{width:100%;padding:15px;border:none;background:#ff2d00;color:#fff;font-family:'Barlow Condensed',sans-serif;font-weight:700;font-size:16px;letter-spacing:2px;text-transform:uppercase;cursor:pointer;transition:background .2s}
.btn:hover{background:#e02500}.error{background:rgba(255,45,0,.1);border:1px solid rgba(255,45,0,.3);color:#ff6b35;padding:12px;font-family:'Barlow Condensed',sans-serif;font-size:14px;font-weight:700;margin-bottom:18px}
.back{display:block;text-align:center;margin-top:16px;color:#444;font-size:13px;text-decoration:none;transition:color .2s}.back:hover{color:#ff2d00}</style></head>
<body><div class="box"><div class="logo">PARKER SLINGSHOT</div><div class="sub">Admin Panel</div>
<?php if($error): ?><div class="error">⚠ <?=htmlspecialchars($error)?></div><?php endif; ?>
<form method="POST"><label>Email</label><input type="email" name="email" placeholder="admin@parkerslingshot.com" required>
<label>Password</label><input type="password" name="password" placeholder="••••••••" required>
<button type="submit" class="btn">Sign In</button></form>
<a href="/" class="back">← Back to Site</a></div></body></html>
+7
View File
@@ -0,0 +1,7 @@
<?php
// admin/logout.php
require_once __DIR__ . '/../config.php';
unset($_SESSION['pcs_admin']);
session_destroy();
header('Location: /admin/login.php');
exit;
+39
View File
@@ -0,0 +1,39 @@
<?php
/**
* Secure document viewer - admin only
* Serves uploaded license/insurance files securely
*/
require_once __DIR__ . '/../config.php';
requireAdmin();
$type = $_GET['type'] ?? '';
$file = basename($_GET['file'] ?? '');
$booking = (int)($_GET['booking'] ?? 0);
if (!$file || !$type || !$booking) {
die('Invalid request.');
}
// Verify booking exists
$b = db()->prepare("SELECT id FROM pcs_bookings WHERE id=?");
$b->execute([$booking]);
if (!$b->fetch()) die('Booking not found.');
if ($type === 'license') {
$path = LICENSE_DIR . $file;
} elseif ($type === 'insurance') {
$path = INSURANCE_DIR . $file;
} else {
die('Invalid document type.');
}
if (!file_exists($path)) die('File not found.');
// Serve the file
$mime = mime_content_type($path);
header('Content-Type: ' . $mime);
header('Content-Disposition: inline; filename="' . $file . '"');
header('Content-Length: ' . filesize($path));
header('Cache-Control: no-store, no-cache');
readfile($path);
exit;
+136
View File
@@ -0,0 +1,136 @@
<?php
/**
* Parker County Slingshot - Booking API
*/
require_once __DIR__ . '/../config.php';
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
jsonOut(['success'=>false,'error'=>'Method not allowed'], 405);
}
$name = trim($_POST['name'] ?? '');
$email = trim($_POST['email'] ?? '');
$phone = trim($_POST['phone'] ?? '');
$dob = trim($_POST['dob'] ?? '');
$method = trim($_POST['payment_method'] ?? 'cash');
$notes = trim($_POST['notes'] ?? '');
$startD = trim($_POST['start_date'] ?? '');
$endD = trim($_POST['end_date'] ?? '');
// Validate required fields
if (!$name || !$email || !$phone || !$dob || !$startD || !$endD) {
jsonOut(['success'=>false,'error'=>'All fields are required.']);
}
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
jsonOut(['success'=>false,'error'=>'Invalid email address.']);
}
// Validate dates
$start = DateTime::createFromFormat('Y-m-d', $startD);
$end = DateTime::createFromFormat('Y-m-d', $endD);
if (!$start || !$end || $end <= $start) {
jsonOut(['success'=>false,'error'=>'Invalid dates selected.']);
}
$days = (int)$start->diff($end)->days;
$minDays = (int)(getSetting('min_days', 1));
$maxDays = (int)(getSetting('max_days', 3));
if ($days < $minDays || $days > $maxDays) {
jsonOut(['success'=>false,'error'=>"Rental must be $minDays-$maxDays days."]);
}
// Validate age (25+)
$dobDt = DateTime::createFromFormat('Y-m-d', $dob);
if (!$dobDt) jsonOut(['success'=>false,'error'=>'Invalid date of birth.']);
$age = (new DateTime())->diff($dobDt)->y;
if ($age < 25) {
jsonOut(['success'=>false,'error'=>'Must be 25 or older to rent.']);
}
// Check availability
$conflict = db()->prepare("
SELECT COUNT(*) FROM pcs_bookings
WHERE status IN ('approved','active','pending')
AND NOT (end_date <= :start OR start_date >= :end)
");
$conflict->execute(['start'=>$startD,'end'=>$endD]);
if ($conflict->fetchColumn() > 0) {
jsonOut(['success'=>false,'error'=>'Those dates are no longer available.']);
}
// Check blocked dates
$cursor = clone $start;
while ($cursor <= $end) {
$blocked = db()->prepare("SELECT id FROM pcs_blocked_dates WHERE blocked_date=?");
$blocked->execute([$cursor->format('Y-m-d')]);
if ($blocked->fetch()) {
jsonOut(['success'=>false,'error'=>'One or more selected dates are unavailable.']);
}
$cursor->modify('+1 day');
}
// Handle file uploads
$licenseFile = null;
$insuranceFile = null;
function saveUpload(string $field, string $dir, string $prefix): ?string {
if (empty($_FILES[$field]['tmp_name'])) return null;
$file = $_FILES[$field];
$allowed = ['image/jpeg','image/png','image/gif','image/webp','application/pdf'];
if (!in_array($file['type'], $allowed)) return null;
if ($file['size'] > MAX_UPLOAD_BYTES) return null;
if (!is_dir($dir)) mkdir($dir, 0755, true);
$ext = pathinfo($file['name'], PATHINFO_EXTENSION);
$name = $prefix.'_'.time().'_'.bin2hex(random_bytes(4)).'.'.$ext;
$path = $dir.$name;
move_uploaded_file($file['tmp_name'], $path);
return $name;
}
$licenseFile = saveUpload('license', LICENSE_DIR, 'lic');
$insuranceFile = saveUpload('insurance', INSURANCE_DIR, 'ins');
if (!$licenseFile) {
jsonOut(['success'=>false,'error'=>"Driver's license upload failed. Check file type and size."]);
}
if (!$insuranceFile) {
jsonOut(['success'=>false,'error'=>'Insurance upload failed. Check file type and size.']);
}
// Create or find customer
$customer = db()->prepare("SELECT id FROM pcs_customers WHERE email=?");
$customer->execute([$email]);
$cust = $customer->fetch();
if ($cust) {
$custId = $cust['id'];
db()->prepare("UPDATE pcs_customers SET name=?,phone=?,dob=? WHERE id=?")
->execute([$name,$phone,$dob,$custId]);
} else {
db()->prepare("INSERT INTO pcs_customers (email,name,phone,dob) VALUES (?,?,?,?)")
->execute([$email,$name,$phone,$dob]);
$custId = db()->lastInsertId();
}
// Generate booking ref
$ref = 'PSR-'.strtoupper(bin2hex(random_bytes(3)));
$pricePerDay = (float)(getSetting('price_per_day', 150));
$total = $days * $pricePerDay;
// Get available fleet
$fleet = db()->query("SELECT id FROM pcs_fleet WHERE is_available=1 LIMIT 1")->fetch();
$fleetId = $fleet['id'] ?? null;
// Insert booking
db()->prepare("INSERT INTO pcs_bookings
(booking_ref,customer_id,fleet_id,start_date,end_date,days,price_per_day,total_amount,
payment_method,license_file,insurance_file,customer_notes)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?)")->execute([
$ref, $custId, $fleetId, $startD, $endD, $days,
$pricePerDay, $total, $method, $licenseFile, $insuranceFile, $notes
]);
jsonOut(['success'=>true,'ref'=>$ref,'message'=>'Reservation submitted successfully.']);
+47
View File
@@ -0,0 +1,47 @@
<?php
require_once __DIR__ . '/db.php';
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: ' . SITE_URL);
header('Cache-Control: no-store, no-cache, must-revalidate');
$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));
// Bookings that overlap this month — including those that start last month and end this month
$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 for this month
$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)),
]);
+282
View File
@@ -0,0 +1,282 @@
<?php
require_once __DIR__ . '/db.php';
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: ' . SITE_URL . '');
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; }
$input = json_decode(file_get_contents('php://input'), true) ?: $_POST;
$name = trim(strip_tags($input['name'] ?? ''));
$email = trim(strip_tags($input['email'] ?? ''));
$phone = trim(strip_tags($input['phone'] ?? ''));
$package = trim(strip_tags($input['package'] ?? ''));
$date = trim(strip_tags($input['date'] ?? ''));
$message = trim(strip_tags($input['message'] ?? ''));
$squareToken = trim($input['square_token'] ?? '');
if (!$name || !$email || !$package || !$date) {
http_response_code(400);
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;
}
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;
}
$pkg = PACKAGES[$package];
$rentalDate = $date;
$endDate = date('Y-m-d', strtotime($date . ' +' . $pkg['days'] . ' days'));
// 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;
}
$ref = generateRef();
$dateLabel = date('F j, Y', strtotime($rentalDate));
$pkgLabel = $pkg['label'];
$amountLabel = '$' . number_format($pkg['amount'], 2);
$depositLabel = '$' . number_format(DEPOSIT_AMOUNT, 2);
$balance = $pkg['amount'] - DEPOSIT_AMOUNT;
$balanceLabel = '$' . number_format($balance, 2);
// ── Square: create customer + card on file + deposit hold ─────────────────────
$sqCustomerId = null;
$sqCardId = null;
$sqCardLast4 = null;
$sqCardBrand = null;
$sqPaymentId = null;
$sqPaymentStatus = null;
$paymentDeclined = false;
$paymentError = '';
if ($squareToken) {
// 1. Create Square Customer
$custResp = squareApi('POST', '/customers', [
'idempotency_key' => $ref . '-cust',
'given_name' => $name,
'email_address' => $email,
'phone_number' => $phone ?: null,
'reference_id' => $ref,
]);
$sqCustomerId = $custResp['customer']['id'] ?? null;
// 2. Create card on file
if ($sqCustomerId) {
$cardResp = squareApi('POST', '/cards', [
'idempotency_key' => $ref . '-card',
'source_id' => $squareToken,
'card' => [
'cardholder_name' => $name,
'customer_id' => $sqCustomerId,
],
]);
$sqCardId = $cardResp['card']['id'] ?? null;
$sqCardLast4 = $cardResp['card']['last_4'] ?? null;
$sqCardBrand = $cardResp['card']['card_brand'] ?? null;
}
// 3. Deposit hold — use card_id if card was saved, else fall back to nonce
$sourceId = $sqCardId ?: $squareToken;
$payBody = [
'source_id' => $sourceId,
'idempotency_key' => $ref . '-dep-' . time(),
'amount_money' => ['amount' => (int)(DEPOSIT_AMOUNT * 100), 'currency' => 'USD'],
'autocomplete' => false,
'location_id' => SQUARE_LOCATION_ID,
'note' => "Deposit hold — booking {$ref}",
'reference_id' => $ref,
'buyer_email_address' => $email,
];
if ($sqCustomerId) $payBody['customer_id'] = $sqCustomerId;
$sqResp = squareApi('POST', '/payments', $payBody);
if (!empty($sqResp['payment']['id'])) {
$sqPaymentId = $sqResp['payment']['id'];
$sqPaymentStatus = $sqResp['payment']['status']; // APPROVED
} else {
// Card declined or error
$paymentDeclined = true;
$errDetail = $sqResp['errors'][0]['detail'] ?? ($sqResp['errors'][0]['code'] ?? '');
$errCode = $sqResp['errors'][0]['code'] ?? '';
$paymentError = match($errCode) {
'CARD_DECLINED', 'CARD_DECLINED_VERIFICATION_REQUIRED' => 'Your card was declined.',
'INSUFFICIENT_FUNDS' => 'Insufficient funds on card.',
'INVALID_CARD' => 'Card information is invalid.',
'EXPIRED_CARD' => 'Your card has expired.',
'CVV_FAILURE' => 'Card security code (CVV) did not match.',
'ADDRESS_VERIFICATION_FAILURE' => 'Card address verification failed.',
default => 'Payment could not be processed. Please try a different card.',
};
}
}
// ── Always create the booking (admin sees declined attempts too) ──────────────
$stmt = db()->prepare(
"INSERT INTO bookings
(booking_ref, name, email, phone, package, rental_date, end_date, amount, notes,
square_customer_id, square_card_id, card_last4, card_brand,
square_payment_id, square_payment_status)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)"
);
$stmt->execute([
$ref, $name, $email, $phone, $package, $rentalDate, $endDate, $pkg['amount'], $message,
$sqCustomerId, $sqCardId, $sqCardLast4, $sqCardBrand,
$sqPaymentId, $sqPaymentStatus,
]);
// ── Email templates ───────────────────────────────────────────────────────────
$cardBadge = $sqCardLast4
? " <span style='font-size:12px;color:#16a34a;font-weight:700'>✓ " . htmlspecialchars($sqCardBrand ?? 'Card') . " •••• " . htmlspecialchars($sqCardLast4) . " on file</span>"
: '';
if ($paymentDeclined) {
// ── Payment failed emails ────────────────────────────────────────────────
$adminFailHtml = "<div style='max-width:600px;margin:0 auto;font-family:Arial,sans-serif'>
<div style='background:#dc2626;padding:20px;text-align:center'>
<h1 style='color:#fff;margin:0;font-size:18px'>Payment Declined — Booking {$ref}</h1>
</div>
<div style='padding:24px;background:#fff;border:1px solid #e5e7eb'>
<p style='color:#dc2626;font-weight:700;font-size:15px'>⚠ The customer's card was declined. Please contact them to arrange an alternate payment method.</p>
<table style='width:100%;font-size:14px;margin-top:16px'>
<tr><td style='color:#6b7280;padding:6px 0;width:100px'>Ref</td><td style='padding:6px 0;font-weight:700'>{$ref}</td></tr>
<tr><td style='color:#6b7280;padding:6px 0'>Name</td><td style='padding:6px 0'>" . htmlspecialchars($name) . "</td></tr>
<tr><td style='color:#6b7280;padding:6px 0'>Email</td><td style='padding:6px 0'><a href='mailto:" . htmlspecialchars($email) . "'>" . htmlspecialchars($email) . "</a></td></tr>
<tr><td style='color:#6b7280;padding:6px 0'>Phone</td><td style='padding:6px 0'>" . htmlspecialchars($phone ?: '—') . "</td></tr>
<tr><td style='color:#6b7280;padding:6px 0'>Package</td><td style='padding:6px 0;font-weight:700'>{$pkgLabel}{$amountLabel}</td></tr>
<tr><td style='color:#6b7280;padding:6px 0'>Date</td><td style='padding:6px 0;font-weight:700'>{$dateLabel}</td></tr>
<tr><td style='color:#6b7280;padding:6px 0'>Error</td><td style='padding:6px 0;color:#dc2626'>" . htmlspecialchars($paymentError) . "</td></tr>
</table>
<div style='margin-top:16px;padding:12px;background:#fef2f2;border-left:4px solid #dc2626'>
<p style='margin:0;font-size:13px;color:#991b1b'><strong>Contact this customer to:</strong> take payment over the phone, ask them to try a different card online, or arrange alternative payment at pickup.</p>
</div>
</div>
</div>";
$custFailHtml = "<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='margin-top:0;color:#0d0d0d'>We Received Your Booking Request</h2>
<p style='color:#374151'>Hey " . htmlspecialchars($name) . ", we received your reservation request (Ref: <strong>{$ref}</strong>) but unfortunately we weren't able to process your payment.</p>
<div style='margin:20px 0;padding:16px;background:#fef2f2;border:1px solid #fecaca;border-radius:8px'>
<p style='margin:0 0 8px;font-weight:700;color:#991b1b'>Payment Issue</p>
<p style='margin:0;font-size:14px;color:#374151'>" . htmlspecialchars($paymentError) . "</p>
</div>
<p style='color:#374151'>To secure your booking, please:</p>
<ul style='color:#374151;font-size:14px;line-height:1.8;padding-left:20px'>
<li>Reply to this email with a preferred callback time</li>
<li>Call or text us at <strong>" . ADMIN_PHONE . "</strong></li>
<li>Try again online at <a href='" . SITE_URL . "' style='color:#f97316'>parkerslingshot.epictravelexpeditions.com</a> with a different card</li>
</ul>
<p style='color:#374151'>Your requested date is <strong>{$dateLabel}</strong> and we'll hold it for 24 hours while we sort out payment.</p>
<p style='color:#374151'>Ride on,<br><strong>The Parker County Slingshot Team</strong><br>" . ADMIN_PHONE . "</p>
</div>
<div style='background:#f3f4f6;padding:16px;text-align:center'>
<p style='margin:0;font-size:12px;color:#9ca3af'>&copy; " . date('Y') . " Parker County Slingshot Rentals &mdash; Weatherford, TX</p>
</div>
</div>";
sendEmail(ADMIN_EMAIL, 'Parker Slingshot Admin', "⚠ Payment Declined — {$ref}: {$name} ({$pkgLabel})", $adminFailHtml);
sendEmail($email, $name, "About Your Booking Request {$ref} — Parker County Slingshot Rentals", $custFailHtml);
echo json_encode([
'success' => false,
'payment_failed' => true,
'ref' => $ref,
'error' => $paymentError,
'contact_phone' => ADMIN_PHONE,
'contact_email' => ADMIN_EMAIL,
'message' => "We received your request (Ref: {$ref}) but couldn't process your card. Please call or text us at " . ADMIN_PHONE . " or reply to the confirmation email we sent.",
]);
exit;
}
// ── Payment succeeded — send standard confirmation emails ─────────────────────
$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: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>
<tr><td style='color:#6b7280;padding:8px 0'>Deposit Hold</td><td style='padding:8px 0'>{$depositLabel} (card held — not charged yet){$cardBadge}</td></tr>
<tr><td style='color:#6b7280;padding:8px 0'>Balance Due</td><td style='padding:8px 0;font-weight:700;color:#16a34a'>{$balanceLabel} at pickup</td></tr>
</table>
" . ($message ? "<div style='margin-top:12px;padding:12px;background:#fff7ed;border-left:4px solid #f97316'>" . nl2br(htmlspecialchars($message)) . "</div>" : "") . "
<p style='margin-top:8px;font-size:13px;color:#16a34a;font-weight:700'>✓ \$" . number_format(DEPOSIT_AMOUNT, 2) . " deposit hold authorized (Square — not yet captured)</p>
<p style='margin-top:4px;font-size:13px;color:#9ca3af'>Submitted " . date('F j, Y g:i A') . " CT</p>
</div>
</div>";
$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='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>
<p style='margin:4px 0;font-size:14px;color:#374151'><strong>Deposit (card hold today):</strong> {$depositLabel} <span style='font-size:12px;color:#16a34a;font-weight:700'>✓ Authorized</span>{$cardBadge}</p>
<p style='margin:4px 0;font-size:14px;color:#374151'><strong>Balance due at pickup:</strong> <span style='font-weight:700;color:#16a34a'>{$balanceLabel}</span></p>
</div>
<div style='margin:20px 0;padding:16px;background:#fff7ed;border:1px solid #fed7aa;border-radius:10px;text-align:center'>
<p style='margin:0 0 10px;font-size:14px;font-weight:700;color:#111'>Next Step: Sign Your Rental Agreement</p>
<p style='margin:0 0 14px;font-size:13px;color:#6b7280'>Once your booking is confirmed you'll sign our digital waiver online — no printer needed. Your link:</p>
<a href='" . SITE_URL . "/waiver.php?ref={$ref}' style='display:inline-block;background:#f97316;color:#fff;text-decoration:none;padding:10px 24px;border-radius:6px;font-weight:700;font-size:14px'>Sign Rental Agreement &rarr;</a>
</div>
<p style='color:#374151'>Questions? Call or text <strong>" . ADMIN_PHONE . "</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'>&copy; " . date('Y') . " Parker County Slingshot Rentals &mdash; Weatherford, TX</p>
</div>
</div>";
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);
$msg = "Booking request received! Your reference is {$ref}. We'll be in touch shortly.";
if ($sqPaymentId) $msg .= " A \$" . number_format(DEPOSIT_AMOUNT, 2) . " refundable deposit hold has been placed on your card.";
echo json_encode([
'success' => true,
'ref' => $ref,
'deposit_held' => (bool)$sqPaymentId,
'square_payment_id' => $sqPaymentId,
'message' => $msg,
]);
+91
View File
@@ -0,0 +1,91 @@
<?php
define('SITE_URL', 'https://parkerslingshot.epictravelexpeditions.com');
define('PARKER_DB_HOST', 'localhost');
define('PARKER_DB_NAME', 'epic_parkersling');
define('PARKER_DB_USER', 'epic_parkersling');
define('PARKER_DB_PASS', 'REPLACE_WITH_DB_PASSWORD');
define('ADMIN_USER', 'admin');
define('ADMIN_PASS', '$2y$10$ynnk3RfarOD7VIJizC30kuXqu6tQ3gotNrlp5y33afh5fPOgnAMU6'); // Parker2026!
define('ADMIN_SESSION_KEY', 'parker_admin_auth');
define('ADMIN_PHONE', '(817) 555-0199');
// Mailjet credentials — fill in once you have your Mailjet account
define('MAILJET_API_KEY', 'YOUR_MAILJET_API_KEY'); // Mailjet API Key (public)
define('MAILJET_SECRET_KEY', 'YOUR_MAILJET_SECRET_KEY'); // Mailjet Secret Key (private)
define('MAIL_FROM', 'noreply@parkerslingshotrentals.com');
define('MAIL_FROM_NAME', 'Parker County Slingshot Rentals');
define('ADMIN_EMAIL', 'info@parkerslingshotrentals.com');
define('SQUARE_ACCESS_TOKEN', 'YOUR_SQUARE_ACCESS_TOKEN
define('SQUARE_APP_ID', 'YOUR_SQUARE_APP_ID
define('SQUARE_LOCATION_ID', 'YOUR_SQUARE_LOCATION_ID');
define('SQUARE_VERSION', '2024-01-18');
define('DEPOSIT_AMOUNT', 45.00); // $45 deposit hold — balance due at pickup
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 squareApi(string $method, string $path, array $body = []): array {
$ch = curl_init('https://connect.squareup.com/v2' . $path);
$headers = [
'Authorization: Bearer ' . SQUARE_ACCESS_TOKEN,
'Content-Type: application/json',
'Square-Version: ' . SQUARE_VERSION,
];
$opts = [CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => $headers, CURLOPT_TIMEOUT => 30, CURLOPT_SSL_VERIFYPEER => false];
if ($method === 'POST') {
$opts[CURLOPT_POST] = true;
$opts[CURLOPT_POSTFIELDS] = $body ? json_encode($body) : '{}';
}
curl_setopt_array($ch, $opts);
$resp = curl_exec($ch);
curl_close($ch);
return json_decode($resp ?: '{}', true);
}
function generateRef(): string {
return 'PSR-' . strtoupper(substr(uniqid(), -6));
}
function sendEmail(string $to, string $toName, string $subject, string $html): bool {
if (strpos(MAILJET_API_KEY, 'YOUR_') !== false) return false;
$payload = json_encode([
'Messages' => [[
'From' => ['Email' => MAIL_FROM, 'Name' => MAIL_FROM_NAME],
'To' => [['Email' => $to, 'Name' => $toName]],
'Subject' => $subject,
'HTMLPart' => $html,
]],
]);
$ch = curl_init('https://api.mailjet.com/v3.1/send');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $payload,
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_USERPWD => MAILJET_API_KEY . ':' . MAILJET_SECRET_KEY,
CURLOPT_TIMEOUT => 15,
CURLOPT_SSL_VERIFYPEER => false,
]);
$resp = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
return $code === 200;
}
+1216
View File
File diff suppressed because it is too large Load Diff
+365
View File
@@ -0,0 +1,365 @@
<?php
require_once __DIR__ . '/db.php';
header('X-Frame-Options: DENY');
header('X-Content-Type-Options: nosniff');
$ref = strtoupper(trim($_GET['ref'] ?? ''));
$error = '';
$booking = null;
$signed = false;
if ($ref) {
$stmt = db()->prepare("SELECT * FROM bookings WHERE booking_ref = ?");
$stmt->execute([$ref]);
$booking = $stmt->fetch();
if (!$booking) {
$error = 'Booking reference not found. Please check your confirmation email.';
} elseif ($booking['status'] === 'cancelled') {
$error = 'This booking has been cancelled. Please contact us if you have questions.';
} elseif ($booking['waiver_signed']) {
$signed = true;
}
}
// Handle submission
if ($_SERVER['REQUEST_METHOD'] === 'POST' && $booking && !$signed) {
$sigName = trim(strip_tags($_POST['sig_name'] ?? ''));
$sigData = $_POST['sig_data'] ?? ''; // base64 canvas PNG
$checks = (array)($_POST['checks'] ?? []);
$required = ['age','license','insurance','rules','damage','waiver'];
$missing = array_diff($required, $checks);
if (!$sigName) {
$error = 'Please type your full name to sign.';
} elseif ($missing) {
$error = 'Please check all required boxes before signing.';
} elseif (!$sigData || strpos($sigData, 'data:image/png;base64,') !== 0) {
$error = 'Please draw your signature in the box above.';
} else {
$ip = $_SERVER['HTTP_X_FORWARDED_FOR'] ?? $_SERVER['REMOTE_ADDR'] ?? '';
$ip = explode(',', $ip)[0];
db()->prepare(
"UPDATE bookings SET waiver_signed=1, waiver_signed_at=NOW(), waiver_ip=?, waiver_name=?, waiver_sig=? WHERE booking_ref=?"
)->execute([trim($ip), $sigName, $sigData, $ref]);
$pkg = PACKAGES[$booking['package']] ?? ['label'=>$booking['package']];
$dateLabel = date('F j, Y', strtotime($booking['rental_date']));
$adminHtml = "<div style='font-family:Arial,sans-serif;max-width:600px;margin:0 auto'>
<div style='background:#f97316;padding:20px;text-align:center'>
<h1 style='color:#fff;margin:0;font-size:18px'>Waiver Signed — {$ref}</h1>
</div>
<div style='padding:24px;background:#fff;border:1px solid #e5e7eb'>
<p><strong>" . htmlspecialchars($booking['name']) . "</strong> signed the rental waiver for booking <strong>{$ref}</strong>.</p>
<table style='width:100%;font-size:14px'>
<tr><td style='color:#6b7280;padding:6px 0;width:110px'>Package</td><td style='padding:6px 0'>" . htmlspecialchars($pkg['label']) . "</td></tr>
<tr><td style='color:#6b7280;padding:6px 0'>Date</td><td style='padding:6px 0'>{$dateLabel}</td></tr>
<tr><td style='color:#6b7280;padding:6px 0'>Signed by</td><td style='padding:6px 0'>" . htmlspecialchars($sigName) . "</td></tr>
<tr><td style='color:#6b7280;padding:6px 0'>IP</td><td style='padding:6px 0'>" . htmlspecialchars($ip) . "</td></tr>
<tr><td style='color:#6b7280;padding:6px 0'>Timestamp</td><td style='padding:6px 0'>" . date('F j, Y g:i A') . " CT</td></tr>
</table>
<img src='{$sigData}' style='margin-top:16px;border:1px solid #e5e7eb;border-radius:6px;max-width:100%;height:auto' alt='Signature' />
</div>
</div>";
$custHtml = "<div style='font-family:Arial,sans-serif;max-width:600px;margin:0 auto'>
<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='margin-top:0'>Waiver Signed — You're All Set!</h2>
<p style='color:#374151'>Hey " . htmlspecialchars($booking['name']) . ", your rental agreement for booking <strong>{$ref}</strong> is signed and on file. See you on <strong>{$dateLabel}</strong>!</p>
<p style='color:#374151'>Remember to bring:</p>
<ul style='color:#374151'>
<li>Valid driver's license</li>
<li>Proof of personal auto insurance</li>
</ul>
<p style='color:#374151'>Questions? Call or text <strong>(817) 555-0199</strong>.</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'>&copy; " . date('Y') . " Parker County Slingshot Rentals &mdash; Weatherford, TX</p>
</div>
</div>";
sendEmail(ADMIN_EMAIL, 'Parker Slingshot Admin', "Waiver Signed: {$ref}" . $booking['name'], $adminHtml);
sendEmail($booking['email'], $booking['name'], "Rental Agreement Signed — {$ref}", $custHtml);
$signed = true;
}
}
$pkgLabel = $booking ? (PACKAGES[$booking['package']]['label'] ?? $booking['package']) : '';
$dateLabel = $booking ? date('F j, Y', strtotime($booking['rental_date'])) : '';
?><!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Rental Agreement — Parker County Slingshot Rentals</title>
<meta name="robots" content="noindex,nofollow" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Barlow+Condensed:wght@700;800&display=swap" rel="stylesheet" />
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root { --orange: #f97316; --orange-dark: #ea580c; --black: #0d0d0d; }
body { font-family: 'Inter', sans-serif; background: #f3f4f6; color: #111; line-height: 1.6; }
header { background: var(--black); padding: 1.25rem 2rem; display: flex; align-items: center; justify-content: space-between; }
header a { font-family: 'Barlow Condensed', sans-serif; font-size: 1.3rem; font-weight: 800; color: var(--orange); text-decoration: none; }
header span { font-size: 0.85rem; color: rgba(255,255,255,0.4); }
.wrap { max-width: 760px; margin: 2.5rem auto; padding: 0 1rem 4rem; }
.card { background: #fff; border-radius: 12px; box-shadow: 0 2px 12px rgba(0,0,0,0.08); padding: 2rem 2.5rem; margin-bottom: 1.5rem; }
@media (max-width: 600px) { .card { padding: 1.5rem; } }
h1 { font-family: 'Barlow Condensed', sans-serif; font-size: 2rem; font-weight: 800; margin-bottom: 0.25rem; }
h2 { font-size: 1rem; font-weight: 700; color: var(--orange); text-transform: uppercase; letter-spacing: 1px; margin: 1.75rem 0 0.75rem; }
p { color: #374151; font-size: 0.95rem; margin-bottom: 0.75rem; }
.booking-banner { background: #fff7ed; border: 1px solid #fed7aa; border-radius: 10px; padding: 1.25rem 1.5rem; margin-bottom: 1.75rem; display: flex; gap: 2rem; flex-wrap: wrap; }
.bb-item { font-size: 0.9rem; }
.bb-label { color: #9ca3af; font-size: 0.75rem; text-transform: uppercase; letter-spacing: 1px; margin-bottom: 0.2rem; }
.bb-value { font-weight: 700; color: #111; }
.bb-ref { color: var(--orange); font-size: 1.1rem; }
.clause { display: flex; gap: 0.85rem; align-items: flex-start; padding: 0.85rem 0; border-bottom: 1px solid #f3f4f6; }
.clause:last-child { border-bottom: none; }
.clause input[type=checkbox] { margin-top: 3px; width: 18px; height: 18px; accent-color: var(--orange); flex-shrink: 0; cursor: pointer; }
.clause label { font-size: 0.92rem; color: #374151; cursor: pointer; }
.clause label strong { color: #111; }
.sig-wrap { border: 2px dashed #d1d5db; border-radius: 8px; overflow: hidden; position: relative; background: #fafafa; margin-top: 0.5rem; }
#sigCanvas { display: block; width: 100%; height: 160px; cursor: crosshair; touch-action: none; }
.sig-clear { position: absolute; top: 8px; right: 8px; background: rgba(0,0,0,0.06); border: none; border-radius: 6px; padding: 4px 10px; font-size: 0.78rem; cursor: pointer; color: #6b7280; }
.sig-clear:hover { background: rgba(239,68,68,0.1); color: #ef4444; }
.sig-hint { font-size: 0.78rem; color: #9ca3af; margin-top: 0.4rem; }
input[type=text] { width: 100%; border: 1px solid #d1d5db; border-radius: 8px; padding: 0.75rem 1rem; font-size: 1rem; font-family: inherit; outline: none; transition: border-color 0.2s; margin-top: 0.4rem; }
input[type=text]:focus { border-color: var(--orange); }
.btn-sign { display: block; width: 100%; background: var(--orange); color: white; border: none; border-radius: 8px; padding: 1rem; font-size: 1.05rem; font-weight: 700; cursor: pointer; transition: background 0.2s; margin-top: 1.5rem; }
.btn-sign:hover { background: var(--orange-dark); }
.btn-sign:disabled { background: #d1d5db; cursor: not-allowed; }
.alert { padding: 0.9rem 1.1rem; border-radius: 8px; font-size: 0.9rem; margin-bottom: 1rem; }
.alert-error { background: rgba(239,68,68,0.08); border: 1px solid rgba(239,68,68,0.25); color: #dc2626; }
.alert-success { background: rgba(34,197,94,0.08); border: 1px solid rgba(34,197,94,0.25); color: #16a34a; }
.success-icon { font-size: 3rem; text-align: center; margin-bottom: 1rem; }
.success-box { text-align: center; padding: 1rem 0; }
.success-box h1 { color: #16a34a; margin-bottom: 0.5rem; }
.success-box p { color: #374151; max-width: 480px; margin: 0 auto 1rem; }
.checklist { text-align: left; display: inline-block; margin: 1rem auto; }
.checklist li { padding: 0.35rem 0; color: #374151; font-size: 0.95rem; list-style: none; }
.checklist li::before { content: '✓ '; color: #16a34a; font-weight: 700; }
.back-link { text-align: center; margin-top: 1.5rem; }
.back-link a { color: var(--orange); text-decoration: none; font-weight: 600; font-size: 0.9rem; }
</style>
</head>
<body>
<header>
<a href="/">Parker County Slingshot Rentals</a>
<span>Rental Agreement</span>
</header>
<div class="wrap">
<?php if (!$ref || $error === 'Booking reference not found. Please check your confirmation email.'): ?>
<!-- No valid ref — show lookup form -->
<div class="card">
<h1>Rental Agreement</h1>
<p style="margin:0.75rem 0 1.5rem;color:#6b7280;">Enter your booking reference from your confirmation email to access your rental agreement.</p>
<?php if ($error): ?><div class="alert alert-error"><?= htmlspecialchars($error) ?></div><?php endif; ?>
<form method="get" action="/waiver.php">
<label style="font-weight:600;font-size:0.9rem;display:block;margin-bottom:0.4rem;">Booking Reference</label>
<input type="text" name="ref" placeholder="PSR-XXXXXX" value="<?= htmlspecialchars($ref) ?>" style="text-transform:uppercase" maxlength="12" required />
<button type="submit" class="btn-sign" style="margin-top:1rem;">Look Up My Booking</button>
</form>
</div>
<?php elseif ($signed): ?>
<!-- Already signed or just signed -->
<div class="card">
<div class="success-icon">✅</div>
<div class="success-box">
<h1>You're All Set!</h1>
<p>Your rental agreement for booking <strong><?= htmlspecialchars($ref) ?></strong> is signed and on file. We'll see you on <strong><?= htmlspecialchars($dateLabel) ?></strong>!</p>
<p style="color:#6b7280;font-size:0.88rem;">A confirmation was sent to <?= htmlspecialchars($booking['email']) ?>.</p>
<p style="font-weight:700;margin-top:1rem;margin-bottom:0.25rem;">Remember to bring:</p>
<ul class="checklist">
<li>Valid driver's license</li>
<li>Proof of personal auto insurance</li>
</ul>
</div>
<div class="back-link"><a href="/">← Back to Parker County Slingshot Rentals</a></div>
</div>
<?php elseif ($booking): ?>
<!-- Waiver form -->
<?php if ($error): ?><div class="alert alert-error"><?= htmlspecialchars($error) ?></div><?php endif; ?>
<div class="card">
<h1>Rental Agreement</h1>
<p style="color:#6b7280;margin-top:0.25rem;margin-bottom:1.5rem;">Please read and sign the agreement below for your upcoming rental.</p>
<div class="booking-banner">
<div class="bb-item"><div class="bb-label">Booking Ref</div><div class="bb-value bb-ref"><?= htmlspecialchars($booking['booking_ref']) ?></div></div>
<div class="bb-item"><div class="bb-label">Name</div><div class="bb-value"><?= htmlspecialchars($booking['name']) ?></div></div>
<div class="bb-item"><div class="bb-label">Package</div><div class="bb-value"><?= htmlspecialchars($pkgLabel) ?></div></div>
<div class="bb-item"><div class="bb-label">Rental Date</div><div class="bb-value"><?= htmlspecialchars($dateLabel) ?></div></div>
</div>
<h2>Rental Terms & Conditions</h2>
<p>This Rental Agreement ("Agreement") is entered into between Parker County Slingshot Rentals ("Company") and the renter identified above ("Renter"). By signing below, Renter agrees to all terms stated herein.</p>
<h2>Eligibility Requirements</h2>
<p>Renter must be at least 25 years of age and hold a valid Class C driver's license (or equivalent) issued by a U.S. state or territory. Renter must not have any DUI/DWI convictions within the past 5 years.</p>
<h2>Insurance Requirement</h2>
<p>Renter is required to carry and provide proof of valid personal auto insurance at the time of vehicle pickup. The Company maintains a fleet insurance policy covering the vehicle; however, Renter's personal insurance is primary for liability arising from Renter's operation of the vehicle. Renter accepts financial responsibility for any deductible, damages, or losses not covered by either policy.</p>
<h2>Vehicle Use & Rules</h2>
<p>Renter agrees to operate the Polaris Slingshot in a safe, lawful manner and specifically agrees to:</p>
<ul style="color:#374151;font-size:0.92rem;padding-left:1.25rem;margin-bottom:0.75rem;">
<li style="margin-bottom:0.35rem;">Obey all applicable traffic laws and speed limits</li>
<li style="margin-bottom:0.35rem;">Never operate the vehicle under the influence of alcohol, drugs, or any impairing substance</li>
<li style="margin-bottom:0.35rem;">Never allow an unauthorized third party to operate the vehicle</li>
<li style="margin-bottom:0.35rem;">Wear the provided DOT-approved helmet at all times while operating the vehicle</li>
<li style="margin-bottom:0.35rem;">Not take the vehicle off paved roads or outside the approved driving area</li>
<li>Return the vehicle at the agreed-upon time and location in the same condition it was received</li>
</ul>
<h2>Damage & Security Deposit</h2>
<p>A refundable security deposit is required at pickup. Renter is financially responsible for any damage to the vehicle, including but not limited to collision damage, tire damage, interior damage, and any fines or citations incurred during the rental period. The Company reserves the right to apply the security deposit toward any such costs.</p>
<h2>Assumption of Risk & Release of Liability</h2>
<p>Renter acknowledges that operating a Polaris Slingshot involves inherent risks including, but not limited to, physical injury or death. Renter voluntarily assumes all such risks and, to the fullest extent permitted by Texas law, releases and holds harmless Parker County Slingshot Rentals, its owners, employees, and agents from any and all claims, damages, or liability arising out of Renter's use of the vehicle.</p>
<h2>Cancellation Policy</h2>
<p>Cancellations made more than 24 hours before the rental start time are fully refunded. Cancellations within 24 hours of the rental start time are subject to a 50% cancellation fee.</p>
</div>
<form method="post" action="/waiver.php?ref=<?= urlencode($ref) ?>" id="waiverForm">
<input type="hidden" name="sig_data" id="sigDataInput" />
<div class="card">
<h2 style="margin-top:0">Acknowledgments</h2>
<p style="margin-bottom:1rem;color:#6b7280;font-size:0.88rem;">Check each box to confirm you have read and agree to that section.</p>
<div class="clause">
<input type="checkbox" name="checks[]" value="age" id="ck_age" required />
<label for="ck_age"><strong>Age & License:</strong> I confirm I am 25 years of age or older and hold a valid driver's license.</label>
</div>
<div class="clause">
<input type="checkbox" name="checks[]" value="insurance" id="ck_ins" required />
<label for="ck_ins"><strong>Insurance:</strong> I will provide proof of valid personal auto insurance at pickup and understand it is required.</label>
</div>
<div class="clause">
<input type="checkbox" name="checks[]" value="rules" id="ck_rules" required />
<label for="ck_rules"><strong>Vehicle Rules:</strong> I agree to operate the Slingshot safely, lawfully, and sober, and to wear the provided helmet at all times.</label>
</div>
<div class="clause">
<input type="checkbox" name="checks[]" value="damage" id="ck_dmg" required />
<label for="ck_dmg"><strong>Damage Responsibility:</strong> I understand I am financially responsible for any damage or fines incurred during my rental period.</label>
</div>
<div class="clause">
<input type="checkbox" name="checks[]" value="license" id="ck_lic" required />
<label for="ck_lic"><strong>License Verification:</strong> I consent to Parker County Slingshot Rentals verifying my driver's license at pickup.</label>
</div>
<div class="clause">
<input type="checkbox" name="checks[]" value="waiver" id="ck_waiver" required />
<label for="ck_waiver"><strong>Assumption of Risk:</strong> I have read and understand the Assumption of Risk and Release of Liability section and agree to its terms.</label>
</div>
</div>
<div class="card">
<h2 style="margin-top:0">Your Signature</h2>
<label style="font-weight:600;font-size:0.9rem;display:block;margin-bottom:0.2rem;">Full Legal Name</label>
<input type="text" name="sig_name" id="sigName" placeholder="Type your full name as it appears on your license" required />
<p style="margin:1.25rem 0 0.4rem;font-weight:600;font-size:0.9rem;">Draw Your Signature</p>
<div class="sig-wrap">
<canvas id="sigCanvas"></canvas>
<button type="button" class="sig-clear" id="clearBtn">Clear</button>
</div>
<p class="sig-hint">Use your mouse or finger to sign in the box above.</p>
<p style="margin-top:1.5rem;font-size:0.82rem;color:#9ca3af;">
By clicking "Sign &amp; Submit" below, you confirm that you have read this entire Rental Agreement, that all acknowledgments above are checked, and that your typed name and drawn signature constitute a legally binding electronic signature under the ESIGN Act and Texas law. Signed: <span id="sigDateDisplay"><?= date('F j, Y') ?></span>.
</p>
<button type="submit" class="btn-sign" id="submitBtn">Sign &amp; Submit Rental Agreement</button>
</div>
</form>
<?php endif; ?>
</div><!-- /.wrap -->
<script>
(function() {
const canvas = document.getElementById('sigCanvas');
if (!canvas) return;
// Size canvas to its CSS width
function resizeCanvas() {
const rect = canvas.getBoundingClientRect();
canvas.width = rect.width * window.devicePixelRatio;
canvas.height = rect.height * window.devicePixelRatio;
canvas.getContext('2d').scale(window.devicePixelRatio, window.devicePixelRatio);
}
resizeCanvas();
const ctx = canvas.getContext('2d');
let drawing = false;
let hasDrawn = false;
ctx.strokeStyle = '#111';
ctx.lineWidth = 2.2;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
function getPos(e) {
const r = canvas.getBoundingClientRect();
const src = e.touches ? e.touches[0] : e;
return { x: src.clientX - r.left, y: src.clientY - r.top };
}
function startDraw(e) {
e.preventDefault();
drawing = true;
const p = getPos(e);
ctx.beginPath();
ctx.moveTo(p.x, p.y);
}
function draw(e) {
if (!drawing) return;
e.preventDefault();
const p = getPos(e);
ctx.lineTo(p.x, p.y);
ctx.stroke();
hasDrawn = true;
}
function endDraw() { drawing = false; }
canvas.addEventListener('mousedown', startDraw);
canvas.addEventListener('mousemove', draw);
canvas.addEventListener('mouseup', endDraw);
canvas.addEventListener('mouseleave', endDraw);
canvas.addEventListener('touchstart', startDraw, { passive: false });
canvas.addEventListener('touchmove', draw, { passive: false });
canvas.addEventListener('touchend', endDraw);
document.getElementById('clearBtn').addEventListener('click', () => {
ctx.clearRect(0, 0, canvas.width, canvas.height);
hasDrawn = false;
document.getElementById('sigDataInput').value = '';
});
document.getElementById('waiverForm').addEventListener('submit', function(e) {
if (!hasDrawn) {
e.preventDefault();
alert('Please draw your signature before submitting.');
canvas.style.borderColor = '#ef4444';
return;
}
// Export canvas to base64 PNG
document.getElementById('sigDataInput').value = canvas.toDataURL('image/png');
});
})();
</script>
</body>
</html>