@@ -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 =
'
' +
'
' +
- '
Anteprima prenotazione ' +
+ '
' + (wm ? "Anteprima lista d'attesa" : 'Anteprima prenotazione') + ' ' +
'
' + name + ' ' + surname + ' · ' + email + '
' +
- '
' + qty + ' ' + (qty === 1 ? 'persona' : 'persone') + ' · Totale ' + fmtMoney(qty * COST) + '
' +
+ '
' + qty + ' ' + (qty === 1 ? 'persona' : 'persone') + (wm ? " · in lista d'attesa" : ' · Totale ' + fmtMoney(qty * COST) + ' ') + '
' +
'
Marketing: ' + (marketing ? 'sì' : 'no') + '
' +
- '
Nel widget pubblico, qui parte la prenotazione reale (POST all\'endpoint).
' +
+ '
' + (wm ? "Nel widget pubblico, qui parte l'iscrizione alla lista d'attesa." : 'Nel widget pubblico, qui parte la prenotazione reale (POST all\'endpoint).') + '
' +
'
';
});
})();
diff --git a/public/event_save.php b/public/event_save.php
index 6e40759..4389b2c 100644
--- a/public/event_save.php
+++ b/public/event_save.php
@@ -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,
diff --git a/public/event_waitlist.php b/public/event_waitlist.php
new file mode 100644
index 0000000..a493cc8
--- /dev/null
+++ b/public/event_waitlist.php
@@ -0,0 +1,59 @@
+ 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;
+ }
+}
diff --git a/wp plugin/yogasoul-eventi.zip b/wp plugin/yogasoul-eventi.zip
new file mode 100644
index 0000000..e5861a3
Binary files /dev/null and b/wp plugin/yogasoul-eventi.zip differ
diff --git a/wp plugin/yogasoul-eventi/assets/yogasoul-eventi.css b/wp plugin/yogasoul-eventi/assets/yogasoul-eventi.css
new file mode 100644
index 0000000..bf99300
--- /dev/null
+++ b/wp plugin/yogasoul-eventi/assets/yogasoul-eventi.css
@@ -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;
+ }
+}
diff --git a/wp plugin/yogasoul-eventi/assets/yogasoul-eventi.js b/wp plugin/yogasoul-eventi/assets/yogasoul-eventi.js
new file mode 100644
index 0000000..2c0a753
--- /dev/null
+++ b/wp plugin/yogasoul-eventi/assets/yogasoul-eventi.js
@@ -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 =
+ '
' +
+ '
' +
+ "
" +
+ title +
+ " " +
+ "
" +
+ escapeHtml(name + " " + surname) +
+ " · " +
+ escapeHtml(email) +
+ "
" +
+ "
" +
+ escapeHtml(msg) +
+ "
" +
+ "
";
+ }
+ }
+
+ 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 =
+ '
× ';
+ 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();
+ }
+})();
diff --git a/wp plugin/yogasoul-eventi/yogasoul-eventi.php b/wp plugin/yogasoul-eventi/yogasoul-eventi.php
new file mode 100644
index 0000000..f32b460
--- /dev/null
+++ b/wp plugin/yogasoul-eventi/yogasoul-eventi.php
@@ -0,0 +1,275 @@
+ '',
+ '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 '
';
+ }
+ $data = json_decode(wp_remote_retrieve_body($resp), true);
+ if (!is_array($data) || empty($data['ok'])) {
+ return '
';
+ }
+ if (empty($data['events'])) {
+ return '
';
+ }
+
+ $out = '
';
+ foreach ($data['events'] as $ev) {
+ $out .= yse_render_card($ev);
+ }
+ $out .= '
';
+ return $out;
+}
+
+/* Icona SVG inline (stroke = currentColor) */
+function yse_icon($name)
+{
+ $p = [
+ 'user' => '
',
+ 'pin' => '
',
+ 'clock' => '
',
+ 'heart' => '
',
+ 'bag' => '
',
+ 'info' => '
',
+ 'img' => '
',
+ 'chev' => '
',
+ ];
+ $inner = $p[$name] ?? '';
+ return '
' . $inner . ' ';
+}
+
+/* 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();
+?>
+
+
+
+
+
+
+
+
+
+
+ Dettagli
+
+
+
+
+ Capacità /
+
+
+
+ ·
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Galleria
+
+
+
+
+
+
+
+
+
+
+
Dove
+
+
+
+
+
+
+
+
+
+
+
Evento esaurito — non ci sono più posti disponibili.
+
+
+
Posti esauriti — puoi iscriverti alla lista d'attesa : ti avviseremo se si libera un posto.
+
+
+
+
+
Persone
+
+ −
+ 1
+ +
+
+
+
+
+
+
+
+
+
+
+