<?php
/**
* Bothnia Cafe - Pöytävaraus (Pe/La) 16-18, 17-19, 18-20 + 2/3 hlö
* Shortcode: [bothnia_table_booking]
* Features:
* - Only Fri/Sat selectable
* - Time slots (auto-hide past slots for today, Europe/Helsinki)
* - Guests only 2 or 3
* - Email to info@bothniacafe.com
* - Reply-To set to customer
* - GDPR consent checkbox
* - Anti-spam: honeypot + simple rate limit by IP (transients)
* - Auto-reply to customer
*/
add_shortcode('bothnia_table_booking', 'bothnia_table_booking_shortcode');
function bothnia_table_booking_shortcode() {
$result_html = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['bothnia_booking_submit'])) {
$result_html = bothnia_handle_booking_submit();
}
ob_start();
?>
<style>
.bothnia-booking{max-width:520px}
.bothnia-booking input,.bothnia-booking select,.bothnia-booking textarea{width:100%;padding:10px 12px;border:1px solid #ccc;border-radius:12px;font-size:16px}
.bothnia-booking button{padding:10px 14px;border:0;border-radius:12px;cursor:pointer}
.bothnia-msg{padding:10px 12px;border-radius:12px;margin:12px 0}
.bothnia-ok{border:1px solid #b7ebc6;background:#f0fff4}
.bothnia-err{border:1px solid #f4b8b8;background:#fff5f5}
.bothnia-small{font-size:13px;opacity:.8}
</style>
<div class="bothnia-booking">
<h3>Pöytävaraus</h3>
<?php echo $result_html; ?>
<form method="post" class="bothnia-booking__form" autocomplete="on">
<?php wp_nonce_field('bothnia_booking_action', 'bothnia_booking_nonce'); ?>
<!-- Honeypot (bots tend to fill this). Hidden from humans -->
<div style="position:absolute;left:-9999px;top:-9999px;height:0;overflow:hidden;">
<label for="bb_company">Yritys</label>
<input id="bb_company" name="bb_company" type="text" value="" tabindex="-1" autocomplete="off">
</div>
<label for="bb_date"><strong>Päivämäärä (vain perjantai tai lauantai)</strong></label><br>
<input id="bb_date" name="bb_date" type="date" required><br><br>
<label for="bb_slot"><strong>Aika</strong></label><br>
<select id="bb_slot" name="bb_slot" required disabled>
<option value="">Valitse ensin päivä</option>
</select><br><br>
<label for="bb_guests"><strong>Henkilömäärä</strong></label><br>
<select id="bb_guests" name="bb_guests" required>
<option value="2">2</option>
<option value="3">3</option>
</select><br><br>
<label for="bb_name"><strong>Nimi</strong></label><br>
<input id="bb_name" name="bb_name" type="text" required placeholder="Esim. Ilia"><br><br>
<label for="bb_phone"><strong>Puhelin</strong></label><br>
<input id="bb_phone" name="bb_phone" type="tel" required placeholder="+358..."><br><br>
<label for="bb_email"><strong>Sähköposti</strong></label><br>
<input id="bb_email" name="bb_email" type="email" required placeholder="esim. nimi@email.com"><br><br>
<label for="bb_notes"><strong>Lisätiedot (valinnainen)</strong></label><br>
<textarea id="bb_notes" name="bb_notes" rows="3" placeholder="Toiveet, allergiat jne."></textarea><br><br>
<label style="display:flex; gap:10px; align-items:flex-start;">
<input type="checkbox" name="bb_consent" value="1" required style="margin-top:3px;">
<span>Hyväksyn, että Bothnia Cafe käsittelee tietojani varauspyynnön käsittelyä ja yhteydenottoa varten.</span>
</label><br>
<button type="submit" name="bothnia_booking_submit">Lähetä varauspyyntö</button>
</form>
<p class="bothnia-small" style="margin-top:10px;">
Huom: tämä on varauspyyntö — vahvistamme varauksen erikseen.
</p>
</div>
<script>
(function(){
const dateEl = document.getElementById('bb_date');
const slotEl = document.getElementById('bb_slot');
const slots = [
{ value: '16-18', label: '16:00–18:00', startH: 16 },
{ value: '17-19', label: '17:00–19:00', startH: 17 },
{ value: '18-20', label: '18:00–20:00', startH: 18 },
];
const TZ = 'Europe/Helsinki';
function getHelsinkiNow(){
const fmt = new Intl.DateTimeFormat('en-CA', {
timeZone: TZ,
year: 'numeric', month: '2-digit', day: '2-digit',
hour: '2-digit', minute: '2-digit',
hour12: false
});
const parts = fmt.formatToParts(new Date());
const map = {};
parts.forEach(p => { if (p.type !== 'literal') map[p.type] = p.value; });
const hour = parseInt(map.hour, 10);
const minute = parseInt(map.minute, 10);
return {
ymd: `${map.year}-${map.month}-${map.day}`,
minutesTotal: hour * 60 + minute
};
}
function isFriOrSat(dateStr){
const d = new Date(dateStr + 'T12:00:00');
const day = d.getDay(); // 5=Fri 6=Sat
return day === 5 || day === 6;
}
function resetSlots(text){
slotEl.innerHTML = '';
const o = document.createElement('option');
o.value = '';
o.textContent = text;
slotEl.appendChild(o);
slotEl.disabled = true;
}
function renderSlots(dateStr){
const now = getHelsinkiNow();
const isToday = (dateStr === now.ymd);
slotEl.innerHTML = '';
const p = document.createElement('option');
p.value = '';
p.textContent = 'Valitse aika';
slotEl.appendChild(p);
let any = false;
slots.forEach(s => {
if (isToday) {
const startMinutes = s.startH * 60;
if (now.minutesTotal >= startMinutes) return; // hide past slots
}
const o = document.createElement('option');
o.value = s.value;
o.textContent = s.label;
slotEl.appendChild(o);
any = true;
});
if (!any) {
resetSlots('Tälle päivälle ei ole enää vapaita aikavälejä');
return;
}
slotEl.disabled = false;
}
dateEl.addEventListener('change', () => {
if(!dateEl.value){
resetSlots('Valitse ensin päivä');
return;
}
if(!isFriOrSat(dateEl.value)){
resetSlots('Vain perjantai/lauantai');
return;
}
renderSlots(dateEl.value);
});
resetSlots('Valitse ensin päivä');
})();
</script>
<?php
return ob_get_clean();
}
function bothnia_handle_booking_submit() {
// Nonce
if (!isset($_POST['bothnia_booking_nonce']) || !wp_verify_nonce($_POST['bothnia_booking_nonce'], 'bothnia_booking_action')) {
return bothnia_err('Virhe: lomakkeen suojaus epäonnistui. Päivitä sivu ja yritä uudelleen.');
}
// Honeypot (should be empty)
$honeypot = isset($_POST['bb_company']) ? sanitize_text_field($_POST['bb_company']) : '';
if (!empty($honeypot)) {
return bothnia_err('Varauspyynnön lähetys epäonnistui.'); // intentionally vague
}
// Rate limit (by IP): max 5 submissions / 30 minutes
$ip = isset($_SERVER['REMOTE_ADDR']) ? sanitize_text_field($_SERVER['REMOTE_ADDR']) : 'unknown';
$rate_key = 'bothnia_booking_rate_' . md5($ip);
$count = (int) get_transient($rate_key);
if ($count >= 5) {
return bothnia_err('Liian monta pyyntöä lyhyessä ajassa. Yritä myöhemmin uudelleen.');
}
set_transient($rate_key, $count + 1, 30 * MINUTE_IN_SECONDS);
// Consent
$consent = isset($_POST['bb_consent']) ? sanitize_text_field($_POST['bb_consent']) : '';
if ($consent !== '1') {
return bothnia_err('Hyväksy tietojen käsittely varauspyyntöä varten.');
}
// Sanitize
$date = isset($_POST['bb_date']) ? sanitize_text_field($_POST['bb_date']) : '';
$slot = isset($_POST['bb_slot']) ? sanitize_text_field($_POST['bb_slot']) : '';
$guests = isset($_POST['bb_guests']) ? sanitize_text_field($_POST['bb_guests']) : '';
$name = isset($_POST['bb_name']) ? sanitize_text_field($_POST['bb_name']) : '';
$phone = isset($_POST['bb_phone']) ? sanitize_text_field($_POST['bb_phone']) : '';
$email = isset($_POST['bb_email']) ? sanitize_email($_POST['bb_email']) : '';
$notes = isset($_POST['bb_notes']) ? sanitize_textarea_field($_POST['bb_notes']) : '';
// Validate date format
if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $date)) {
return bothnia_err('Virhe: päivämäärä ei kelpaa.');
}
// Validate Fri/Sat
$ts = strtotime($date . ' 12:00:00');
if ($ts === false) return bothnia_err('Virhe: päivämäärä ei kelpaa.');
$day = (int) date('w', $ts); // 5=Fri, 6=Sat
if (!in_array($day, [5,6], true)) {
return bothnia_err('Varaus on mahdollista vain perjantaina tai lauantaina.');
}
// Validate slot + guests
$allowed_slots = ['16-18' => '16:00–18:00', '17-19' => '17:00–19:00', '18-20' => '18:00–20:00'];
if (!isset($allowed_slots[$slot])) {
return bothnia_err('Valitse sallittu aikaväli (16–18, 17–19 tai 18–20).');
}
if (!in_array($guests, ['2','3'], true)) {
return bothnia_err('Henkilömäärä voi olla vain 2 tai 3.');
}
if (mb_strlen($name) < 2) return bothnia_err('Kirjoita nimi.');
if (mb_strlen($phone) < 5) return bothnia_err('Kirjoita puhelinnumero.');
if (!is_email($email)) return bothnia_err('Kirjoita kelvollinen sähköpostiosoite.');
// Prepare email (to cafe)
$to = 'info@bothniacafe.com';
$subject = 'Uusi pöytävarauspyyntö (Bothnia Cafe)';
$slot_pretty = $allowed_slots[$slot];
$body =
"Uusi pöytävarauspyyntö\n\n" .
"Päivämäärä: {$date}\n" .
"Aika: {$slot_pretty}\n" .
"Henkilömäärä: {$guests}\n" .
"Nimi: {$name}\n" .
"Puhelin: {$phone}\n" .
"Sähköposti: {$email}\n" .
"Lisätiedot: " . ($notes ? $notes : '-') . "\n\n" .
"Lähetetty sivustolta: " . home_url() . "\n" .
"IP: " . $ip . "\n";
// From should be your domain email for best deliverability
$from_name = 'Bothnia Cafe';
$from_email = 'info@bothniacafe.com';
$headers_to_cafe = [
'Content-Type: text/plain; charset=UTF-8',
'From: ' . $from_name . ' <' . $from_email . '>',
'Reply-To: ' . $name . ' <' . $email . '>',
];
$sent_to_cafe = wp_mail($to, $subject, $body, $headers_to_cafe);
if (!$sent_to_cafe) {
return bothnia_err('Varauspyynnön lähetys epäonnistui. Yritä uudelleen tai soita meille.');
}
// Auto-reply to customer
$cust_subject = 'Kiitos varauspyynnöstäsi – Bothnia Cafe';
$cust_body =
"Hei " . $name . "!\n\n" .
"Kiitos varauspyynnöstäsi. Tässä yhteenveto:\n\n" .
"Päivämäärä: {$date}\n" .
"Aika: {$slot_pretty}\n" .
"Henkilömäärä: {$guests}\n\n" .
"Tämä viesti on varauspyynnön vastaanottokuittaus. Vahvistamme varauksen erikseen mahdollisimman pian.\n\n" .
"Ystävällisin terveisin,\n" .
"Bothnia Cafe\n";
$headers_to_customer = [
'Content-Type: text/plain; charset=UTF-8',
'From: ' . $from_name . ' <' . $from_email . '>',
];
// We won't fail the whole flow if autoresponder fails, but it's nice to know (optional).
wp_mail($email, $cust_subject, $cust_body, $headers_to_customer);
return '<div class="bothnia-msg bothnia-ok">
Kiitos! Varauspyyntö on lähetetty. Otamme sinuun yhteyttä mahdollisimman pian.
</div>';
}
function bothnia_err($msg) {
return '<div class="bothnia-msg bothnia-err">'.$msg.'</div>';
}