nodebb-plugin-calendar-onekite 2.2.0 → 10.0.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/helloasso.js DELETED
@@ -1,129 +0,0 @@
1
- 'use strict';
2
-
3
- /**
4
- * HelloAsso integration helper.
5
- *
6
- * This file contains a minimal implementation to create a checkout "intent" URL.
7
- * You MUST configure the following settings in the plugin ACP (or environment variables):
8
- * - helloassoOrganizationSlug
9
- * - helloassoFormSlug (checkout form)
10
- * - helloassoClientId
11
- * - helloassoClientSecret
12
- *
13
- * Notes:
14
- * - HelloAsso uses OAuth2 client_credentials for API access.
15
- * - Depending on your HelloAsso setup, endpoints may differ (sandbox/production).
16
- * - This module is designed to be easy to adapt.
17
- */
18
-
19
- const meta = require.main.require('./src/meta');
20
- const Settings = meta.settings;
21
-
22
- let cachedToken = null;
23
- let cachedTokenExp = 0;
24
-
25
- async function getSettings() {
26
- const s = (await Settings.get('calendar-onekite')) || {};
27
- return {
28
- apiBase: s.helloassoApiBase || 'https://api.helloasso.com',
29
- organizationSlug: s.helloassoOrganizationSlug || '',
30
- formSlug: s.helloassoFormSlug || '',
31
- clientId: s.helloassoClientId || '',
32
- clientSecret: s.helloassoClientSecret || '',
33
- returnUrl: s.helloassoReturnUrl || '',
34
- };
35
- }
36
-
37
- async function fetchJson(url, opts) {
38
- const res = await fetch(url, opts);
39
- const text = await res.text();
40
- let data = null;
41
- try { data = text ? JSON.parse(text) : null; } catch {}
42
- if (!res.ok) {
43
- const msg = data?.message || data?.error || text || `HTTP ${res.status}`;
44
- throw new Error(msg);
45
- }
46
- return data;
47
- }
48
-
49
- async function getAccessToken() {
50
- const s = await getSettings();
51
- if (!s.clientId || !s.clientSecret) {
52
- throw new Error('HelloAsso: clientId/clientSecret non configurés');
53
- }
54
-
55
- const now = Math.floor(Date.now() / 1000);
56
- if (cachedToken && cachedTokenExp && now < cachedTokenExp - 30) {
57
- return cachedToken;
58
- }
59
-
60
- const tokenUrl = `${s.apiBase}/oauth2/token`;
61
- const body = new URLSearchParams();
62
- body.set('grant_type', 'client_credentials');
63
- body.set('client_id', s.clientId);
64
- body.set('client_secret', s.clientSecret);
65
-
66
- const data = await fetchJson(tokenUrl, {
67
- method: 'POST',
68
- headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
69
- body,
70
- });
71
-
72
- cachedToken = data.access_token;
73
- cachedTokenExp = now + Number(data.expires_in || 3600);
74
- return cachedToken;
75
- }
76
-
77
- /**
78
- * Create a checkout intent (returns a URL).
79
- * We store rid/eid/uid in "metadata"/customFields so the webhook can reconcile.
80
- *
81
- * This implementation targets the "checkout intent" endpoint. If your HelloAsso
82
- * account uses a different flow, adapt this function accordingly.
83
- */
84
- async function createHelloAssoCheckoutIntent({ eid, rid, uid, itemId, quantity, amount }) {
85
- const s = await getSettings();
86
- if (!s.organizationSlug || !s.formSlug) {
87
- throw new Error('HelloAsso: organizationSlug/formSlug non configurés');
88
- }
89
-
90
- const token = await getAccessToken();
91
-
92
- // Endpoint may vary. This is the common v5 checkout intent endpoint.
93
- const url = `${s.apiBase}/v5/organizations/${encodeURIComponent(s.organizationSlug)}/forms/Checkout/${encodeURIComponent(s.formSlug)}/checkout-intents`;
94
-
95
- const payload = {
96
- totalAmount: Math.round(Number(amount || 0) * 100), // cents
97
- initialAmount: Math.round(Number(amount || 0) * 100),
98
- // Return URL after payment
99
- returnUrl: s.returnUrl || undefined,
100
- // Store custom fields for reconciliation (webhook)
101
- metadata: {
102
- eid: String(eid),
103
- rid: String(rid),
104
- uid: String(uid),
105
- itemId: String(itemId),
106
- quantity: String(quantity),
107
- },
108
- };
109
-
110
- const data = await fetchJson(url, {
111
- method: 'POST',
112
- headers: {
113
- 'Authorization': `Bearer ${token}`,
114
- 'Content-Type': 'application/json',
115
- },
116
- body: JSON.stringify(payload),
117
- });
118
-
119
- // HelloAsso returns a checkoutUrl / redirectUrl depending on endpoint
120
- const checkoutUrl = data?.redirectUrl || data?.checkoutUrl || data?.url;
121
- if (!checkoutUrl) {
122
- throw new Error('HelloAsso: checkout URL introuvable dans la réponse');
123
- }
124
- return checkoutUrl;
125
- }
126
-
127
- module.exports = {
128
- createHelloAssoCheckoutIntent,
129
- };
@@ -1,4 +0,0 @@
1
- .calendar-modal{display:none;position:fixed;z-index:9999;left:0;top:0;width:100%;height:100%;overflow:auto;background:rgba(0,0,0,.45)}
2
- .calendar-modal-content{background:#fff;margin:5vh auto;padding:16px;width:min(900px,92vw);border-radius:10px;box-shadow:0 10px 40px rgba(0,0,0,.2)}
3
- .calendar-close{float:right;cursor:pointer;font-size:22px;line-height:1}
4
- #calendar{min-height:600px}
@@ -1,26 +0,0 @@
1
- (function () {
2
- 'use strict';
3
-
4
- function ready(fn){
5
- if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', fn);
6
- else fn();
7
- }
8
-
9
- function boot(){
10
- if (!document.querySelector('#planning-calendar')) return;
11
-
12
- if (typeof window.require !== 'function') {
13
- console.warn('[calendar-onekite] require is not defined (planning)');
14
- return;
15
- }
16
-
17
- window.require([
18
- 'plugins/nodebb-plugin-calendar-onekite/static/js/admin-planning'
19
- ], function (Mod) {
20
- try { Mod && Mod.init && Mod.init(); } catch (e) { console.error(e); }
21
- });
22
- }
23
-
24
- ready(boot);
25
- document.addEventListener('action:ajaxify.end', boot);
26
- })();
@@ -1,133 +0,0 @@
1
- define('plugins/nodebb-plugin-calendar-onekite/static/js/admin-planning', ['api', 'alerts'], function (api, alerts) {
2
- 'use strict';
3
-
4
- const STATE = { inited:false, fc:null, locations:[], inventory:[], rows:[] };
5
-
6
- function qs(sel, root=document){ return root.querySelector(sel); }
7
- function escapeHtml(s){
8
- return String(s ?? '').replaceAll('&','&amp;').replaceAll('<','&lt;').replaceAll('>','&gt;')
9
- .replaceAll('"','&quot;').replaceAll("'","&#039;");
10
- }
11
-
12
- async function loadInventory() {
13
- const data = await api.get('/calendar/inventory');
14
- STATE.locations = Array.isArray(data?.locations) ? data.locations : [];
15
- STATE.inventory = Array.isArray(data?.inventory) ? data.inventory : [];
16
- }
17
-
18
- async function loadPlanningRows() {
19
- STATE.rows = await api.get('/admin/calendar/planning');
20
- if (!Array.isArray(STATE.rows)) STATE.rows = [];
21
- }
22
-
23
- function fillFilters() {
24
- const locSel = qs('#planning-filter-location');
25
- const itemSel = qs('#planning-filter-item');
26
- if (!locSel || !itemSel) return;
27
-
28
- locSel.innerHTML = '<option value="">Tous</option>' + STATE.locations.map(l =>
29
- `<option value="${escapeHtml(l.id)}">${escapeHtml(l.name||l.id)}</option>`
30
- ).join('');
31
-
32
- itemSel.innerHTML = '<option value="">Tous</option>' + STATE.inventory.map(i =>
33
- `<option value="${escapeHtml(i.id)}">${escapeHtml(i.name||i.id)}</option>`
34
- ).join('');
35
- }
36
-
37
- function colorForStatus(status) {
38
- // don't hardcode colors in code for charts rule? This isn't matplotlib; ok.
39
- if (status === 'paid') return '#d4edda';
40
- if (status === 'awaiting_payment') return '#fff3cd';
41
- if (status === 'pending_admin') return '#cfe2ff';
42
- return '#e2e3e5';
43
- }
44
-
45
- function toFcEvents() {
46
- const loc = qs('#planning-filter-location')?.value || '';
47
- const item = qs('#planning-filter-item')?.value || '';
48
- const status = qs('#planning-filter-status')?.value || '';
49
-
50
- return STATE.rows
51
- .filter(r => !loc || r.locationId === loc)
52
- .filter(r => !item || r.itemId === item)
53
- .filter(r => !status || r.status === status)
54
- .map(r => {
55
- // FullCalendar end is exclusive; add 1 day for all-day style ranges
56
- const end = new Date(r.dateEnd);
57
- end.setDate(end.getDate() + 1);
58
-
59
- return {
60
- id: String(r.rid),
61
- title: `${r.itemName} x${r.quantity} — ${r.pickupLocation} — uid ${r.uid}`,
62
- start: r.dateStart,
63
- end: end.toISOString().slice(0,10),
64
- allDay: true,
65
- backgroundColor: colorForStatus(r.status),
66
- borderColor: colorForStatus(r.status),
67
- extendedProps: r,
68
- };
69
- });
70
- }
71
-
72
- function renderCalendar() {
73
- const el = qs('#planning-calendar');
74
- if (!el) return;
75
- const FC = window.FullCalendar;
76
- if (!FC) return alerts.error('FullCalendar non chargé');
77
-
78
- if (STATE.fc) {
79
- STATE.fc.removeAllEvents();
80
- STATE.fc.addEventSource(toFcEvents());
81
- return;
82
- }
83
-
84
- STATE.fc = new FC.Calendar(el, {
85
- initialView: 'timeGridWeek',
86
- height: 'auto',
87
- locale: 'fr',
88
- firstDay: 1,
89
- headerToolbar: { left: 'prev,next today', center: 'title', right: 'timeGridWeek,dayGridMonth,listWeek' },
90
- events: toFcEvents(),
91
- eventClick: (arg) => {
92
- const r = arg.event.extendedProps || {};
93
- alerts.success(`RID ${r.rid} — ${r.status}`);
94
- },
95
- });
96
- STATE.fc.render();
97
- }
98
-
99
- function bindFilters() {
100
- ['#planning-filter-location', '#planning-filter-item', '#planning-filter-status'].forEach(sel => {
101
- qs(sel)?.addEventListener('change', () => renderCalendar());
102
- });
103
- }
104
-
105
- async function initOnce() {
106
- if (STATE.inited) return;
107
- if (!qs('#planning-calendar')) return;
108
- STATE.inited = true;
109
-
110
- try {
111
- await Promise.all([loadInventory(), loadPlanningRows()]);
112
- fillFilters();
113
- bindFilters();
114
- renderCalendar();
115
- } catch (e) {
116
- alerts.error(e?.message || e);
117
- }
118
- }
119
-
120
- function resetIfLeft() {
121
- if (!qs('#planning-calendar')) {
122
- STATE.inited = false;
123
- STATE.fc = null;
124
- STATE.rows = [];
125
- }
126
- }
127
-
128
- const obs = new MutationObserver(() => { resetIfLeft(); initOnce(); });
129
- obs.observe(document.body, { childList:true, subtree:true });
130
- initOnce();
131
-
132
- return { init: initOnce };
133
- });
@@ -1,26 +0,0 @@
1
- (function () {
2
- 'use strict';
3
-
4
- function ready(fn){
5
- if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', fn);
6
- else fn();
7
- }
8
-
9
- function boot(){
10
- if (!document.querySelector('#calendar-onekite-admin') && !document.querySelector('#calendar-planning')) return;
11
-
12
- if (typeof window.require !== 'function') {
13
- console.warn('[calendar-onekite] require is not defined (ACP)');
14
- return;
15
- }
16
-
17
- window.require([
18
- 'plugins/nodebb-plugin-calendar-onekite/static/js/admin'
19
- ], function (Admin) {
20
- try { Admin && Admin.init && Admin.init(); } catch (e) { console.error(e); }
21
- });
22
- }
23
-
24
- ready(boot);
25
- document.addEventListener('action:ajaxify.end', boot);
26
- })();
@@ -1,193 +0,0 @@
1
- define('plugins/nodebb-plugin-calendar-onekite/static/js/admin', ['api', 'alerts'], function (api, alerts) {
2
- 'use strict';
3
-
4
- const STATE = { initedSettings:false, initedPlanning:false, delegationBound:false };
5
-
6
- function qs(sel, root=document){ return root.querySelector(sel); }
7
- function qsa(sel, root=document){ return Array.from(root.querySelectorAll(sel)); }
8
-
9
- function escapeHtml(s){
10
- return String(s ?? '')
11
- .replaceAll('&','&amp;').replaceAll('<','&lt;').replaceAll('>','&gt;')
12
- .replaceAll('"','&quot;').replaceAll("'","&#039;");
13
- }
14
-
15
- async function saveSettings() {
16
- const locationsJson = qs('#calendar-onekite-locations')?.value || '[]';
17
- const inventoryJson = qs('#calendar-onekite-inventory')?.value || '[]';
18
-
19
- // Validate JSON early to avoid saving broken config
20
- try { JSON.parse(locationsJson || '[]'); } catch { return alerts.error('JSON Lieux invalide'); }
21
- try { JSON.parse(inventoryJson || '[]'); } catch { return alerts.error('JSON Inventaire invalide'); }
22
-
23
- const settings = {
24
- allowedGroups: qs('#calendar-onekite-groups')?.value || '',
25
- allowedBookingGroups: qs('#calendar-onekite-book-groups')?.value || '',
26
- limit: qs('#calendar-onekite-widget-limit')?.value || '',
27
- locationsJson,
28
- inventoryJson,
29
- helloassoApiBase: qs('#calendar-onekite-helloasso-apibase')?.value || '',
30
- helloassoOrganizationSlug: qs('#calendar-onekite-helloasso-org')?.value || '',
31
- helloassoFormSlug: qs('#calendar-onekite-helloasso-form')?.value || '',
32
- helloassoClientId: qs('#calendar-onekite-helloasso-clientid')?.value || '',
33
- helloassoClientSecret: qs('#calendar-onekite-helloasso-secret')?.value || '',
34
- helloassoReturnUrl: qs('#calendar-onekite-helloasso-return')?.value || '',
35
- };
36
-
37
- try {
38
- await api.put('/admin/plugins/calendar-onekite', settings);
39
- alerts.success('Paramètres enregistrés');
40
- } catch (e) {
41
- alerts.error(e?.message || e);
42
- }
43
- }
44
-
45
- function renderPending(container, list) {
46
- if (!Array.isArray(list) || list.length === 0) {
47
- container.innerHTML = '<p>Aucune réservation en attente.</p>';
48
- return;
49
- }
50
-
51
- container.innerHTML = list.map(block => {
52
- const ev = block.event || {};
53
- const reservations = Array.isArray(block.reservations) ? block.reservations : [];
54
- const rows = reservations.map(r => `
55
- <div class="reservation-row mb-2 p-2 border rounded" data-rid="${escapeHtml(r.rid)}">
56
- <p><strong>RID :</strong> ${escapeHtml(r.rid)}</p>
57
- <p><strong>UID :</strong> ${escapeHtml(r.uid)}</p>
58
- <p><strong>Matériel :</strong> ${escapeHtml(r.itemId)}</p>
59
- <p><strong>Quantité :</strong> ${escapeHtml(r.quantity)}</p>
60
- <p><strong>Dates :</strong> ${escapeHtml(r.dateStart)} → ${escapeHtml(r.dateEnd)} (${escapeHtml(r.days||'')} jours)</p>
61
- <p><strong>Lieu de retrait :</strong> ${escapeHtml(r.pickupLocation || 'Non précisé')}</p>
62
- <button class="btn btn-success btn-sm js-validate">Valider</button>
63
- <button class="btn btn-danger btn-sm js-cancel">Annuler</button>
64
- </div>
65
- `).join('');
66
-
67
- return `
68
- <div class="card mb-3">
69
- <div class="card-header">
70
- <strong>${escapeHtml(ev.title)}</strong><br>
71
- <small>${escapeHtml(ev.start)} → ${escapeHtml(ev.end)}</small>
72
- </div>
73
- <div class="card-body">${rows}</div>
74
- </div>
75
- `;
76
- }).join('');
77
- }
78
-
79
- async function loadPending() {
80
- const container = qs('#pending-reservations');
81
- if (!container) return;
82
- try {
83
- const list = await api.get('/admin/calendar/pending');
84
- renderPending(container, list);
85
- } catch (e) {
86
- alerts.error(e?.message || e);
87
- }
88
- }
89
-
90
- async function validateReservation(rid, row) {
91
- try {
92
- await api.post(`/admin/calendar/reservation/${encodeURIComponent(rid)}/validate`, {});
93
- row.style.background = '#d4edda';
94
- qsa('.js-validate, .js-cancel', row).forEach(b => b.remove());
95
- row.insertAdjacentHTML('beforeend', '<p><strong>Validée.</strong> Lien de paiement envoyé.</p>');
96
- } catch (e) {
97
- alerts.error(e?.message || e);
98
- }
99
- }
100
-
101
- async function cancelReservation(rid, row) {
102
- if (!window.confirm('Annuler cette réservation ?')) return;
103
- try {
104
- await api.post(`/admin/calendar/reservation/${encodeURIComponent(rid)}/cancel`, {});
105
- row.style.background = '#f8d7da';
106
- qsa('.js-validate, .js-cancel', row).forEach(b => b.remove());
107
- row.insertAdjacentHTML('beforeend', '<p><strong>Annulée.</strong></p>');
108
- } catch (e) {
109
- alerts.error(e?.message || e);
110
- }
111
- }
112
-
113
- function bindDelegationOnce() {
114
- if (STATE.delegationBound) return;
115
- STATE.delegationBound = true;
116
-
117
- document.addEventListener('click', (ev) => {
118
- const t = ev.target;
119
- if (!(t instanceof HTMLElement)) return;
120
-
121
- const row = t.closest('.reservation-row');
122
- if (!row) return;
123
-
124
- const rid = row.getAttribute('data-rid');
125
- if (!rid) return;
126
-
127
- if (t.classList.contains('js-validate')) validateReservation(rid, row);
128
- if (t.classList.contains('js-cancel')) cancelReservation(rid, row);
129
- }, { passive: true });
130
- }
131
-
132
- async function loadPlanning() {
133
- const tbody = qs('#planning-body');
134
- if (!tbody) return;
135
-
136
- try {
137
- const rows = await api.get('/admin/calendar/planning');
138
- if (!rows || !rows.length) {
139
- tbody.innerHTML = '<tr><td colspan="9">Aucune réservation future.</td></tr>';
140
- return;
141
- }
142
- tbody.innerHTML = rows.map(r => `
143
- <tr>
144
- <td>${escapeHtml(r.eventTitle)}</td>
145
- <td>${escapeHtml(r.itemName)}</td>
146
- <td>${escapeHtml(r.pickupLocation || 'Non précisé')}</td>
147
- <td>${escapeHtml(r.uid)}</td>
148
- <td>${escapeHtml(r.quantity)}</td>
149
- <td>${escapeHtml(r.dateStart)}</td>
150
- <td>${escapeHtml(r.dateEnd)}</td>
151
- <td>${escapeHtml(r.days)}</td>
152
- <td>${escapeHtml(r.status)}</td>
153
- </tr>
154
- `).join('');
155
- } catch (e) {
156
- alerts.error(e?.message || e);
157
- }
158
- }
159
-
160
- function initSettingsPage() {
161
- const root = qs('#calendar-onekite-admin');
162
- if (!root || STATE.initedSettings) return;
163
- STATE.initedSettings = true;
164
-
165
- qs('#calendar-onekite-save')?.addEventListener('click', saveSettings);
166
- bindDelegationOnce();
167
- loadPending();
168
- }
169
-
170
- function initPlanningPage() {
171
- const root = qs('#calendar-planning');
172
- if (!root || STATE.initedPlanning) return;
173
- STATE.initedPlanning = true;
174
- loadPlanning();
175
- }
176
-
177
- function resetFlagsIfLeft() {
178
- if (!qs('#calendar-onekite-admin')) STATE.initedSettings = false;
179
- if (!qs('#calendar-planning')) STATE.initedPlanning = false;
180
- }
181
-
182
- const obs = new MutationObserver(() => {
183
- resetFlagsIfLeft();
184
- initSettingsPage();
185
- initPlanningPage();
186
- });
187
- obs.observe(document.body, { childList: true, subtree: true });
188
-
189
- initSettingsPage();
190
- initPlanningPage();
191
-
192
- return {};
193
- });
@@ -1,55 +0,0 @@
1
- define('plugins/nodebb-plugin-calendar-onekite/static/js/my-reservations', ['api', 'alerts'], function (api, alerts) {
2
- 'use strict';
3
- const STATE = { inited:false };
4
-
5
- function qs(sel, root=document){ return root.querySelector(sel); }
6
- function escapeHtml(s){
7
- return String(s ?? '')
8
- .replaceAll('&','&amp;').replaceAll('<','&lt;').replaceAll('>','&gt;')
9
- .replaceAll('"','&quot;').replaceAll("'","&#039;");
10
- }
11
-
12
- async function load() {
13
- const root = qs('#calendar-my-reservations');
14
- const tbody = qs('#my-reservations-body');
15
- if (!root || !tbody) return;
16
-
17
- try {
18
- const rows = await api.get('/calendar/my-reservations');
19
- if (!rows || !rows.length) {
20
- tbody.innerHTML = '<tr><td colspan="7">Aucune réservation.</td></tr>';
21
- return;
22
- }
23
- tbody.innerHTML = rows.map(r => `
24
- <tr>
25
- <td>${escapeHtml(r.eventTitle)}</td>
26
- <td>${escapeHtml(r.itemName)}</td>
27
- <td>${escapeHtml(r.pickupLocation || '')}</td>
28
- <td>${escapeHtml(r.dateStart)}</td>
29
- <td>${escapeHtml(r.dateEnd)}</td>
30
- <td>${escapeHtml(r.days || '')}</td>
31
- <td>${escapeHtml(r.status)}</td>
32
- </tr>
33
- `).join('');
34
- } catch (e) {
35
- alerts.error(e?.message || e);
36
- }
37
- }
38
-
39
- function initOnce() {
40
- if (STATE.inited) return;
41
- if (!qs('#calendar-my-reservations')) return;
42
- STATE.inited = true;
43
- load();
44
- }
45
-
46
- function resetIfLeft() {
47
- if (!qs('#calendar-my-reservations')) STATE.inited = false;
48
- }
49
-
50
- const obs = new MutationObserver(() => { resetIfLeft(); initOnce(); });
51
- obs.observe(document.body, { childList:true, subtree:true });
52
- initOnce();
53
-
54
- return { init: initOnce };
55
- });
@@ -1,63 +0,0 @@
1
- (function () {
2
- 'use strict';
3
-
4
- function log(...a){ try{ console.log('[calendar-onekite]', ...a);}catch{} }
5
- function warn(...a){ try{ console.warn('[calendar-onekite]', ...a);}catch{} }
6
-
7
- function ready(fn){
8
- if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', fn);
9
- else fn();
10
- }
11
-
12
- function runWithModules(mods, cb){
13
- if (typeof window.require === 'function') {
14
- window.require(mods, function(){ cb.apply(null, arguments); });
15
- return true;
16
- }
17
- return false;
18
- }
19
-
20
- async function fetchJson(path, method='GET', body){
21
- const url = path.startsWith('/api/') || path.startsWith('/api/v3/') ? path : ('/api/v3' + path);
22
- const headers = { 'Content-Type':'application/json' };
23
- const csrf = (window.ajaxify && window.ajaxify.data && window.ajaxify.data.csrf_token) || null;
24
- if (csrf) headers['x-csrf-token'] = csrf;
25
- const res = await fetch(url, { method, headers, credentials:'same-origin', body: body ? JSON.stringify(body): undefined });
26
- if (!res.ok) {
27
- let msg = res.statusText;
28
- try { const j = await res.json(); msg = j.error || j.message || msg; } catch {}
29
- const e = new Error(msg); e.status=res.status; throw e;
30
- }
31
- return res.json();
32
- }
33
-
34
- function initCalendar(api, alerts){
35
- // use the existing calendar module (AMD) if present; else minimal inline init
36
- if (runWithModules(['plugins/nodebb-plugin-calendar-onekite/static/js/calendar'], function(Calendar){
37
- if (Calendar && Calendar.init) Calendar.init();
38
- })) return;
39
-
40
- // Fallback: we don't have AMD loader, do nothing but tell why
41
- warn('AMD require introuvable, impossible d\'initialiser le calendrier.');
42
- if (alerts && alerts.error) alerts.error('Calendrier: require introuvable (scripts NodeBB non chargés)');
43
- }
44
-
45
- function boot(){
46
- if (!document.querySelector('#calendar')) return;
47
- // Ensure FullCalendar loaded
48
- if (!window.FullCalendar) {
49
- warn('FullCalendar non chargé (CDN bloqué ?).');
50
- }
51
- runWithModules(['api', 'alerts'], function(api, alerts){
52
- initCalendar(api, alerts);
53
- }) || initCalendar(null, null);
54
- }
55
-
56
- ready(boot);
57
-
58
- // NodeBB ajaxify navigation
59
- if (window.socket && window.socket.on) {
60
- // do nothing
61
- }
62
- document.addEventListener('action:ajaxify.end', boot);
63
- })();