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:
+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