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:
2026-05-22 13:39:20 +00:00
parent c2ab75f97d
commit 2ecf8f04c4
6 changed files with 662 additions and 114 deletions
+163 -9
View File
@@ -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">&#8249;</button>
<h3 id="calTitle">Loading&hellip;</h3>
<button class="cal-nav" id="calNext" aria-label="Next month">&#8250;</button>
</div>
<div class="cal-grid" id="calGrid">
<div class="cal-loading" style="grid-column:1/-1">Checking availability&hellip;</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&hellip;</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.');
}