create events api wp
This commit is contained in:
@@ -0,0 +1,257 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/bootstrap.php';
|
||||
require_once dirname(__DIR__) . '/include/event_waitlist.php';
|
||||
|
||||
/**
|
||||
* Disdetta pubblica di una prenotazione tramite token (link email, senza login).
|
||||
*
|
||||
* GET apiwp/cancel.php?token=... -> mostra pagina di conferma (NON annulla:
|
||||
* evita che gli scanner email prefetchino il link e annullino da soli)
|
||||
* POST (token + confirm=1) -> annulla e, se la prenotazione occupava posti,
|
||||
* promuove automaticamente la prima in lista d'attesa.
|
||||
*/
|
||||
$pdo = pdo_conn();
|
||||
$token = trim($_POST['token'] ?? $_GET['token'] ?? '');
|
||||
$isPost = ($_SERVER['REQUEST_METHOD'] ?? 'GET') === 'POST';
|
||||
|
||||
$booking = null;
|
||||
if ($token !== '') {
|
||||
$q = $pdo->prepare("SELECT b.*, e.title AS ev_title, e.starts_at AS ev_starts, e.id AS eid
|
||||
FROM event_bookings b JOIN events e ON e.id = b.event_id
|
||||
WHERE b.token = ?");
|
||||
$q->execute([$token]);
|
||||
$booking = $q->fetch(PDO::FETCH_ASSOC);
|
||||
}
|
||||
|
||||
$state = 'form'; // form | notfound | already | toolate | done
|
||||
$promotedCount = 0;
|
||||
|
||||
if (!$booking) {
|
||||
$state = 'notfound';
|
||||
} elseif ($booking['status'] === 'cancelled') {
|
||||
$state = 'already';
|
||||
} elseif (strtotime($booking['ev_starts']) <= time()) {
|
||||
$state = 'toolate';
|
||||
} elseif ($isPost && !empty($_POST['confirm'])) {
|
||||
try {
|
||||
$pdo->beginTransaction();
|
||||
// Lock evento per serializzare con eventuali prenotazioni concorrenti
|
||||
$pdo->prepare("SELECT id FROM events WHERE id = ? FOR UPDATE")->execute([(int) $booking['eid']]);
|
||||
|
||||
// Rileggo lo stato aggiornato della prenotazione
|
||||
$bl = $pdo->prepare("SELECT * FROM event_bookings WHERE id = ?");
|
||||
$bl->execute([(int) $booking['id']]);
|
||||
$fresh = $bl->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if ($fresh && $fresh['status'] !== 'cancelled') {
|
||||
$wasOccupying = in_array($fresh['status'], ['confirmed', 'pending'], true);
|
||||
$pdo->prepare("UPDATE event_bookings SET status = 'cancelled', updated_at = NOW() WHERE id = ?")
|
||||
->execute([(int) $fresh['id']]);
|
||||
|
||||
if ($wasOccupying) {
|
||||
$promoted = promoteFromWaitlist($pdo, (int) $booking['eid']);
|
||||
$promotedCount = count($promoted);
|
||||
// TODO Step 4: email "sei stato promosso" ai $promoted
|
||||
}
|
||||
}
|
||||
$pdo->commit();
|
||||
$state = 'done';
|
||||
} catch (Throwable $ex) {
|
||||
if ($pdo->inTransaction()) {
|
||||
$pdo->rollBack();
|
||||
}
|
||||
error_log('[apiwp/cancel] ' . $ex->getMessage());
|
||||
$state = 'error';
|
||||
}
|
||||
}
|
||||
|
||||
function e($v): string
|
||||
{
|
||||
return htmlspecialchars((string) $v, ENT_QUOTES, 'UTF-8');
|
||||
}
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="it">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Disdetta prenotazione — YogaSoul</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,500;9..144,600&family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
--accent: #b07aa1;
|
||||
--ink: #2b2622;
|
||||
--muted: #8c837c;
|
||||
--line: #ece7e2;
|
||||
--paper: #faf8f6;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
background: var(--paper);
|
||||
color: var(--ink);
|
||||
font-family: 'Inter', system-ui, sans-serif;
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: #fff;
|
||||
border-radius: 18px;
|
||||
box-shadow: 0 14px 40px rgba(43, 38, 34, .10);
|
||||
max-width: 460px;
|
||||
width: 100%;
|
||||
padding: 34px 30px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.icn {
|
||||
width: 58px;
|
||||
height: 58px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0 auto 16px;
|
||||
}
|
||||
|
||||
.icn.warn {
|
||||
background: #fdecea;
|
||||
color: #c0392b;
|
||||
}
|
||||
|
||||
.icn.ok {
|
||||
background: #e7f7ee;
|
||||
color: #1a7f52;
|
||||
}
|
||||
|
||||
.icn.info {
|
||||
background: #eef0f3;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-family: 'Fraunces', serif;
|
||||
font-weight: 600;
|
||||
font-size: 23px;
|
||||
margin: 0 0 8px;
|
||||
}
|
||||
|
||||
p {
|
||||
color: #4a433d;
|
||||
font-size: 15px;
|
||||
margin: 6px 0;
|
||||
}
|
||||
|
||||
.ev {
|
||||
background: var(--paper);
|
||||
border-radius: 12px;
|
||||
padding: 14px 16px;
|
||||
margin: 18px 0;
|
||||
}
|
||||
|
||||
.ev .t {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.ev .d {
|
||||
color: var(--muted);
|
||||
font-size: 14px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
border: none;
|
||||
border-radius: 11px;
|
||||
padding: 14px 20px;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
font-family: inherit;
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: #e74c3c;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
filter: brightness(.94);
|
||||
}
|
||||
|
||||
.btn-light {
|
||||
background: #f3f4f6;
|
||||
color: #374151;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.muted {
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
margin-top: 14px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="card">
|
||||
<?php if ($state === 'notfound') : ?>
|
||||
<div class="icn warn">✕</div>
|
||||
<h1>Link non valido</h1>
|
||||
<p>Questa prenotazione non esiste o il link è errato.</p>
|
||||
|
||||
<?php elseif ($state === 'already') : ?>
|
||||
<div class="icn info">✓</div>
|
||||
<h1>Già annullata</h1>
|
||||
<p>Questa prenotazione risulta già annullata.</p>
|
||||
|
||||
<?php elseif ($state === 'toolate') : ?>
|
||||
<div class="icn info">⏱</div>
|
||||
<h1>Non più annullabile</h1>
|
||||
<p>L'evento è già iniziato: non è più possibile annullare online. Contattaci per assistenza.</p>
|
||||
|
||||
<?php elseif ($state === 'error') : ?>
|
||||
<div class="icn warn">!</div>
|
||||
<h1>Si è verificato un errore</h1>
|
||||
<p>Riprova tra poco. Se il problema persiste, contattaci.</p>
|
||||
|
||||
<?php elseif ($state === 'done') : ?>
|
||||
<div class="icn ok">✓</div>
|
||||
<h1>Prenotazione annullata</h1>
|
||||
<p>La tua prenotazione per <strong><?php echo e($booking['ev_title']); ?></strong> è stata annullata.</p>
|
||||
<?php if ($promotedCount > 0) : ?>
|
||||
<p class="muted">Un posto è stato assegnato alla prima persona in lista d'attesa.</p>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php else : /* form */ ?>
|
||||
<div class="icn warn">?</div>
|
||||
<h1>Annullare la prenotazione?</h1>
|
||||
<div class="ev">
|
||||
<div class="t"><?php echo e($booking['ev_title']); ?></div>
|
||||
<div class="d"><?php echo e(date('d/m/Y H:i', strtotime($booking['ev_starts']))); ?> · <?php echo (int) $booking['seats']; ?> post<?php echo (int) $booking['seats'] === 1 ? 'o' : 'i'; ?><?php echo $booking['status'] === 'waiting' ? ' (in lista d\'attesa)' : ''; ?></div>
|
||||
</div>
|
||||
<p>Confermi di voler annullare la prenotazione a nome di <strong><?php echo e($booking['name']); ?></strong>?</p>
|
||||
<form method="POST">
|
||||
<input type="hidden" name="token" value="<?php echo e($token); ?>">
|
||||
<input type="hidden" name="confirm" value="1">
|
||||
<button type="submit" class="btn btn-danger">Sì, annulla la prenotazione</button>
|
||||
</form>
|
||||
<p class="muted">Se hai cambiato idea, chiudi pure questa pagina.</p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -67,6 +67,7 @@ $val = [
|
||||
'what_to_bring' => $ev['what_to_bring'] ?? '',
|
||||
'suitable_for' => $ev['suitable_for'] ?? '',
|
||||
'gallery' => $ev['gallery'] ?? '',
|
||||
'waitlist_enabled' => ($ev['waitlist_enabled'] ?? 'Y') === 'Y',
|
||||
];
|
||||
|
||||
$isEdit = $id > 0;
|
||||
@@ -531,7 +532,7 @@ $pageTitle = $isEdit ? 'Modifica evento' : 'Nuovo evento';
|
||||
<label class="form-label">Colore card</label>
|
||||
<input type="color" class="form-control" name="color" value="<?php echo e($val['color']); ?>">
|
||||
</div>
|
||||
<div class="col-md-9 mb-3">
|
||||
<div class="col-md-5 mb-3">
|
||||
<label class="form-label">Stato</label>
|
||||
<select name="status" class="form-select">
|
||||
<option value="draft" <?php echo $val['status'] === 'draft' ? 'selected' : ''; ?>>Bozza</option>
|
||||
@@ -539,6 +540,13 @@ $pageTitle = $isEdit ? 'Modifica evento' : 'Nuovo evento';
|
||||
<option value="cancelled" <?php echo $val['status'] === 'cancelled' ? 'selected' : ''; ?>>Annullato</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4 mb-3">
|
||||
<label class="form-label">Lista d'attesa</label>
|
||||
<div class="form-check form-switch" style="padding-top:8px;">
|
||||
<input class="form-check-input" type="checkbox" role="switch" id="waitlist_enabled" name="waitlist_enabled" value="Y" <?php echo $val['waitlist_enabled'] ? 'checked' : ''; ?>>
|
||||
<label class="form-check-label" for="waitlist_enabled" style="font-weight:400;">Consenti iscrizione a evento pieno</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-between mt-2">
|
||||
|
||||
@@ -274,7 +274,7 @@ $isEdit = $editId > 0 && $edit;
|
||||
|
||||
<div class="row">
|
||||
<!-- Form -->
|
||||
<div class="col-lg-5">
|
||||
<div class="col-12">
|
||||
<div class="card admin-card mb-4">
|
||||
<div class="card-body">
|
||||
<div class="section-title"><?php echo $isEdit ? 'Modifica sede' : 'Nuova sede'; ?></div>
|
||||
@@ -327,7 +327,7 @@ $isEdit = $editId > 0 && $edit;
|
||||
</div>
|
||||
|
||||
<!-- Elenco -->
|
||||
<div class="col-lg-7">
|
||||
<div class="col-12">
|
||||
<div class="card admin-card">
|
||||
<div class="card-body">
|
||||
<div class="section-title">Sedi salvate</div>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<?php
|
||||
require_once('include/headscript.php');
|
||||
require_once('include/event_waitlist.php');
|
||||
|
||||
/**
|
||||
* Modulo Eventi - elenco partecipanti di un evento.
|
||||
@@ -34,8 +35,30 @@ if ($eventId <= 0) {
|
||||
// Annullamento singola iscrizione (deve avvenire prima di ogni output)
|
||||
if (isset($_GET['cancel_booking'])) {
|
||||
$bookingId = (int) $_GET['cancel_booking'];
|
||||
$upd = $pdo->prepare("UPDATE event_bookings SET status = 'cancelled', updated_at = NOW() WHERE id = ? AND event_id = ?");
|
||||
$upd->execute([$bookingId, $eventId]);
|
||||
try {
|
||||
$pdo->beginTransaction();
|
||||
$pdo->prepare("SELECT id FROM events WHERE id = ? FOR UPDATE")->execute([$eventId]);
|
||||
|
||||
$bl = $pdo->prepare("SELECT status FROM event_bookings WHERE id = ? AND event_id = ?");
|
||||
$bl->execute([$bookingId, $eventId]);
|
||||
$bStatus = $bl->fetchColumn();
|
||||
|
||||
if ($bStatus !== false && $bStatus !== 'cancelled') {
|
||||
$wasOccupying = in_array($bStatus, ['confirmed', 'pending'], true);
|
||||
$pdo->prepare("UPDATE event_bookings SET status = 'cancelled', updated_at = NOW() WHERE id = ? AND event_id = ?")
|
||||
->execute([$bookingId, $eventId]);
|
||||
if ($wasOccupying) {
|
||||
// libera posti -> promuove la prima in lista d'attesa
|
||||
promoteFromWaitlist($pdo, $eventId);
|
||||
}
|
||||
}
|
||||
$pdo->commit();
|
||||
} catch (Throwable $ex) {
|
||||
if ($pdo->inTransaction()) {
|
||||
$pdo->rollBack();
|
||||
}
|
||||
error_log('[participants cancel] ' . $ex->getMessage());
|
||||
}
|
||||
header('Location: event_participants.php?id=' . $eventId . '&message=booking_cancelled');
|
||||
exit();
|
||||
}
|
||||
@@ -52,15 +75,18 @@ $bStmt = $pdo->prepare("SELECT * FROM event_bookings WHERE event_id = ? ORDER BY
|
||||
$bStmt->execute([$eventId]);
|
||||
$bookings = $bStmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// Riepilogo (solo prenotazioni non annullate)
|
||||
// Riepilogo: i posti occupati sono solo confermate/pending; la lista d'attesa a parte
|
||||
$bookedSeats = 0;
|
||||
$waitingSeats = 0;
|
||||
$activeCount = 0;
|
||||
$revenue = 0.0;
|
||||
foreach ($bookings as $b) {
|
||||
if ($b['status'] !== 'cancelled') {
|
||||
if (in_array($b['status'], ['confirmed', 'pending'], true)) {
|
||||
$bookedSeats += (int) $b['seats'];
|
||||
$activeCount++;
|
||||
$revenue += (float) $b['amount'];
|
||||
} elseif ($b['status'] === 'waiting') {
|
||||
$waitingSeats += (int) $b['seats'];
|
||||
}
|
||||
}
|
||||
$maxSeats = (int) $event['max_seats'];
|
||||
@@ -73,6 +99,8 @@ function bookingBadge(string $status): array
|
||||
return ['Confermata', '#e7f7ee', '#1a7f52'];
|
||||
case 'cancelled':
|
||||
return ['Annullata', '#fdecea', '#c0392b'];
|
||||
case 'waiting':
|
||||
return ["Lista d'attesa", '#eef2fb', '#3a5bb8'];
|
||||
default:
|
||||
return ['In attesa', '#fff4e5', '#b26a00'];
|
||||
}
|
||||
@@ -290,19 +318,25 @@ function bookingBadge(string $status): array
|
||||
</div>
|
||||
|
||||
<div class="row mt-4">
|
||||
<div class="col-md-4 mb-3">
|
||||
<div class="col-md-3 mb-3">
|
||||
<div class="stat-box">
|
||||
<div class="stat-value"><?php echo $bookedSeats; ?> / <?php echo $maxSeats; ?></div>
|
||||
<div class="stat-label">Posti prenotati</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4 mb-3">
|
||||
<div class="col-md-3 mb-3">
|
||||
<div class="stat-box">
|
||||
<div class="stat-value"><?php echo $waitingSeats; ?></div>
|
||||
<div class="stat-label">In lista d'attesa</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 mb-3">
|
||||
<div class="stat-box">
|
||||
<div class="stat-value"><?php echo $activeCount; ?></div>
|
||||
<div class="stat-label">Iscrizioni attive</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4 mb-3">
|
||||
<div class="col-md-3 mb-3">
|
||||
<div class="stat-box">
|
||||
<div class="stat-value"><?php echo e(number_format($revenue, 2, ',', '.')); ?> <?php echo e($event['currency']); ?></div>
|
||||
<div class="stat-label">Incasso stimato</div>
|
||||
|
||||
+35
-10
@@ -35,14 +35,17 @@ if (!$ev) {
|
||||
exit();
|
||||
}
|
||||
|
||||
$bs = $pdo->prepare("SELECT COALESCE(SUM(seats),0) FROM event_bookings WHERE event_id = ? AND status <> 'cancelled'");
|
||||
$bs = $pdo->prepare("SELECT COALESCE(SUM(seats),0) FROM event_bookings WHERE event_id = ? AND status IN ('confirmed','pending')");
|
||||
$bs->execute([$id]);
|
||||
$booked = (int) $bs->fetchColumn();
|
||||
$maxSeats = (int) $ev['max_seats'];
|
||||
$remaining = max(0, $maxSeats - $booked);
|
||||
$soldOut = $remaining <= 0;
|
||||
$maxPer = max(1, (int) $ev['max_per_booking']);
|
||||
$maxQty = $soldOut ? 0 : max(1, min($maxPer, $remaining));
|
||||
$waitlistOn = ($ev['waitlist_enabled'] ?? 'N') === 'Y';
|
||||
$waitMode = $soldOut && $waitlistOn; // pieno ma con lista d'attesa
|
||||
$hardFull = $soldOut && !$waitlistOn; // pieno senza lista d'attesa
|
||||
$maxQty = $soldOut ? ($waitMode ? $maxPer : 0) : max(1, min($maxPer, $remaining));
|
||||
|
||||
$cost = (float) $ev['cost'];
|
||||
$curr = $ev['currency'] ?: 'EUR';
|
||||
@@ -573,6 +576,15 @@ function textBlock(?string $t): string
|
||||
color: #c0392b;
|
||||
}
|
||||
|
||||
.waitlist-banner {
|
||||
background: color-mix(in srgb, var(--accent) 12%, white);
|
||||
color: var(--accent-ink);
|
||||
border-radius: 12px;
|
||||
padding: 12px 14px;
|
||||
font-size: 13.5px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.form-error {
|
||||
color: #c0392b;
|
||||
font-size: 13px;
|
||||
@@ -817,11 +829,14 @@ function textBlock(?string $t): string
|
||||
|
||||
<!-- BOOKING (compatto, passo unico) -->
|
||||
<div class="booking" id="booking">
|
||||
<?php if ($soldOut) : ?>
|
||||
<?php if ($hardFull) : ?>
|
||||
<div class="soldout-note">Evento esaurito — non ci sono più posti disponibili.</div>
|
||||
<?php else : ?>
|
||||
<?php if ($waitMode) : ?>
|
||||
<div class="waitlist-banner">Posti esauriti — puoi iscriverti alla <strong>lista d'attesa</strong>: ti avviseremo se si libera un posto.</div>
|
||||
<?php endif; ?>
|
||||
<div class="booking-compact">
|
||||
<div class="bc-title">Prenota questo evento</div>
|
||||
<div class="bc-title"><?php echo $waitMode ? "Lista d'attesa" : 'Prenota questo evento'; ?></div>
|
||||
<div class="bc-block">
|
||||
<div class="lbl">Persone</div>
|
||||
<div class="stepper">
|
||||
@@ -834,7 +849,7 @@ function textBlock(?string $t): string
|
||||
<div class="lbl">Totale</div>
|
||||
<div class="val" id="q-total"><?php echo e($priceLabel); ?></div>
|
||||
</div>
|
||||
<button type="button" class="cta compact" id="b-open">Prenota</button>
|
||||
<button type="button" class="cta compact" id="b-open"><?php echo $waitMode ? "Lista d'attesa" : 'Prenota'; ?></button>
|
||||
</div>
|
||||
|
||||
<div class="booking-form" id="booking-form">
|
||||
@@ -860,12 +875,15 @@ function textBlock(?string $t): string
|
||||
<input type="checkbox" id="b-marketing">
|
||||
<span>Desidero ricevere aggiornamenti e novità via email.</span>
|
||||
</label>
|
||||
<button type="button" class="cta full" id="b-submit">Conferma prenotazione</button>
|
||||
<button type="button" class="cta full" id="b-submit"><?php echo $waitMode ? "Iscriviti alla lista d'attesa" : 'Conferma prenotazione'; ?></button>
|
||||
<div class="form-error" id="b-error"></div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
var WAIT_MODE = <?php echo $waitMode ? 'true' : 'false'; ?>;
|
||||
</script>
|
||||
</div>
|
||||
|
||||
<div class="lightbox" id="lightbox">
|
||||
@@ -912,7 +930,13 @@ function textBlock(?string $t): string
|
||||
totEl.textContent = fmtMoney(qty * COST);
|
||||
minus.disabled = qty <= 1;
|
||||
plus.disabled = qty >= MAXQTY;
|
||||
if (submitBtn) submitBtn.textContent = 'Conferma prenotazione' + (COST > 0 ? ' · ' + fmtMoney(qty * COST) : '');
|
||||
if (submitBtn) {
|
||||
if (typeof WAIT_MODE !== 'undefined' && WAIT_MODE) {
|
||||
submitBtn.textContent = "Iscriviti alla lista d'attesa";
|
||||
} else {
|
||||
submitBtn.textContent = 'Conferma prenotazione' + (COST > 0 ? ' · ' + fmtMoney(qty * COST) : '');
|
||||
}
|
||||
}
|
||||
}
|
||||
minus.addEventListener('click', function() {
|
||||
if (qty > 1) {
|
||||
@@ -956,14 +980,15 @@ function textBlock(?string $t): string
|
||||
err.textContent = '';
|
||||
|
||||
var marketing = document.getElementById('b-marketing').checked;
|
||||
var wm = (typeof WAIT_MODE !== 'undefined' && WAIT_MODE);
|
||||
document.getElementById('booking').innerHTML =
|
||||
'<div class="booking-done">' +
|
||||
' <div class="ok"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" width="24" height="24"><path d="M20 6 9 17l-5-5"/></svg></div>' +
|
||||
' <h3>Anteprima prenotazione</h3>' +
|
||||
' <h3>' + (wm ? "Anteprima lista d'attesa" : 'Anteprima prenotazione') + '</h3>' +
|
||||
' <p><strong>' + name + ' ' + surname + '</strong> · ' + email + '</p>' +
|
||||
' <p>' + qty + ' ' + (qty === 1 ? 'persona' : 'persone') + ' · Totale <strong>' + fmtMoney(qty * COST) + '</strong></p>' +
|
||||
' <p>' + qty + ' ' + (qty === 1 ? 'persona' : 'persone') + (wm ? " · in lista d'attesa" : ' · Totale <strong>' + fmtMoney(qty * COST) + '</strong>') + '</p>' +
|
||||
' <p>Marketing: ' + (marketing ? 'sì' : 'no') + '</p>' +
|
||||
' <p class="hintline">Nel widget pubblico, qui parte la prenotazione reale (POST all\'endpoint).</p>' +
|
||||
' <p class="hintline">' + (wm ? "Nel widget pubblico, qui parte l'iscrizione alla lista d'attesa." : 'Nel widget pubblico, qui parte la prenotazione reale (POST all\'endpoint).') + '</p>' +
|
||||
'</div>';
|
||||
});
|
||||
})();
|
||||
|
||||
@@ -110,6 +110,8 @@ $instructions = trim($_POST['instructions'] ?? '');
|
||||
$whatToBring = trim($_POST['what_to_bring'] ?? '');
|
||||
$suitableFor = trim($_POST['suitable_for'] ?? '');
|
||||
|
||||
$waitlistEnabled = (isset($_POST['waitlist_enabled']) && $_POST['waitlist_enabled'] === 'Y') ? 'Y' : 'N';
|
||||
|
||||
// Galleria: array JSON di path -> validazione e re-encode pulito
|
||||
$gallery = null;
|
||||
$galleryRaw = $_POST['gallery'] ?? '';
|
||||
@@ -135,6 +137,7 @@ try {
|
||||
organizer_name = :organizer_name, starts_at = :starts_at, ends_at = :ends_at,
|
||||
address = :address, lat = :lat, lng = :lng, cost = :cost, currency = :currency,
|
||||
max_seats = :max_seats, max_per_booking = :max_per_booking, color = :color,
|
||||
waitlist_enabled = :waitlist_enabled,
|
||||
cover_image = :cover_image, gallery = :gallery, status = :status, updated_at = NOW()
|
||||
WHERE id = :id";
|
||||
$params = [':id' => $id];
|
||||
@@ -143,11 +146,13 @@ try {
|
||||
(user_id, title, slug, description, instructions, what_to_bring, suitable_for,
|
||||
organizer_name, starts_at, ends_at,
|
||||
address, lat, lng, cost, currency, max_seats, max_per_booking, color,
|
||||
waitlist_enabled,
|
||||
cover_image, gallery, status, created_at, updated_at)
|
||||
VALUES
|
||||
(:user_id, :title, :slug, :description, :instructions, :what_to_bring, :suitable_for,
|
||||
:organizer_name, :starts_at, :ends_at,
|
||||
:address, :lat, :lng, :cost, :currency, :max_seats, :max_per_booking, :color,
|
||||
:waitlist_enabled,
|
||||
:cover_image, :gallery, :status, NOW(), NOW())";
|
||||
$params = [':user_id' => $currentUserId];
|
||||
}
|
||||
@@ -169,6 +174,7 @@ try {
|
||||
':currency' => $currency,
|
||||
':max_seats' => $maxSeats,
|
||||
':max_per_booking' => $maxPerBooking,
|
||||
':waitlist_enabled' => $waitlistEnabled,
|
||||
':color' => $color,
|
||||
':cover_image' => $coverImage !== '' ? $coverImage : null,
|
||||
':gallery' => $gallery,
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Lista d'attesa — promozione automatica.
|
||||
*
|
||||
* IMPORTANTE: chiamare DENTRO una transazione in cui la riga dell'evento è già
|
||||
* bloccata (SELECT ... FOR UPDATE), tipicamente subito dopo aver liberato posti
|
||||
* (cancellazione di una prenotazione confermata). Così due operazioni simultanee
|
||||
* non promuovono oltre la capienza.
|
||||
*
|
||||
* Promuove in ordine di arrivo (FIFO) le prenotazioni 'waiting' che entrano nei
|
||||
* posti liberi e si ferma alla prima che non ci sta, per rispettare la coda
|
||||
* (non "salta" chi è davanti).
|
||||
*
|
||||
* @return array<int,array> le prenotazioni promosse (per inviare le email nello Step 4)
|
||||
*/
|
||||
if (!function_exists('promoteFromWaitlist')) {
|
||||
function promoteFromWaitlist(PDO $pdo, int $eventId): array
|
||||
{
|
||||
$ev = $pdo->prepare("SELECT max_seats FROM events WHERE id = ?");
|
||||
$ev->execute([$eventId]);
|
||||
$maxSeats = (int) $ev->fetchColumn();
|
||||
if ($maxSeats <= 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$os = $pdo->prepare("SELECT COALESCE(SUM(seats),0) FROM event_bookings
|
||||
WHERE event_id = ? AND status IN ('confirmed','pending')");
|
||||
$os->execute([$eventId]);
|
||||
$free = $maxSeats - (int) $os->fetchColumn();
|
||||
if ($free <= 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$ws = $pdo->prepare("SELECT * FROM event_bookings
|
||||
WHERE event_id = ? AND status = 'waiting'
|
||||
ORDER BY created_at ASC, id ASC");
|
||||
$ws->execute([$eventId]);
|
||||
$waiting = $ws->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$promoted = [];
|
||||
$upd = $pdo->prepare("UPDATE event_bookings SET status = 'confirmed', updated_at = NOW() WHERE id = ?");
|
||||
foreach ($waiting as $w) {
|
||||
$need = (int) $w['seats'];
|
||||
if ($need <= $free) {
|
||||
$upd->execute([$w['id']]);
|
||||
$free -= $need;
|
||||
$w['status'] = 'confirmed';
|
||||
$promoted[] = $w;
|
||||
if ($free <= 0) {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
break; // rispetta la coda
|
||||
}
|
||||
}
|
||||
return $promoted;
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@@ -0,0 +1,497 @@
|
||||
/* YogaSoul Eventi — stili widget (namespaced .yse-) */
|
||||
.yse-widget {
|
||||
--yse-ink: #2b2622;
|
||||
--yse-muted: #8c837c;
|
||||
--yse-line: #ece7e2;
|
||||
--yse-card: #ffffff;
|
||||
max-width: 720px;
|
||||
margin: 0 auto;
|
||||
font-family:
|
||||
"Inter",
|
||||
system-ui,
|
||||
-apple-system,
|
||||
sans-serif;
|
||||
color: var(--yse-ink);
|
||||
line-height: 1.55;
|
||||
}
|
||||
.yse-widget * {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.yse-empty {
|
||||
text-align: center;
|
||||
color: var(--yse-muted);
|
||||
padding: 26px;
|
||||
}
|
||||
|
||||
.yse-card {
|
||||
--yse-accent: #4caf50;
|
||||
--yse-accent-soft: color-mix(in srgb, var(--yse-accent) 10%, white);
|
||||
--yse-accent-ink: color-mix(in srgb, var(--yse-accent) 62%, black);
|
||||
background: var(--yse-card);
|
||||
border-radius: 18px;
|
||||
box-shadow: 0 14px 40px rgba(43, 38, 34, 0.1);
|
||||
overflow: hidden;
|
||||
}
|
||||
.yse-card + .yse-card {
|
||||
margin-top: 22px;
|
||||
}
|
||||
|
||||
/* Header compatto */
|
||||
.yse-chead {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
padding: 18px 22px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.yse-datechip {
|
||||
flex-shrink: 0;
|
||||
border: 1px solid var(--yse-line);
|
||||
border-radius: 12px;
|
||||
padding: 7px 11px 5px;
|
||||
text-align: center;
|
||||
min-width: 54px;
|
||||
}
|
||||
.yse-datechip .yse-m {
|
||||
font-size: 10.5px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.12em;
|
||||
color: var(--yse-accent-ink);
|
||||
}
|
||||
.yse-datechip .yse-d {
|
||||
font-family: "Fraunces", serif;
|
||||
font-size: 23px;
|
||||
font-weight: 600;
|
||||
color: var(--yse-ink);
|
||||
line-height: 1;
|
||||
}
|
||||
.yse-main {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.yse-title-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.yse-title-row h3 {
|
||||
font-family: "Fraunces", serif;
|
||||
font-weight: 500;
|
||||
font-size: 21px;
|
||||
margin: 0;
|
||||
color: var(--yse-ink);
|
||||
}
|
||||
.yse-toggle {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--yse-accent-ink);
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 2px;
|
||||
}
|
||||
.yse-toggle svg {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
.yse-toggle.yse-on svg {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
.yse-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px 20px;
|
||||
margin-top: 9px;
|
||||
}
|
||||
.yse-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
color: var(--yse-muted);
|
||||
font-size: 13.5px;
|
||||
}
|
||||
.yse-item svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
color: var(--yse-accent-ink);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.yse-item strong {
|
||||
color: var(--yse-ink);
|
||||
font-weight: 600;
|
||||
}
|
||||
.yse-price {
|
||||
flex-shrink: 0;
|
||||
align-self: flex-start;
|
||||
background: var(--yse-accent);
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
padding: 7px 13px;
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
/* Dettagli espandibili */
|
||||
.yse-details {
|
||||
display: none;
|
||||
border-top: 1px solid var(--yse-line);
|
||||
}
|
||||
.yse-details.yse-open {
|
||||
display: block;
|
||||
}
|
||||
.yse-hero {
|
||||
position: relative;
|
||||
height: 260px;
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
var(--yse-accent-ink),
|
||||
var(--yse-accent)
|
||||
);
|
||||
}
|
||||
.yse-hero img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
.yse-emblem {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: rgba(255, 255, 255, 0.35);
|
||||
}
|
||||
.yse-emblem svg {
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
}
|
||||
.yse-body {
|
||||
padding: 22px;
|
||||
}
|
||||
.yse-section {
|
||||
margin-top: 24px;
|
||||
}
|
||||
.yse-body h4 {
|
||||
font-family: "Fraunces", serif;
|
||||
font-weight: 600;
|
||||
font-size: 18px;
|
||||
color: var(--yse-ink);
|
||||
margin: 0 0 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 9px;
|
||||
}
|
||||
.yse-body h4 svg {
|
||||
width: 19px;
|
||||
height: 19px;
|
||||
color: var(--yse-accent-ink);
|
||||
}
|
||||
.yse-desc {
|
||||
font-size: 15px;
|
||||
color: #4a433d;
|
||||
}
|
||||
.yse-desc p {
|
||||
margin: 0 0 10px;
|
||||
}
|
||||
.yse-desc img {
|
||||
max-width: 100%;
|
||||
border-radius: 10px;
|
||||
}
|
||||
.yse-detail-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
|
||||
gap: 12px;
|
||||
margin-top: 22px;
|
||||
}
|
||||
.yse-detail {
|
||||
background: var(--yse-accent-soft);
|
||||
border: 1px solid color-mix(in srgb, var(--yse-accent) 18%, white);
|
||||
border-radius: 13px;
|
||||
padding: 14px 15px;
|
||||
}
|
||||
.yse-dh {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-weight: 600;
|
||||
color: var(--yse-accent-ink);
|
||||
margin-bottom: 6px;
|
||||
font-size: 13.5px;
|
||||
}
|
||||
.yse-dh svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
.yse-dt {
|
||||
font-size: 13.5px;
|
||||
color: #4a433d;
|
||||
}
|
||||
.yse-gallery {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.yse-gallery img {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
object-fit: cover;
|
||||
border-radius: 11px;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
transform 0.15s,
|
||||
box-shadow 0.15s;
|
||||
}
|
||||
.yse-gallery img:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 18px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
.yse-map {
|
||||
border: 0;
|
||||
width: 100%;
|
||||
height: 210px;
|
||||
border-radius: 13px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Booking */
|
||||
.yse-booking {
|
||||
padding: 18px 22px;
|
||||
border-top: 1px solid var(--yse-line);
|
||||
}
|
||||
.yse-soldout {
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
color: #c0392b;
|
||||
}
|
||||
.yse-wl-banner {
|
||||
background: color-mix(in srgb, var(--yse-accent) 12%, white);
|
||||
color: var(--yse-accent-ink);
|
||||
border-radius: 12px;
|
||||
padding: 12px 14px;
|
||||
font-size: 13.5px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
.yse-bcompact {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: 18px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.yse-btitle {
|
||||
font-family: "Fraunces", serif;
|
||||
font-weight: 600;
|
||||
font-size: 18px;
|
||||
color: var(--yse-ink);
|
||||
margin-right: auto;
|
||||
align-self: center;
|
||||
}
|
||||
.yse-lbl {
|
||||
font-size: 12px;
|
||||
color: var(--yse-muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.yse-stepper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border: 1px solid var(--yse-line);
|
||||
border-radius: 11px;
|
||||
overflow: hidden;
|
||||
background: #fff;
|
||||
}
|
||||
.yse-stepper button {
|
||||
border: none;
|
||||
background: #fff;
|
||||
width: 42px;
|
||||
height: 44px;
|
||||
font-size: 21px;
|
||||
color: var(--yse-accent-ink);
|
||||
cursor: pointer;
|
||||
line-height: 1;
|
||||
}
|
||||
.yse-stepper button:disabled {
|
||||
color: #cfc8c2;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.yse-num {
|
||||
width: 50px;
|
||||
text-align: center;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.yse-total .yse-val {
|
||||
font-family: "Fraunces", serif;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: var(--yse-accent-ink);
|
||||
}
|
||||
.yse-cta {
|
||||
border: none;
|
||||
border-radius: 11px;
|
||||
background: var(--yse-accent);
|
||||
color: #fff;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
font-family: inherit;
|
||||
cursor: pointer;
|
||||
transition: filter 0.15s;
|
||||
}
|
||||
.yse-cta:hover {
|
||||
filter: brightness(0.94);
|
||||
}
|
||||
.yse-cta:disabled {
|
||||
background: #cfc8c2;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.yse-open {
|
||||
padding: 13px 24px;
|
||||
}
|
||||
.yse-full {
|
||||
width: 100%;
|
||||
padding: 14px;
|
||||
margin-top: 6px;
|
||||
}
|
||||
.yse-bform {
|
||||
display: none;
|
||||
margin-top: 18px;
|
||||
padding-top: 18px;
|
||||
border-top: 1px dashed var(--yse-line);
|
||||
}
|
||||
.yse-bform.yse-open {
|
||||
display: block;
|
||||
}
|
||||
.yse-frow {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 12px;
|
||||
}
|
||||
.yse-field {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.yse-field label {
|
||||
display: block;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--yse-ink);
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.yse-field input {
|
||||
width: 100%;
|
||||
border: 1px solid var(--yse-line);
|
||||
border-radius: 10px;
|
||||
padding: 11px 13px;
|
||||
font-size: 15px;
|
||||
font-family: inherit;
|
||||
background: #fff;
|
||||
color: var(--yse-ink);
|
||||
}
|
||||
.yse-field input:focus {
|
||||
outline: none;
|
||||
border-color: var(--yse-accent);
|
||||
box-shadow: 0 0 0 3px color-mix(in srgb, var(--yse-accent) 22%, white);
|
||||
}
|
||||
.yse-consent {
|
||||
font-size: 13px;
|
||||
color: #4a433d;
|
||||
display: flex;
|
||||
gap: 9px;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 9px;
|
||||
}
|
||||
.yse-consent input {
|
||||
margin-top: 3px;
|
||||
}
|
||||
.yse-hp {
|
||||
position: absolute !important;
|
||||
left: -9999px !important;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
}
|
||||
.yse-error {
|
||||
color: #c0392b;
|
||||
font-size: 13px;
|
||||
margin-top: 8px;
|
||||
min-height: 18px;
|
||||
}
|
||||
.yse-done {
|
||||
text-align: center;
|
||||
padding: 8px 4px;
|
||||
}
|
||||
.yse-done .yse-ok {
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
border-radius: 50%;
|
||||
background: var(--yse-accent);
|
||||
color: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0 auto 10px;
|
||||
}
|
||||
.yse-done h3 {
|
||||
margin: 0 0 6px;
|
||||
font-family: "Fraunces", serif;
|
||||
}
|
||||
.yse-done p {
|
||||
color: #4a433d;
|
||||
font-size: 14px;
|
||||
margin: 2px 0;
|
||||
}
|
||||
|
||||
/* Lightbox */
|
||||
.yse-lightbox {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(24, 19, 16, 0.9);
|
||||
display: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 99999;
|
||||
padding: 24px;
|
||||
}
|
||||
.yse-lightbox.yse-open {
|
||||
display: flex;
|
||||
}
|
||||
.yse-lightbox img {
|
||||
max-width: 92vw;
|
||||
max-height: 86vh;
|
||||
border-radius: 12px;
|
||||
}
|
||||
.yse-lightbox .yse-lbclose {
|
||||
position: absolute;
|
||||
top: 18px;
|
||||
right: 22px;
|
||||
color: #fff;
|
||||
font-size: 34px;
|
||||
cursor: pointer;
|
||||
background: none;
|
||||
border: none;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
@media (max-width: 560px) {
|
||||
.yse-hero {
|
||||
height: 200px;
|
||||
}
|
||||
.yse-title-row h3 {
|
||||
font-size: 19px;
|
||||
}
|
||||
.yse-frow {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.yse-btitle {
|
||||
width: 100%;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.yse-widget * {
|
||||
transition: none !important;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,246 @@
|
||||
/* YogaSoul Eventi — interazioni widget */
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
function fmtMoney(v, sym) {
|
||||
if (v <= 0) return "Gratuito";
|
||||
return (
|
||||
sym +
|
||||
v.toLocaleString("it-IT", {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
function apiBase(el) {
|
||||
var w = el.closest(".yse-widget");
|
||||
return (w && w.getAttribute("data-api")) || "";
|
||||
}
|
||||
|
||||
function initCard(card) {
|
||||
var api = apiBase(card);
|
||||
var eid = parseInt(card.getAttribute("data-eid"), 10);
|
||||
var cost = parseFloat(card.getAttribute("data-cost")) || 0;
|
||||
var sym = card.getAttribute("data-sym") || "€";
|
||||
var maxQty = parseInt(card.getAttribute("data-maxqty"), 10) || 0;
|
||||
var waitMode = card.getAttribute("data-waitmode") === "1";
|
||||
|
||||
// Toggle dettagli
|
||||
var toggle = card.querySelector(".yse-toggle");
|
||||
if (toggle) {
|
||||
toggle.addEventListener("click", function () {
|
||||
var det = card.querySelector(".yse-details");
|
||||
if (!det) return;
|
||||
var open = det.classList.toggle("yse-open");
|
||||
toggle.classList.toggle("yse-on", open);
|
||||
toggle.setAttribute("aria-expanded", open ? "true" : "false");
|
||||
var lbl = toggle.querySelector(".yse-tlabel");
|
||||
if (lbl) lbl.textContent = open ? "Chiudi" : "Dettagli";
|
||||
});
|
||||
}
|
||||
|
||||
var booking = card.querySelector(".yse-booking");
|
||||
var numEl = card.querySelector(".yse-num");
|
||||
if (!numEl) return; // esaurito senza lista d'attesa: nessuna prenotazione
|
||||
|
||||
var totEl = card.querySelector(".yse-total .yse-val");
|
||||
var minus = card.querySelector(".yse-minus");
|
||||
var plus = card.querySelector(".yse-plus");
|
||||
var openBtn = card.querySelector(".yse-open");
|
||||
var form = card.querySelector(".yse-bform");
|
||||
var submit = card.querySelector(".yse-submit");
|
||||
var qty = 1;
|
||||
|
||||
function refresh() {
|
||||
numEl.textContent = qty;
|
||||
if (totEl) totEl.textContent = fmtMoney(qty * cost, sym);
|
||||
minus.disabled = qty <= 1;
|
||||
plus.disabled = qty >= maxQty;
|
||||
if (submit) {
|
||||
submit.textContent = waitMode
|
||||
? "Iscriviti alla lista d'attesa"
|
||||
: "Conferma prenotazione" +
|
||||
(cost > 0 ? " · " + fmtMoney(qty * cost, sym) : "");
|
||||
}
|
||||
}
|
||||
minus.addEventListener("click", function () {
|
||||
if (qty > 1) {
|
||||
qty--;
|
||||
refresh();
|
||||
}
|
||||
});
|
||||
plus.addEventListener("click", function () {
|
||||
if (qty < maxQty) {
|
||||
qty++;
|
||||
refresh();
|
||||
}
|
||||
});
|
||||
refresh();
|
||||
|
||||
openBtn.addEventListener("click", function () {
|
||||
form.classList.add("yse-open");
|
||||
openBtn.style.display = "none";
|
||||
var n = card.querySelector(".yse-name");
|
||||
if (n) n.focus();
|
||||
});
|
||||
|
||||
submit.addEventListener("click", function () {
|
||||
var name = card.querySelector(".yse-name").value.trim();
|
||||
var surname = card.querySelector(".yse-surname").value.trim();
|
||||
var email = card.querySelector(".yse-email").value.trim();
|
||||
var privacy = card.querySelector(".yse-c-privacy").checked;
|
||||
var mkt = card.querySelector(".yse-c-mkt").checked;
|
||||
var hp = card.querySelector(".yse-hp").value;
|
||||
var err = card.querySelector(".yse-error");
|
||||
|
||||
if (!name || !surname) {
|
||||
err.textContent = "Inserisci nome e cognome.";
|
||||
return;
|
||||
}
|
||||
if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email)) {
|
||||
err.textContent = "Inserisci un'email valida.";
|
||||
return;
|
||||
}
|
||||
if (!privacy) {
|
||||
err.textContent = "Devi accettare l'informativa privacy.";
|
||||
return;
|
||||
}
|
||||
err.textContent = "";
|
||||
|
||||
submit.disabled = true;
|
||||
submit.textContent = "Invio in corso…";
|
||||
|
||||
var body = new URLSearchParams();
|
||||
body.set("event_id", eid);
|
||||
body.set("name", name);
|
||||
body.set("surname", surname);
|
||||
body.set("email", email);
|
||||
body.set("seats", qty);
|
||||
body.set("consent", "1");
|
||||
body.set("marketing", mkt ? "1" : "0");
|
||||
body.set("website", hp); // honeypot
|
||||
|
||||
fetch(api + "/book.php", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type":
|
||||
"application/x-www-form-urlencoded;charset=UTF-8",
|
||||
},
|
||||
body: body.toString(),
|
||||
})
|
||||
.then(function (r) {
|
||||
return r.json().then(function (j) {
|
||||
return { status: r.status, j: j };
|
||||
});
|
||||
})
|
||||
.then(function (res) {
|
||||
var j = res.j || {};
|
||||
if (j.ok) {
|
||||
showDone(booking, j, name, surname, email);
|
||||
return;
|
||||
}
|
||||
// Errori gestiti
|
||||
submit.disabled = false;
|
||||
refresh();
|
||||
if (j.code === "partial") {
|
||||
err.textContent =
|
||||
j.error ||
|
||||
"Sono rimasti solo " + j.remaining + " posti.";
|
||||
if (
|
||||
typeof j.remaining === "number" &&
|
||||
j.remaining > 0
|
||||
) {
|
||||
maxQty = j.remaining;
|
||||
if (qty > maxQty) {
|
||||
qty = maxQty;
|
||||
refresh();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
err.textContent =
|
||||
j.error || "Prenotazione non riuscita. Riprova.";
|
||||
}
|
||||
})
|
||||
.catch(function () {
|
||||
submit.disabled = false;
|
||||
refresh();
|
||||
err.textContent = "Errore di rete. Riprova.";
|
||||
});
|
||||
});
|
||||
|
||||
function showDone(container, j, name, surname, email) {
|
||||
var waiting = j.status === "waiting";
|
||||
var title = waiting
|
||||
? "Sei in lista d'attesa"
|
||||
: "Prenotazione confermata!";
|
||||
var msg =
|
||||
j.message ||
|
||||
(waiting
|
||||
? "Ti avviseremo appena si libera un posto."
|
||||
: "Ti abbiamo inviato una email di riepilogo.");
|
||||
container.innerHTML =
|
||||
'<div class="yse-done">' +
|
||||
' <div class="yse-ok"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" width="24" height="24">' +
|
||||
' <path d="M20 6 9 17l-5-5"/></svg></div>' +
|
||||
" <h3>" +
|
||||
title +
|
||||
"</h3>" +
|
||||
" <p><strong>" +
|
||||
escapeHtml(name + " " + surname) +
|
||||
"</strong> · " +
|
||||
escapeHtml(email) +
|
||||
"</p>" +
|
||||
" <p>" +
|
||||
escapeHtml(msg) +
|
||||
"</p>" +
|
||||
"</div>";
|
||||
}
|
||||
}
|
||||
|
||||
function escapeHtml(s) {
|
||||
var d = document.createElement("div");
|
||||
d.textContent = s;
|
||||
return d.innerHTML;
|
||||
}
|
||||
|
||||
// Lightbox condiviso
|
||||
function ensureLightbox() {
|
||||
var lb = document.querySelector(".yse-lightbox");
|
||||
if (lb) return lb;
|
||||
lb = document.createElement("div");
|
||||
lb.className = "yse-lightbox";
|
||||
lb.innerHTML =
|
||||
'<button class="yse-lbclose" aria-label="Chiudi">×</button><img src="" alt="">';
|
||||
document.body.appendChild(lb);
|
||||
function close() {
|
||||
lb.classList.remove("yse-open");
|
||||
lb.querySelector("img").src = "";
|
||||
}
|
||||
lb.addEventListener("click", function (e) {
|
||||
if (e.target === lb || e.target.classList.contains("yse-lbclose"))
|
||||
close();
|
||||
});
|
||||
document.addEventListener("keydown", function (e) {
|
||||
if (e.key === "Escape") close();
|
||||
});
|
||||
return lb;
|
||||
}
|
||||
|
||||
function init() {
|
||||
document.querySelectorAll(".yse-card").forEach(initCard);
|
||||
document.querySelectorAll(".yse-gallery img").forEach(function (img) {
|
||||
img.addEventListener("click", function () {
|
||||
var lb = ensureLightbox();
|
||||
lb.querySelector("img").src = img.getAttribute("data-full");
|
||||
lb.classList.add("yse-open");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
})();
|
||||
@@ -0,0 +1,275 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Plugin Name: YogaSoul Eventi
|
||||
* Description: Mostra gli eventi di YogiBook con card di prenotazione (lista d'attesa inclusa). Shortcode: [yogasoul_eventi], [yogasoul_eventi ids="12,15"], [yogasoul_eventi id="12"], [yogasoul_eventi limit="3"].
|
||||
* Version: 1.0.0
|
||||
* Author: YogaSoul
|
||||
*/
|
||||
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Base dell'API YogiBook. Sovrascrivibile in wp-config.php con:
|
||||
* define('YSE_API_BASE', 'https://yogibook.yogasoul.it/apiwp');
|
||||
*/
|
||||
if (!defined('YSE_API_BASE')) {
|
||||
define('YSE_API_BASE', 'https://yogibook.yogasoul.it/apiwp');
|
||||
}
|
||||
|
||||
/* Registra gli asset (accodati solo quando lo shortcode è in pagina) */
|
||||
add_action('wp_enqueue_scripts', function () {
|
||||
wp_register_style(
|
||||
'yse-fonts',
|
||||
'https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,400;9..144,500;9..144,600&family=Inter:wght@400;500;600&display=swap',
|
||||
[],
|
||||
null
|
||||
);
|
||||
wp_register_style('yse-eventi', plugins_url('assets/yogasoul-eventi.css', __FILE__), ['yse-fonts'], '1.0.0');
|
||||
wp_register_script('yse-eventi', plugins_url('assets/yogasoul-eventi.js', __FILE__), [], '1.0.0', true);
|
||||
});
|
||||
|
||||
add_shortcode('yogasoul_eventi', 'yse_render_shortcode');
|
||||
|
||||
function yse_render_shortcode($atts)
|
||||
{
|
||||
$atts = shortcode_atts([
|
||||
'ids' => '',
|
||||
'id' => '',
|
||||
'limit' => '',
|
||||
], $atts, 'yogasoul_eventi');
|
||||
|
||||
wp_enqueue_style('yse-eventi');
|
||||
wp_enqueue_script('yse-eventi');
|
||||
|
||||
// Costruisce l'URL dell'endpoint lista
|
||||
$query = [];
|
||||
if ($atts['id'] !== '') {
|
||||
$query['id'] = (int) $atts['id'];
|
||||
}
|
||||
if ($atts['ids'] !== '') {
|
||||
$clean = preg_replace('/[^0-9,]/', '', $atts['ids']);
|
||||
if ($clean !== '') {
|
||||
$query['ids'] = $clean;
|
||||
}
|
||||
}
|
||||
if ($atts['limit'] !== '') {
|
||||
$query['limit'] = (int) $atts['limit'];
|
||||
}
|
||||
$url = YSE_API_BASE . '/events.php' . (!empty($query) ? '?' . http_build_query($query) : '');
|
||||
|
||||
// Lettura server-side (niente CORS)
|
||||
$resp = wp_remote_get($url, ['timeout' => 8]);
|
||||
if (is_wp_error($resp) || (int) wp_remote_retrieve_response_code($resp) !== 200) {
|
||||
return '<div class="yse-widget"><p class="yse-empty">Eventi non disponibili al momento.</p></div>';
|
||||
}
|
||||
$data = json_decode(wp_remote_retrieve_body($resp), true);
|
||||
if (!is_array($data) || empty($data['ok'])) {
|
||||
return '<div class="yse-widget"><p class="yse-empty">Eventi non disponibili al momento.</p></div>';
|
||||
}
|
||||
if (empty($data['events'])) {
|
||||
return '<div class="yse-widget"><p class="yse-empty">Nessun evento in programma al momento.</p></div>';
|
||||
}
|
||||
|
||||
$out = '<div class="yse-widget" data-api="' . esc_attr(YSE_API_BASE) . '">';
|
||||
foreach ($data['events'] as $ev) {
|
||||
$out .= yse_render_card($ev);
|
||||
}
|
||||
$out .= '</div>';
|
||||
return $out;
|
||||
}
|
||||
|
||||
/* Icona SVG inline (stroke = currentColor) */
|
||||
function yse_icon($name)
|
||||
{
|
||||
$p = [
|
||||
'user' => '<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/>',
|
||||
'pin' => '<path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"/><circle cx="12" cy="10" r="3"/>',
|
||||
'clock' => '<circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/>',
|
||||
'heart' => '<path d="M20.8 5.6a5 5 0 0 0-7-.1L12 7l-1.8-1.5a5 5 0 1 0-6.6 7.5L12 21l8.4-8a5 5 0 0 0 .4-7.4z"/>',
|
||||
'bag' => '<path d="M6 7h12l1 13H5L6 7z"/><path d="M9 7a3 3 0 0 1 6 0"/>',
|
||||
'info' => '<circle cx="12" cy="12" r="9"/><path d="M12 11v5"/><path d="M12 8h.01"/>',
|
||||
'img' => '<rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="9" cy="9" r="2"/><path d="m21 15-5-5L5 21"/>',
|
||||
'chev' => '<path d="m6 9 6 6 6-6"/>',
|
||||
];
|
||||
$inner = $p[$name] ?? '';
|
||||
return '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7">' . $inner . '</svg>';
|
||||
}
|
||||
|
||||
/* Rende una singola card evento */
|
||||
function yse_render_card($ev)
|
||||
{
|
||||
$mesiAbbr = [1 => 'GEN', 2 => 'FEB', 3 => 'MAR', 4 => 'APR', 5 => 'MAG', 6 => 'GIU', 7 => 'LUG', 8 => 'AGO', 9 => 'SET', 10 => 'OTT', 11 => 'NOV', 12 => 'DIC'];
|
||||
$mesi = [1 => 'gennaio', 2 => 'febbraio', 3 => 'marzo', 4 => 'aprile', 5 => 'maggio', 6 => 'giugno', 7 => 'luglio', 8 => 'agosto', 9 => 'settembre', 10 => 'ottobre', 11 => 'novembre', 12 => 'dicembre'];
|
||||
|
||||
$id = (int) $ev['id'];
|
||||
$cost = (float) $ev['cost'];
|
||||
$sym = (string) $ev['currency_symbol'];
|
||||
$price = $cost > 0 ? $sym . number_format($cost, 2, ',', '.') : 'Gratuito';
|
||||
$color = (isset($ev['color']) && preg_match('/^#[0-9a-fA-F]{6}$/', $ev['color'])) ? $ev['color'] : '#4CAF50';
|
||||
|
||||
$soldOut = !empty($ev['sold_out']);
|
||||
$waitOn = !empty($ev['waitlist_enabled']);
|
||||
$waitMode = $soldOut && $waitOn;
|
||||
$hardFull = $soldOut && !$waitOn;
|
||||
$maxPer = max(1, (int) $ev['max_per_booking']);
|
||||
$remaining = (int) $ev['remaining'];
|
||||
$maxQty = $soldOut ? ($waitMode ? $maxPer : 0) : max(1, min($maxPer, $remaining));
|
||||
|
||||
$startTs = strtotime($ev['starts_at']);
|
||||
$dayNum = (int) date('j', $startTs);
|
||||
$monAbbr = $mesiAbbr[(int) date('n', $startTs)];
|
||||
$dateLong = $dayNum . ' ' . $mesi[(int) date('n', $startTs)] . ' ' . date('Y', $startTs);
|
||||
$timeLabel = date('H:i', $startTs) . (!empty($ev['ends_at']) ? '–' . date('H:i', strtotime($ev['ends_at'])) : '');
|
||||
|
||||
$address = trim((string) ($ev['address'] ?? ''));
|
||||
$organizer = trim((string) ($ev['organizer'] ?? ''));
|
||||
$mapQuery = $address !== '' ? $address : ((isset($ev['lat'], $ev['lng']) && $ev['lat'] && $ev['lng']) ? ($ev['lat'] . ',' . $ev['lng']) : '');
|
||||
|
||||
$gallery = is_array($ev['gallery'] ?? null) ? $ev['gallery'] : [];
|
||||
$hasDetails = trim((string) ($ev['description'] ?? '')) !== ''
|
||||
|| trim((string) ($ev['suitable_for'] ?? '')) !== ''
|
||||
|| trim((string) ($ev['what_to_bring'] ?? '')) !== ''
|
||||
|| trim((string) ($ev['instructions'] ?? '')) !== ''
|
||||
|| !empty($ev['cover_image']) || !empty($gallery) || $mapQuery !== '';
|
||||
|
||||
ob_start();
|
||||
?>
|
||||
<div class="yse-card" style="--yse-accent: <?php echo esc_attr($color); ?>;"
|
||||
data-eid="<?php echo $id; ?>"
|
||||
data-cost="<?php echo esc_attr($cost); ?>"
|
||||
data-sym="<?php echo esc_attr($sym); ?>"
|
||||
data-maxqty="<?php echo (int) $maxQty; ?>"
|
||||
data-waitmode="<?php echo $waitMode ? 1 : 0; ?>">
|
||||
|
||||
<!-- HEADER COMPATTO -->
|
||||
<div class="yse-chead">
|
||||
<div class="yse-datechip">
|
||||
<div class="yse-m"><?php echo esc_html($monAbbr); ?></div>
|
||||
<div class="yse-d"><?php echo $dayNum; ?></div>
|
||||
</div>
|
||||
<div class="yse-main">
|
||||
<div class="yse-title-row">
|
||||
<h3><?php echo esc_html($ev['title']); ?></h3>
|
||||
<?php if ($hasDetails) : ?>
|
||||
<button type="button" class="yse-toggle" aria-expanded="false">
|
||||
<span class="yse-tlabel">Dettagli</span><?php echo yse_icon('chev'); ?>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="yse-meta">
|
||||
<span class="yse-item"><?php echo yse_icon('user'); ?> Capacità <strong><?php echo (int) $ev['occupied']; ?>/<?php echo (int) $ev['max_seats']; ?></strong></span>
|
||||
<?php if ($address !== '') : ?>
|
||||
<span class="yse-item"><?php echo yse_icon('pin'); ?> <?php echo esc_html($address); ?></span>
|
||||
<?php endif; ?>
|
||||
<span class="yse-item"><?php echo yse_icon('clock'); ?> <?php echo esc_html($dateLong); ?> · <?php echo esc_html($timeLabel); ?></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="yse-price"><?php echo esc_html($price); ?></div>
|
||||
</div>
|
||||
|
||||
<!-- DETTAGLI ESPANDIBILI -->
|
||||
<?php if ($hasDetails) : ?>
|
||||
<div class="yse-details">
|
||||
<div class="yse-hero <?php echo empty($ev['cover_image']) ? 'yse-nocover' : ''; ?>">
|
||||
<?php if (!empty($ev['cover_image'])) : ?>
|
||||
<img src="<?php echo esc_url($ev['cover_image']); ?>" alt="<?php echo esc_attr($ev['title']); ?>">
|
||||
<?php else : ?>
|
||||
<div class="yse-emblem"><?php echo yse_icon('img'); ?></div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="yse-body">
|
||||
<?php if (trim((string) ($ev['description'] ?? '')) !== '') : ?>
|
||||
<div class="yse-desc"><?php echo wp_kses_post($ev['description']); ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (trim((string) ($ev['suitable_for'] ?? '')) !== '' || trim((string) ($ev['what_to_bring'] ?? '')) !== '' || trim((string) ($ev['instructions'] ?? '')) !== '') : ?>
|
||||
<div class="yse-detail-grid">
|
||||
<?php if (trim((string) ($ev['suitable_for'] ?? '')) !== '') : ?>
|
||||
<div class="yse-detail">
|
||||
<div class="yse-dh"><?php echo yse_icon('heart'); ?> Per chi è adatto</div>
|
||||
<div class="yse-dt"><?php echo nl2br(esc_html($ev['suitable_for'])); ?></div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php if (trim((string) ($ev['what_to_bring'] ?? '')) !== '') : ?>
|
||||
<div class="yse-detail">
|
||||
<div class="yse-dh"><?php echo yse_icon('bag'); ?> Cosa portare</div>
|
||||
<div class="yse-dt"><?php echo nl2br(esc_html($ev['what_to_bring'])); ?></div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php if (trim((string) ($ev['instructions'] ?? '')) !== '') : ?>
|
||||
<div class="yse-detail">
|
||||
<div class="yse-dh"><?php echo yse_icon('info'); ?> Istruzioni</div>
|
||||
<div class="yse-dt"><?php echo nl2br(esc_html($ev['instructions'])); ?></div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($gallery)) : ?>
|
||||
<div class="yse-section">
|
||||
<h4><?php echo yse_icon('img'); ?> Galleria</h4>
|
||||
<div class="yse-gallery">
|
||||
<?php foreach ($gallery as $g) : ?>
|
||||
<img src="<?php echo esc_url($g); ?>" alt="" data-full="<?php echo esc_url($g); ?>">
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($mapQuery !== '') : ?>
|
||||
<div class="yse-section">
|
||||
<h4><?php echo yse_icon('pin'); ?> Dove</h4>
|
||||
<iframe class="yse-map" loading="lazy" src="https://www.google.com/maps?q=<?php echo rawurlencode($mapQuery); ?>&output=embed"></iframe>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- BOOKING -->
|
||||
<div class="yse-booking">
|
||||
<?php if ($hardFull) : ?>
|
||||
<div class="yse-soldout">Evento esaurito — non ci sono più posti disponibili.</div>
|
||||
<?php else : ?>
|
||||
<?php if ($waitMode) : ?>
|
||||
<div class="yse-wl-banner">Posti esauriti — puoi iscriverti alla <strong>lista d'attesa</strong>: ti avviseremo se si libera un posto.</div>
|
||||
<?php endif; ?>
|
||||
<div class="yse-bcompact">
|
||||
<div class="yse-btitle"><?php echo $waitMode ? "Lista d'attesa" : 'Prenota questo evento'; ?></div>
|
||||
<div class="yse-bblock">
|
||||
<div class="yse-lbl">Persone</div>
|
||||
<div class="yse-stepper">
|
||||
<button type="button" class="yse-minus">−</button>
|
||||
<span class="yse-num">1</span>
|
||||
<button type="button" class="yse-plus">+</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="yse-bblock yse-total">
|
||||
<div class="yse-lbl">Totale</div>
|
||||
<div class="yse-val"><?php echo esc_html($price); ?></div>
|
||||
</div>
|
||||
<button type="button" class="yse-cta yse-open"><?php echo $waitMode ? "Lista d'attesa" : 'Prenota'; ?></button>
|
||||
</div>
|
||||
|
||||
<div class="yse-bform">
|
||||
<div class="yse-frow">
|
||||
<div class="yse-field"><label>Nome *</label><input type="text" class="yse-name" placeholder="Il tuo nome"></div>
|
||||
<div class="yse-field"><label>Cognome *</label><input type="text" class="yse-surname" placeholder="Il tuo cognome"></div>
|
||||
</div>
|
||||
<div class="yse-field"><label>Email *</label><input type="email" class="yse-email" placeholder="latua@email.it"></div>
|
||||
<label class="yse-consent"><input type="checkbox" class="yse-c-privacy"> <span>Ho letto e accetto l'informativa privacy. *</span></label>
|
||||
<label class="yse-consent"><input type="checkbox" class="yse-c-mkt"> <span>Desidero ricevere aggiornamenti e novità via email.</span></label>
|
||||
<!-- honeypot anti-bot -->
|
||||
<input type="text" class="yse-hp" name="website" tabindex="-1" autocomplete="off">
|
||||
<button type="button" class="yse-cta yse-full yse-submit"><?php echo $waitMode ? "Iscriviti alla lista d'attesa" : 'Conferma prenotazione'; ?></button>
|
||||
<div class="yse-error"></div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
return ob_get_clean();
|
||||
}
|
||||
Reference in New Issue
Block a user