1187 lines
46 KiB
JavaScript
1187 lines
46 KiB
JavaScript
/*!
|
||
* YogiBook - negozio della scuola.
|
||
* Catalogo, scheda prodotto e scelte (classe fissa, data, destinatario del buono).
|
||
* Pensato per essere riusato anche dal plugin WordPress:
|
||
* YBShop.mount(elemento, dati, { onCheckout: function (scelta) { ... } })
|
||
* "dati" è l'oggetto prodotto da ShopService::catalog().
|
||
* Indirizzi: #/slug (scheda prodotto), #/slug/scegli (passo delle scelte).
|
||
*/
|
||
(function () {
|
||
"use strict";
|
||
|
||
var MESI = [
|
||
"gennaio",
|
||
"febbraio",
|
||
"marzo",
|
||
"aprile",
|
||
"maggio",
|
||
"giugno",
|
||
"luglio",
|
||
"agosto",
|
||
"settembre",
|
||
"ottobre",
|
||
"novembre",
|
||
"dicembre",
|
||
];
|
||
var TONES = {
|
||
single: "#DBF6F9",
|
||
fixed: "#DBF6F9",
|
||
free: "#F8EFE4",
|
||
membership: "#F8EFE4",
|
||
voucher: "#E2CEB1",
|
||
};
|
||
var GROUPS = [
|
||
["single", "Per iniziare"],
|
||
["fixed", "Classe fissa"],
|
||
["free", "Flessibili"],
|
||
["membership", "Tessera"],
|
||
["voucher", "Regali"],
|
||
];
|
||
|
||
function esc(s) {
|
||
return String(s === null || s === undefined ? "" : s).replace(
|
||
/[&<>"']/g,
|
||
function (c) {
|
||
return {
|
||
"&": "&",
|
||
"<": "<",
|
||
">": ">",
|
||
'"': """,
|
||
"'": "'",
|
||
}[c];
|
||
},
|
||
);
|
||
}
|
||
function euro(n) {
|
||
return (
|
||
Number(n).toLocaleString("it-IT", {
|
||
minimumFractionDigits: 2,
|
||
maximumFractionDigits: 2,
|
||
}) + " €"
|
||
);
|
||
}
|
||
function fmtDate(iso) {
|
||
var p = String(iso).split("-");
|
||
return (
|
||
parseInt(p[2], 10) + " " + MESI[parseInt(p[1], 10) - 1] + " " + p[0]
|
||
);
|
||
}
|
||
function addDays(iso, n) {
|
||
var p = iso.split("-"),
|
||
d = new Date(+p[0], +p[1] - 1, +p[2] + n);
|
||
return (
|
||
d.getFullYear() +
|
||
"-" +
|
||
String(d.getMonth() + 1).padStart(2, "0") +
|
||
"-" +
|
||
String(d.getDate()).padStart(2, "0")
|
||
);
|
||
}
|
||
function unitLabel(n, unit) {
|
||
var l = {
|
||
days: ["giorno", "giorni"],
|
||
weeks: ["settimana", "settimane"],
|
||
months: ["mese", "mesi"],
|
||
}[unit] || ["", ""];
|
||
return n + " " + (n === 1 ? l[0] : l[1]);
|
||
}
|
||
|
||
function mount(root, DATA, opts) {
|
||
opts = opts || {};
|
||
// cls = classe scelta nell'orario; prefSvc = classe da preselezionare nella scheda (classe fissa)
|
||
var S = {
|
||
filter: "",
|
||
cat: 0,
|
||
pid: null,
|
||
vid: null,
|
||
picked: [],
|
||
occ: null,
|
||
rName: "",
|
||
rEmail: "",
|
||
rMsg: "",
|
||
img: 0,
|
||
cls: 0,
|
||
prefSvc: 0,
|
||
from: "",
|
||
};
|
||
var lastView = null;
|
||
|
||
/* ---------------- dati ---------------- */
|
||
function byId(id) {
|
||
for (var i = 0; i < DATA.products.length; i++)
|
||
if (DATA.products[i].id === id) return DATA.products[i];
|
||
return null;
|
||
}
|
||
function bySlug(slug) {
|
||
for (var i = 0; i < DATA.products.length; i++)
|
||
if (DATA.products[i].slug === slug) return DATA.products[i];
|
||
return null;
|
||
}
|
||
function prod() {
|
||
return byId(S.pid);
|
||
}
|
||
function variant(p) {
|
||
p = p || prod();
|
||
if (!p) return null;
|
||
for (var i = 0; i < p.variants.length; i++)
|
||
if (p.variants[i].id === S.vid) return p.variants[i];
|
||
return null;
|
||
}
|
||
function svc(id) {
|
||
return DATA.services[id] || null;
|
||
}
|
||
function needsChoose(p) {
|
||
return (
|
||
!!p &&
|
||
(p.mode === "fixed" ||
|
||
p.mode === "single" ||
|
||
p.mode === "voucher")
|
||
);
|
||
}
|
||
|
||
function mark(p) {
|
||
if (p.mode === "fixed") return p.slots === 2 ? "2×" : "1×";
|
||
if (p.mode === "single") return p.once ? "Prova" : "1";
|
||
if (p.mode === "membership") return "Tessera";
|
||
if (p.mode === "voucher") return "Regalo";
|
||
var v = p.variants[0];
|
||
return v.lessons ? String(v.lessons) : "∞";
|
||
}
|
||
function ruleLine(p) {
|
||
var lessons = p.variants
|
||
.map(function (v) {
|
||
return v.lessons;
|
||
})
|
||
.filter(Boolean);
|
||
var range = lessons.length
|
||
? (Math.min.apply(null, lessons) ===
|
||
Math.max.apply(null, lessons)
|
||
? Math.min.apply(null, lessons)
|
||
: Math.min.apply(null, lessons) +
|
||
"–" +
|
||
Math.max.apply(null, lessons)) + " lezioni"
|
||
: "";
|
||
if (p.mode === "fixed")
|
||
return (
|
||
(p.slots === 2
|
||
? "2 classi fisse a settimana"
|
||
: "1 classe fissa a settimana") +
|
||
(range ? " · " + range : "")
|
||
);
|
||
if (p.mode === "free")
|
||
return (
|
||
"Prenoti tu" +
|
||
(p.weekly ? " · max " + p.weekly + "/sett." : "") +
|
||
(range ? " · " + range : "")
|
||
);
|
||
if (p.mode === "single") return "Scegli giorno e ora";
|
||
if (p.mode === "membership") return "Quota associativa";
|
||
return "Da regalare con un codice";
|
||
}
|
||
function validityText(p, v) {
|
||
if (p.mode === "membership") {
|
||
if (v.vmode === "calendar_year")
|
||
return "Valida fino al " + fmtDate(DATA.yearEnd);
|
||
if (v.vmode === "season")
|
||
return "Valida fino al " + fmtDate(DATA.seasonEnd);
|
||
if (v.vmode === "duration" && v.vvalue)
|
||
return (
|
||
"Valida " +
|
||
unitLabel(v.vvalue, v.vunit) +
|
||
" dall'acquisto"
|
||
);
|
||
return "Senza scadenza";
|
||
}
|
||
if (p.mode === "voucher")
|
||
return v.vvalue
|
||
? "Il buono scade dopo " + unitLabel(v.vvalue, v.vunit)
|
||
: "Il buono non scade";
|
||
if (p.mode === "single") return "Valida per la lezione che scegli";
|
||
if (v.vmode === "none" || !v.vvalue) return "Senza scadenza";
|
||
return (
|
||
"Da usare entro " +
|
||
unitLabel(v.vvalue, v.vunit) +
|
||
(v.vstart === "purchase"
|
||
? " dall'acquisto"
|
||
: " dalla prima lezione")
|
||
);
|
||
}
|
||
|
||
/* ---------------- posti ---------------- */
|
||
// Classe fissa: minimo dei posti liberi sulle prossime N lezioni coperte dal pacchetto
|
||
function fixedSeats(sid, p, v) {
|
||
var s = svc(sid);
|
||
var n = v && v.lessons ? Math.ceil(v.lessons / (p.slots || 1)) : 8;
|
||
var occ = s ? s.occ.slice(0, n) : [];
|
||
if (!occ.length)
|
||
return {
|
||
cls: "none",
|
||
text: "Nessuna lezione in calendario",
|
||
ok: false,
|
||
};
|
||
if (s.cap === null)
|
||
return { cls: "", text: "Posti disponibili", ok: true };
|
||
var min = Math.min.apply(
|
||
null,
|
||
occ.map(function (o) {
|
||
return o.free;
|
||
}),
|
||
);
|
||
if (min <= 0)
|
||
return { cls: "full", text: "Al completo", ok: false };
|
||
return {
|
||
cls: min <= 2 ? "low" : "",
|
||
text: min === 1 ? "Ultimo posto" : min + " posti liberi",
|
||
ok: true,
|
||
};
|
||
}
|
||
function seatsBadge(o) {
|
||
if (o.mine)
|
||
return { cls: "mine", text: "Sei già iscritta", ok: false };
|
||
if (o.free === null)
|
||
return { cls: "", text: "Posti disponibili", ok: true };
|
||
if (o.free <= 0)
|
||
return { cls: "full", text: "Al completo", ok: false };
|
||
return {
|
||
cls: o.free <= 2 ? "low" : "",
|
||
text: o.free === 1 ? "Ultimo posto" : o.free + " posti",
|
||
ok: true,
|
||
};
|
||
}
|
||
function singleOccurrences(p) {
|
||
var limit = addDays(DATA.today, DATA.singleDays || 21),
|
||
list = [];
|
||
p.services.forEach(function (sid) {
|
||
var s = svc(sid);
|
||
if (!s) return;
|
||
s.occ.forEach(function (o) {
|
||
if (o.date <= limit) list.push({ sid: sid, o: o, s: s });
|
||
});
|
||
});
|
||
list.sort(function (a, b) {
|
||
return (a.o.date + a.o.time).localeCompare(b.o.date + b.o.time);
|
||
});
|
||
return list.slice(0, 40);
|
||
}
|
||
function chooseValid(p) {
|
||
if (!p) return false;
|
||
if (p.mode === "fixed") return S.picked.length === (p.slots || 1);
|
||
if (p.mode === "single") return !!S.occ;
|
||
if (p.mode === "voucher")
|
||
return (
|
||
S.rName.trim() !== "" &&
|
||
/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(S.rEmail.trim())
|
||
);
|
||
return true;
|
||
}
|
||
|
||
/* ---------------- render ---------------- */
|
||
function cover(p, cls) {
|
||
var img = p.cover || (p.gallery && p.gallery[0]) || "";
|
||
return img
|
||
? '<div class="' +
|
||
cls +
|
||
'" style="background-image:url(\'' +
|
||
esc(img) +
|
||
"');\">"
|
||
: '<div class="' +
|
||
cls +
|
||
'" style="background-color:' +
|
||
TONES[p.mode] +
|
||
';"><span class="mark">' +
|
||
esc(mark(p)) +
|
||
"</span>";
|
||
}
|
||
|
||
var DAYS = [
|
||
"Domenica",
|
||
"Lunedì",
|
||
"Martedì",
|
||
"Mercoledì",
|
||
"Giovedì",
|
||
"Venerdì",
|
||
"Sabato",
|
||
];
|
||
var DAY_ORDER = [1, 2, 3, 4, 5, 6, 0];
|
||
var CAT_COLORS = [
|
||
"#6FBABE",
|
||
"#E2A86B",
|
||
"#9C8FD6",
|
||
"#7FB77E",
|
||
"#E58F9E",
|
||
"#5A9BD5",
|
||
"#C9A876",
|
||
];
|
||
|
||
function catColor(s) {
|
||
if (s.color) return s.color;
|
||
var i = 0;
|
||
(DATA.categories || []).forEach(function (c, idx) {
|
||
if (c.id === s.cat) i = idx;
|
||
});
|
||
return CAT_COLORS[i % CAT_COLORS.length];
|
||
}
|
||
function endTime(start, minutes) {
|
||
if (!minutes) return "";
|
||
var p = start.split(":"),
|
||
t = +p[0] * 60 + +p[1] + minutes;
|
||
return (
|
||
String(Math.floor(t / 60) % 24).padStart(2, "0") +
|
||
":" +
|
||
String(t % 60).padStart(2, "0")
|
||
);
|
||
}
|
||
function productsWith(sid) {
|
||
return DATA.products.filter(function (p) {
|
||
return p.kind === "lessons" && p.services.indexOf(sid) !== -1;
|
||
});
|
||
}
|
||
|
||
function renderTop(active) {
|
||
var h =
|
||
'<div class="ys-head"><div><h1 class="ys-h1">Scegli come praticare</h1><p class="ys-lead">' +
|
||
(active === "calendar"
|
||
? "Parti dall'orario: scegli una classe e vedi con quali pacchetti puoi frequentarla."
|
||
: "Pacchetti con la tua classe fissa, ingressi flessibili, una lezione singola o un regalo.") +
|
||
"</p></div>";
|
||
var u = DATA.user || {};
|
||
if (u.packages && u.packages.length) {
|
||
h +=
|
||
'<div class="ys-mine"><b>I tuoi pacchetti attivi</b>' +
|
||
u.packages
|
||
.slice(0, 3)
|
||
.map(function (k) {
|
||
return (
|
||
"<div><strong>" +
|
||
esc(k.title) +
|
||
"</strong><span>" +
|
||
(k.pending
|
||
? "in attesa di pagamento"
|
||
: (k.left !== null
|
||
? k.left + " lezioni"
|
||
: "") +
|
||
(k.expire
|
||
? " · fino al " + fmtDate(k.expire)
|
||
: "")) +
|
||
"</span></div>"
|
||
);
|
||
})
|
||
.join("") +
|
||
"</div>";
|
||
}
|
||
h += "</div>";
|
||
if (u.membershipRequired && !u.hasMembership) {
|
||
h +=
|
||
'<div class="ys-alert"><i class="bx bx-id-card"></i><div>Per frequentare le lezioni serve la <strong>tessera associativa</strong> valida.' +
|
||
(u.membershipProduct
|
||
? ' <a href="#/' +
|
||
esc(byId(u.membershipProduct).slug) +
|
||
'">Acquista la tessera</a>'
|
||
: " Chiedi in segreteria.") +
|
||
"</div></div>";
|
||
}
|
||
var hasCal = Object.keys(DATA.services).some(function (k) {
|
||
return DATA.services[k].occ.length > 0;
|
||
});
|
||
if (hasCal) {
|
||
h +=
|
||
'<div class="ys-tabs" role="tablist">' +
|
||
'<button type="button" role="tab" aria-selected="' +
|
||
(active === "products") +
|
||
'" class="ys-tab' +
|
||
(active === "products" ? " on" : "") +
|
||
'" data-act="tab" data-id="products"><i class="bx bx-grid-alt"></i> Prodotti</button>' +
|
||
'<button type="button" role="tab" aria-selected="' +
|
||
(active === "calendar") +
|
||
'" class="ys-tab' +
|
||
(active === "calendar" ? " on" : "") +
|
||
'" data-act="tab" data-id="calendar"><i class="bx bx-calendar-week"></i> Orario settimanale</button></div>';
|
||
}
|
||
return h;
|
||
}
|
||
|
||
function catSelect() {
|
||
if (!DATA.categories || DATA.categories.length < 2) return "";
|
||
return (
|
||
'<select class="ys-select" data-act="cat" aria-label="Disciplina"><option value="0">Tutte le discipline</option>' +
|
||
DATA.categories
|
||
.map(function (c) {
|
||
return (
|
||
'<option value="' +
|
||
c.id +
|
||
'"' +
|
||
(S.cat === c.id ? " selected" : "") +
|
||
">" +
|
||
esc(c.name) +
|
||
"</option>"
|
||
);
|
||
})
|
||
.join("") +
|
||
"</select>"
|
||
);
|
||
}
|
||
|
||
function productCard(p, fromSvc) {
|
||
var min = Math.min.apply(
|
||
null,
|
||
p.variants.map(function (v) {
|
||
return v.price;
|
||
}),
|
||
);
|
||
return (
|
||
'<button type="button" class="ys-card" data-act="open" data-id="' +
|
||
p.id +
|
||
'"' +
|
||
(fromSvc ? ' data-svc="' + fromSvc + '"' : "") +
|
||
">" +
|
||
cover(p, "ys-card-img") +
|
||
(p.blocked === "used"
|
||
? '<span class="ys-badge muted">Già utilizzata</span>'
|
||
: p.badge
|
||
? '<span class="ys-badge">' + esc(p.badge) + "</span>"
|
||
: "") +
|
||
"</div>" +
|
||
'<span class="ys-card-body"><span class="ys-card-title">' +
|
||
esc(p.title) +
|
||
"</span>" +
|
||
(p.subtitle
|
||
? '<span class="ys-card-sub">' + esc(p.subtitle) + "</span>"
|
||
: "") +
|
||
'<span class="ys-card-rule">' +
|
||
esc(ruleLine(p)) +
|
||
"</span>" +
|
||
'<span class="ys-card-foot"><span class="ys-price-from">' +
|
||
(p.variants.length > 1 ? "<small>da </small>" : "") +
|
||
euro(min) +
|
||
"</span></span></span></button>"
|
||
);
|
||
}
|
||
|
||
function renderCatalog() {
|
||
var h = renderTop("products");
|
||
var present = GROUPS.filter(function (g) {
|
||
return DATA.products.some(function (p) {
|
||
return p.group === g[0];
|
||
});
|
||
});
|
||
h +=
|
||
'<div class="ys-filters" role="toolbar" aria-label="Filtra i prodotti"><button type="button" class="ys-pill' +
|
||
(S.filter === "" ? " on" : "") +
|
||
'" data-act="filter" data-id="">Tutti</button>' +
|
||
present
|
||
.map(function (g) {
|
||
return (
|
||
'<button type="button" class="ys-pill' +
|
||
(S.filter === g[0] ? " on" : "") +
|
||
'" data-act="filter" data-id="' +
|
||
g[0] +
|
||
'">' +
|
||
esc(g[1]) +
|
||
"</button>"
|
||
);
|
||
})
|
||
.join("") +
|
||
catSelect() +
|
||
"</div>";
|
||
|
||
var list = DATA.products.filter(function (p) {
|
||
if (S.filter && p.group !== S.filter) return false;
|
||
if (
|
||
S.cat &&
|
||
p.kind === "lessons" &&
|
||
p.cats.indexOf(S.cat) === -1
|
||
)
|
||
return false;
|
||
if (S.cat && p.kind !== "lessons" && S.filter === "")
|
||
return false;
|
||
return true;
|
||
});
|
||
if (!DATA.products.length)
|
||
return (
|
||
h +
|
||
'<div class="ys-noresult">Al momento non ci sono prodotti in vendita.</div>'
|
||
);
|
||
if (!list.length)
|
||
return (
|
||
h +
|
||
'<div class="ys-noresult">Nessun prodotto con questi filtri.</div>'
|
||
);
|
||
return (
|
||
h +
|
||
'<div class="ys-grid">' +
|
||
list
|
||
.map(function (p) {
|
||
return productCard(p);
|
||
})
|
||
.join("") +
|
||
"</div>"
|
||
);
|
||
}
|
||
|
||
/* Orario settimanale: le classi ricavate dalla prossima lezione in calendario */
|
||
function renderCalendar() {
|
||
var h = renderTop("calendar");
|
||
h +=
|
||
'<div class="ys-filters">' +
|
||
catSelect() +
|
||
'<span class="ys-muted" style="margin-left:auto;">Tocca una classe per vedere come frequentarla</span></div>';
|
||
|
||
var cols = {};
|
||
DAY_ORDER.forEach(function (d) {
|
||
cols[d] = [];
|
||
});
|
||
Object.keys(DATA.services).forEach(function (k) {
|
||
var s = DATA.services[k],
|
||
sid = +k;
|
||
if (
|
||
!s.occ.length ||
|
||
(S.cat && s.cat !== S.cat) ||
|
||
!productsWith(sid).length
|
||
)
|
||
return;
|
||
var o = s.occ[0],
|
||
p = o.date.split("-");
|
||
var wd = new Date(+p[0], +p[1] - 1, +p[2]).getDay();
|
||
cols[wd].push({ sid: sid, s: s, o: o });
|
||
});
|
||
var days = DAY_ORDER.filter(function (d) {
|
||
return cols[d].length || (d >= 1 && d <= 5);
|
||
});
|
||
if (
|
||
!days.some(function (d) {
|
||
return cols[d].length;
|
||
})
|
||
) {
|
||
return (
|
||
h +
|
||
'<div class="ys-noresult">Nessuna classe in calendario' +
|
||
(S.cat ? " per questa disciplina" : "") +
|
||
".</div>"
|
||
);
|
||
}
|
||
|
||
h += '<div class="ys-week" style="--ys-cols:' + days.length + ';">';
|
||
days.forEach(function (d) {
|
||
cols[d].sort(function (a, b) {
|
||
return a.o.time.localeCompare(b.o.time);
|
||
});
|
||
h +=
|
||
'<div class="ys-day"><div class="ys-day-name">' +
|
||
DAYS[d] +
|
||
"</div>";
|
||
if (!cols[d].length) h += '<div class="ys-day-empty">—</div>';
|
||
cols[d].forEach(function (it) {
|
||
var b = seatsBadge(it.o),
|
||
sel = S.cls === it.sid,
|
||
col = catColor(it.s);
|
||
var end = endTime(it.o.time, it.s.minutes);
|
||
h +=
|
||
'<button type="button" class="ys-slot' +
|
||
(sel ? " sel" : "") +
|
||
'" data-act="cls" data-id="' +
|
||
it.sid +
|
||
'" aria-pressed="' +
|
||
sel +
|
||
'" style="--ys-c:' +
|
||
esc(col) +
|
||
';">' +
|
||
'<span class="t">' +
|
||
esc(it.o.time) +
|
||
(end ? "–" + esc(end) : "") +
|
||
"</span>" +
|
||
'<span class="n">' +
|
||
esc(it.s.name) +
|
||
"</span>" +
|
||
(it.s.teacher
|
||
? '<span class="m">' + esc(it.s.teacher) + "</span>"
|
||
: "") +
|
||
'<span class="ys-seats ' +
|
||
b.cls +
|
||
'">' +
|
||
esc(b.text) +
|
||
"</span></button>";
|
||
});
|
||
h += "</div>";
|
||
});
|
||
h += "</div>";
|
||
|
||
if (S.cls && DATA.services[S.cls]) {
|
||
var s = DATA.services[S.cls],
|
||
list = productsWith(S.cls);
|
||
h +=
|
||
'<div class="ys-calpanel" id="ys-calpanel"><div class="ys-calpanel-head" style="--ys-c:' +
|
||
esc(catColor(s)) +
|
||
';"><div>' +
|
||
'<div class="ys-h3" style="margin:0;">' +
|
||
esc(s.name) +
|
||
" · " +
|
||
esc(s.occ[0].label.split(" · ")[0].split(" ")[0]) +
|
||
" " +
|
||
esc(s.occ[0].time) +
|
||
"</div>" +
|
||
'<div class="ys-muted">Prossime lezioni: ' +
|
||
s.occ
|
||
.slice(0, 3)
|
||
.map(function (o) {
|
||
return esc(o.label.replace(/^\S+\s/, ""));
|
||
})
|
||
.join(" · ") +
|
||
"</div></div>" +
|
||
'<button type="button" class="ys-back" data-act="cls" data-id="0" style="min-height:auto;">Chiudi <i class="bx bx-x"></i></button></div>' +
|
||
'<p class="ys-muted" style="margin:0 0 12px;">' +
|
||
(list.length === 1
|
||
? "Puoi frequentarla con questo prodotto:"
|
||
: "Puoi frequentarla con questi prodotti:") +
|
||
"</p>" +
|
||
'<div class="ys-grid">' +
|
||
list
|
||
.map(function (p) {
|
||
return productCard(p, S.cls);
|
||
})
|
||
.join("") +
|
||
"</div></div>";
|
||
}
|
||
return h;
|
||
}
|
||
|
||
function renderProduct(p, v) {
|
||
var imgs = [];
|
||
if (p.cover) imgs.push(p.cover);
|
||
(p.gallery || []).forEach(function (g) {
|
||
if (imgs.indexOf(g) === -1) imgs.push(g);
|
||
});
|
||
var cur = imgs[S.img] || imgs[0];
|
||
|
||
var h =
|
||
'<button type="button" class="ys-back" data-act="home"><i class="bx bx-chevron-left"></i> ' +
|
||
(S.from === "calendar"
|
||
? "Torna all'orario"
|
||
: "Tutti i prodotti") +
|
||
'</button><div class="ys-product"><div>';
|
||
h += cur
|
||
? '<div class="ys-gallery-main" style="background-image:url(\'' +
|
||
esc(cur) +
|
||
'\');" role="img" aria-label="' +
|
||
esc(p.title) +
|
||
'"></div>'
|
||
: '<div class="ys-gallery-main" style="background-color:' +
|
||
TONES[p.mode] +
|
||
';"><span class="mark">' +
|
||
esc(mark(p)) +
|
||
"</span></div>";
|
||
if (imgs.length > 1) {
|
||
h +=
|
||
'<div class="ys-thumbs">' +
|
||
imgs
|
||
.map(function (src, i) {
|
||
return (
|
||
'<button type="button" class="ys-thumb' +
|
||
(src === cur ? " on" : "") +
|
||
'" data-act="img" data-id="' +
|
||
i +
|
||
'" style="background-image:url(\'' +
|
||
esc(src) +
|
||
'\');" aria-label="Foto ' +
|
||
(i + 1) +
|
||
'"></button>'
|
||
);
|
||
})
|
||
.join("") +
|
||
"</div>";
|
||
}
|
||
if (p.pros.length) {
|
||
h +=
|
||
'<div style="margin-top:18px;"><h3 class="ys-h3">Vantaggi</h3>' +
|
||
p.pros
|
||
.map(function (t) {
|
||
return (
|
||
'<div class="ys-li"><span class="ys-check"><i class="bx bx-check"></i></span><span>' +
|
||
esc(t) +
|
||
"</span></div>"
|
||
);
|
||
})
|
||
.join("") +
|
||
"</div>";
|
||
}
|
||
if (p.cons.length) {
|
||
h +=
|
||
'<div class="ys-cons" style="margin-top:12px;"><h3 class="ys-h3" style="color:#7A5A2E;">Da considerare</h3>' +
|
||
p.cons
|
||
.map(function (t) {
|
||
return (
|
||
'<div class="ys-li"><span class="ys-dot"></span><span>' +
|
||
esc(t) +
|
||
"</span></div>"
|
||
);
|
||
})
|
||
.join("") +
|
||
"</div>";
|
||
}
|
||
h += '</div><div class="ys-info">';
|
||
if (p.badge)
|
||
h +=
|
||
'<span class="ys-badge" style="align-self:flex-start;">' +
|
||
esc(p.badge) +
|
||
"</span>";
|
||
h += '<h1 class="ys-title">' + esc(p.title) + "</h1>";
|
||
if (p.subtitle)
|
||
h += '<p class="ys-subtitle">' + esc(p.subtitle) + "</p>";
|
||
if (p.description)
|
||
h += '<div class="ys-desc">' + p.description + "</div>";
|
||
if (p.kind === "lessons" && p.services.length) {
|
||
h +=
|
||
'<div><h3 class="ys-h3">' +
|
||
(p.mode === "fixed"
|
||
? "Classi tra cui scegliere"
|
||
: "Vale per queste classi") +
|
||
'</h3><div class="ys-chips">' +
|
||
p.services
|
||
.map(function (sid) {
|
||
var s = svc(sid);
|
||
return s
|
||
? '<span class="ys-chip">' +
|
||
esc(
|
||
s.name + (s.day ? " · " + s.day : ""),
|
||
) +
|
||
"</span>"
|
||
: "";
|
||
})
|
||
.join("") +
|
||
"</div>" +
|
||
(p.weekly
|
||
? '<div class="ys-muted" style="margin-top:6px;">Puoi prenotare al massimo ' +
|
||
p.weekly +
|
||
" lezioni a settimana.</div>"
|
||
: "") +
|
||
(p.maxres !== null && p.mode !== "single"
|
||
? '<div class="ys-muted">Recuperi inclusi: ' +
|
||
p.maxres +
|
||
".</div>"
|
||
: "") +
|
||
"</div>";
|
||
}
|
||
if (p.suitable || p.requirements) {
|
||
h +=
|
||
'<div class="ys-info2">' +
|
||
(p.suitable
|
||
? "<div><b>Per chi è</b>" + esc(p.suitable) + "</div>"
|
||
: "") +
|
||
(p.requirements
|
||
? "<div><b>Cosa serve</b>" +
|
||
esc(p.requirements) +
|
||
"</div>"
|
||
: "") +
|
||
"</div>";
|
||
}
|
||
h +=
|
||
'<div><h3 class="ys-h3" style="font-size:17px;">' +
|
||
(p.variants.length > 1 ? "Scegli l'opzione" : "Prezzo") +
|
||
'</h3><div class="ys-options">';
|
||
p.variants.forEach(function (x) {
|
||
var sel = v && x.id === v.id;
|
||
h +=
|
||
'<button type="button" class="ys-opt' +
|
||
(sel ? " sel" : "") +
|
||
'" data-act="variant" data-id="' +
|
||
x.id +
|
||
'" aria-pressed="' +
|
||
sel +
|
||
'"><span class="ys-radio"></span>' +
|
||
'<span class="ys-opt-main"><span class="ys-opt-title">' +
|
||
esc(x.label) +
|
||
'</span><span class="ys-muted">' +
|
||
esc(validityText(p, x)) +
|
||
"</span>" +
|
||
(p.mode === "voucher" && x.targetLabel
|
||
? '<span class="ys-muted">Regala: ' +
|
||
esc(x.targetLabel) +
|
||
"</span>"
|
||
: "") +
|
||
(x.note
|
||
? '<span class="ys-opt-note">' + esc(x.note) + "</span>"
|
||
: "") +
|
||
"</span>" +
|
||
'<span class="ys-opt-price">' +
|
||
(x.compare ? "<s>" + euro(x.compare) + "</s>" : "") +
|
||
"<b>" +
|
||
euro(x.price) +
|
||
"</b></span></button>";
|
||
});
|
||
h += "</div></div></div></div>";
|
||
|
||
var blocked = p.blocked === "used";
|
||
h += buyBar(
|
||
v,
|
||
blocked
|
||
? '<button type="button" class="ys-btn" disabled>Già utilizzata</button>'
|
||
: '<button type="button" class="ys-btn" data-act="next">' +
|
||
(needsChoose(p) ? "Continua" : "Acquista") +
|
||
' <i class="bx bx-right-arrow-alt"></i></button>',
|
||
);
|
||
if (blocked)
|
||
h +=
|
||
'<p class="ys-muted" style="margin-top:8px;">Questo prodotto si può acquistare una sola volta e l\'hai già utilizzato.</p>';
|
||
return h;
|
||
}
|
||
|
||
function buyBar(v, button) {
|
||
return (
|
||
'<div class="ys-buy"><div style="display:flex;flex-direction:column;flex:1;min-width:0;"><span class="lbl">' +
|
||
esc(v ? v.label : "") +
|
||
'</span><span class="amt">' +
|
||
(v ? euro(v.price) : "") +
|
||
"</span></div>" +
|
||
button +
|
||
"</div>"
|
||
);
|
||
}
|
||
|
||
function renderChoose(p, v) {
|
||
var h =
|
||
'<button type="button" class="ys-back" data-act="toProduct"><i class="bx bx-chevron-left"></i> ' +
|
||
esc(p.title) +
|
||
'</button><div class="ys-choose">';
|
||
if (p.mode === "fixed") {
|
||
var slots = p.slots || 1;
|
||
h +=
|
||
'<div><h1 class="ys-title">' +
|
||
(slots === 2
|
||
? "Scegli le tue 2 classi"
|
||
: "Scegli la tua classe") +
|
||
'</h1><p class="ys-subtitle" style="font-size:15px;margin-top:6px;">' +
|
||
(slots === 1
|
||
? "Sarà il tuo posto fisso ogni settimana."
|
||
: "Saranno i tuoi due posti fissi ogni settimana.") +
|
||
"</p></div>";
|
||
h +=
|
||
'<span class="ys-pillcount">' +
|
||
S.picked.length +
|
||
" di " +
|
||
slots +
|
||
(slots === 1 ? " scelta" : " scelte") +
|
||
"</span>";
|
||
p.services.forEach(function (sid) {
|
||
var s = svc(sid);
|
||
if (!s) return;
|
||
var seat = fixedSeats(sid, p, v),
|
||
sel = S.picked.indexOf(sid) !== -1;
|
||
h +=
|
||
'<button type="button" class="ys-opt' +
|
||
(sel ? " sel" : "") +
|
||
'" data-act="pickClass" data-id="' +
|
||
sid +
|
||
'" aria-pressed="' +
|
||
sel +
|
||
'"' +
|
||
(!seat.ok && !sel ? " disabled" : "") +
|
||
">" +
|
||
'<span class="ys-box">' +
|
||
(sel ? '<i class="bx bx-check"></i>' : "") +
|
||
'</span><span class="ys-opt-main"><span class="ys-opt-title">' +
|
||
esc(s.day || s.name) +
|
||
"</span>" +
|
||
'<span class="ys-muted">' +
|
||
esc(s.name + (s.teacher ? " · " + s.teacher : "")) +
|
||
"</span>" +
|
||
(s.occ.length
|
||
? '<span class="ys-muted">Prossima lezione: ' +
|
||
esc(s.occ[0].label) +
|
||
"</span>"
|
||
: "") +
|
||
"</span>" +
|
||
'<span class="ys-seats ' +
|
||
seat.cls +
|
||
'">' +
|
||
esc(seat.text) +
|
||
"</span></button>";
|
||
});
|
||
h +=
|
||
'<div class="ys-note"><i class="bx bx-info-circle" style="color:#2F7F84;font-size:18px;"></i><span>Dopo l\'acquisto la scuola ti conferma la data della prima lezione e prenota per te tutte le altre, ogni settimana.</span></div>';
|
||
}
|
||
if (p.mode === "single") {
|
||
var list = singleOccurrences(p);
|
||
h +=
|
||
'<div><h1 class="ys-title">Scegli la lezione</h1><p class="ys-subtitle" style="font-size:15px;margin-top:6px;">Lezioni delle prossime settimane. Il posto si prenota quando completi l\'acquisto.</p></div>';
|
||
if (!list.length)
|
||
h +=
|
||
'<div class="ys-note">Al momento non ci sono lezioni prenotabili per questo prodotto.</div>';
|
||
list.forEach(function (it) {
|
||
var b = seatsBadge(it.o),
|
||
sel = S.occ === it.o.id;
|
||
h +=
|
||
'<button type="button" class="ys-opt' +
|
||
(sel ? " sel" : "") +
|
||
'" data-act="pickOcc" data-id="' +
|
||
it.o.id +
|
||
'" aria-pressed="' +
|
||
sel +
|
||
'"' +
|
||
(!b.ok ? " disabled" : "") +
|
||
'><span class="ys-radio"></span>' +
|
||
'<span class="ys-opt-main"><span class="ys-opt-title" style="font-size:15px;">' +
|
||
esc(it.o.label) +
|
||
'</span><span class="ys-muted">' +
|
||
esc(
|
||
it.s.name +
|
||
(it.s.teacher ? " · " + it.s.teacher : ""),
|
||
) +
|
||
"</span></span>" +
|
||
'<span class="ys-seats ' +
|
||
b.cls +
|
||
'">' +
|
||
esc(b.text) +
|
||
"</span></button>";
|
||
});
|
||
}
|
||
if (p.mode === "voucher") {
|
||
h +=
|
||
'<div><h1 class="ys-title">A chi lo regali?</h1><p class="ys-subtitle" style="font-size:15px;margin-top:6px;">Il buono arriva via email con un codice da usare nel negozio della scuola.</p></div>' +
|
||
'<div class="ys-panel">' +
|
||
'<div><label class="ys-label" for="ys-rname">Nome di chi lo riceve</label><input class="ys-input" id="ys-rname" data-field="rName" value="' +
|
||
esc(S.rName) +
|
||
'" placeholder="Es. Giulia" autocomplete="off"></div>' +
|
||
'<div><label class="ys-label" for="ys-remail">Email a cui inviarlo</label><input class="ys-input" id="ys-remail" type="email" data-field="rEmail" value="' +
|
||
esc(S.rEmail) +
|
||
'" placeholder="nome@email.it" autocomplete="off"><div class="ys-muted" style="margin-top:4px;">Metti la tua se preferisci consegnarlo di persona.</div></div>' +
|
||
'<div><label class="ys-label" for="ys-rmsg">Messaggio (facoltativo)</label><textarea class="ys-input" id="ys-rmsg" data-field="rMsg" rows="3" maxlength="500" placeholder="Buon compleanno!">' +
|
||
esc(S.rMsg) +
|
||
"</textarea></div></div>";
|
||
}
|
||
h += "</div>";
|
||
h += buyBar(
|
||
v,
|
||
'<button type="button" class="ys-btn" data-act="checkout" id="ys-go"' +
|
||
(chooseValid(p) ? "" : " disabled") +
|
||
'>Vai al pagamento <i class="bx bx-right-arrow-alt"></i></button>',
|
||
);
|
||
return h;
|
||
}
|
||
|
||
function view() {
|
||
var hash = decodeURIComponent(
|
||
(location.hash || "").replace(/^#\/?/, ""),
|
||
);
|
||
var parts = hash.split("/");
|
||
if (parts[0] === "orario") return { name: "calendar" };
|
||
var p = parts[0] ? bySlug(parts[0]) : null;
|
||
if (!p) return { name: "catalog" };
|
||
return {
|
||
name:
|
||
parts[1] === "scegli" &&
|
||
needsChoose(p) &&
|
||
p.blocked !== "used"
|
||
? "choose"
|
||
: "product",
|
||
p: p,
|
||
};
|
||
}
|
||
|
||
function render() {
|
||
var vw = view(),
|
||
html;
|
||
if (vw.name === "catalog") {
|
||
S.from = "";
|
||
html = renderCatalog();
|
||
} else if (vw.name === "calendar") {
|
||
S.from = "calendar";
|
||
html = renderCalendar();
|
||
} else {
|
||
if (S.pid !== vw.p.id) {
|
||
S.pid = vw.p.id;
|
||
S.vid = vw.p.variants[0].id;
|
||
S.picked = [];
|
||
S.occ = null;
|
||
S.img = 0;
|
||
// Arrivo dall'orario: preseleziona la classe scelta
|
||
if (S.prefSvc && vw.p.services.indexOf(S.prefSvc) !== -1) {
|
||
if (
|
||
vw.p.mode === "fixed" &&
|
||
fixedSeats(S.prefSvc, vw.p, variant(vw.p)).ok
|
||
)
|
||
S.picked = [S.prefSvc];
|
||
if (vw.p.mode === "single" && svc(S.prefSvc)) {
|
||
var free = svc(S.prefSvc).occ.filter(function (o) {
|
||
return seatsBadge(o).ok;
|
||
})[0];
|
||
if (free) S.occ = free.id;
|
||
}
|
||
}
|
||
S.prefSvc = 0;
|
||
}
|
||
var v = variant(vw.p);
|
||
html =
|
||
vw.name === "choose"
|
||
? renderChoose(vw.p, v)
|
||
: renderProduct(vw.p, v);
|
||
}
|
||
root.innerHTML = html;
|
||
var key = vw.name + (vw.p ? vw.p.id : "");
|
||
if (lastView !== key) {
|
||
lastView = key;
|
||
var top =
|
||
root.getBoundingClientRect().top + window.pageYOffset - 90;
|
||
window.scrollTo({ top: Math.max(0, top) });
|
||
}
|
||
}
|
||
|
||
function go(hash) {
|
||
if (location.hash === hash) render();
|
||
else location.hash = hash;
|
||
}
|
||
|
||
/* ---------------- azioni ---------------- */
|
||
var actions = {
|
||
filter: function (id) {
|
||
S.filter = id || "";
|
||
render();
|
||
},
|
||
open: function (id, el) {
|
||
var p = byId(+id);
|
||
if (!p) return;
|
||
S.prefSvc =
|
||
el && el.getAttribute("data-svc")
|
||
? +el.getAttribute("data-svc")
|
||
: 0;
|
||
S.pid = null; // nuova apertura: riparte dalle scelte predefinite
|
||
go("#/" + encodeURIComponent(p.slug));
|
||
},
|
||
home: function () {
|
||
go(S.from === "calendar" ? "#/orario" : "#/");
|
||
},
|
||
tab: function (id) {
|
||
go(id === "calendar" ? "#/orario" : "#/");
|
||
},
|
||
cls: function (id) {
|
||
S.cls = S.cls === +id ? 0 : +id;
|
||
render();
|
||
var panel = document.getElementById("ys-calpanel");
|
||
if (panel && panel.scrollIntoView)
|
||
panel.scrollIntoView({
|
||
behavior: "smooth",
|
||
block: "nearest",
|
||
});
|
||
},
|
||
toProduct: function () {
|
||
var p = prod();
|
||
if (p) go("#/" + encodeURIComponent(p.slug));
|
||
},
|
||
img: function (i) {
|
||
S.img = +i;
|
||
render();
|
||
},
|
||
variant: function (id) {
|
||
S.vid = +id;
|
||
var p = prod(),
|
||
v = variant(p);
|
||
if (p && p.mode === "fixed")
|
||
S.picked = S.picked.filter(function (sid) {
|
||
return fixedSeats(sid, p, v).ok;
|
||
});
|
||
render();
|
||
},
|
||
next: function () {
|
||
var p = prod();
|
||
if (!p || p.blocked === "used") return;
|
||
if (needsChoose(p))
|
||
go("#/" + encodeURIComponent(p.slug) + "/scegli");
|
||
else actions.checkout();
|
||
},
|
||
pickClass: function (id) {
|
||
var p = prod(),
|
||
sid = +id,
|
||
slots = p.slots || 1,
|
||
arr = S.picked.slice(),
|
||
i = arr.indexOf(sid);
|
||
if (i !== -1) arr.splice(i, 1);
|
||
else if (slots === 1) arr = [sid];
|
||
else if (arr.length < slots) arr.push(sid);
|
||
else arr = [arr[1], sid];
|
||
S.picked = arr;
|
||
render();
|
||
},
|
||
pickOcc: function (id) {
|
||
S.occ = +id;
|
||
render();
|
||
},
|
||
checkout: function () {
|
||
var p = prod(),
|
||
v = variant(p);
|
||
if (!p || !v || !chooseValid(p) || p.blocked === "used") return;
|
||
var sel = {
|
||
product: p,
|
||
variant: v,
|
||
idvariant: v.id,
|
||
services: p.mode === "fixed" ? S.picked.slice() : [],
|
||
idschedule: p.mode === "single" ? S.occ : null,
|
||
recipient:
|
||
p.mode === "voucher"
|
||
? {
|
||
name: S.rName.trim(),
|
||
email: S.rEmail.trim(),
|
||
message: S.rMsg.trim(),
|
||
}
|
||
: null,
|
||
};
|
||
if (typeof opts.onCheckout === "function") opts.onCheckout(sel);
|
||
},
|
||
};
|
||
|
||
root.addEventListener("click", function (ev) {
|
||
var el = ev.target.closest("[data-act]");
|
||
if (
|
||
!el ||
|
||
el.disabled ||
|
||
!root.contains(el) ||
|
||
el.tagName === "SELECT"
|
||
)
|
||
return;
|
||
var fn = actions[el.getAttribute("data-act")];
|
||
if (fn) fn(el.getAttribute("data-id"), el);
|
||
});
|
||
root.addEventListener("change", function (ev) {
|
||
if (ev.target.getAttribute("data-act") === "cat") {
|
||
S.cat = +ev.target.value;
|
||
S.cls = 0;
|
||
render();
|
||
}
|
||
});
|
||
root.addEventListener("input", function (ev) {
|
||
var f = ev.target.getAttribute("data-field");
|
||
if (!f) return;
|
||
S[f] = ev.target.value;
|
||
var btn = document.getElementById("ys-go");
|
||
if (btn) btn.disabled = !chooseValid(prod());
|
||
});
|
||
window.addEventListener("hashchange", render);
|
||
render();
|
||
}
|
||
|
||
window.YBShop = { mount: mount };
|
||
|
||
// Webapp: monta il negozio e invia le scelte a shop_checkout.php
|
||
document.addEventListener("DOMContentLoaded", function () {
|
||
var root = document.getElementById("shop");
|
||
if (!root || !window.YB_SHOP) return;
|
||
mount(root, window.YB_SHOP, {
|
||
onCheckout: function (sel) {
|
||
var f = document.getElementById("checkout-form");
|
||
document.getElementById("f-variant").value = sel.idvariant;
|
||
document.getElementById("f-services").value =
|
||
sel.services.join(",");
|
||
document.getElementById("f-schedule").value =
|
||
sel.idschedule || "";
|
||
document.getElementById("f-rname").value = sel.recipient
|
||
? sel.recipient.name
|
||
: "";
|
||
document.getElementById("f-remail").value = sel.recipient
|
||
? sel.recipient.email
|
||
: "";
|
||
document.getElementById("f-rmsg").value = sel.recipient
|
||
? sel.recipient.message
|
||
: "";
|
||
f.submit();
|
||
},
|
||
});
|
||
});
|
||
})();
|