nodebb-plugin-onekite-calendar 1.0.2 → 1.0.4

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/lib/log.js ADDED
@@ -0,0 +1,29 @@
1
+ 'use strict';
2
+
3
+ // Minimal, opt-in logging.
4
+ // Enable by setting ONEKITE_CALENDAR_DEBUG=1 in the NodeBB process env.
5
+
6
+ let logger;
7
+ try {
8
+ logger = require.main.require('./src/logger');
9
+ } catch (e) {
10
+ logger = null;
11
+ }
12
+
13
+ const enabled = () => !!process.env.ONEKITE_CALENDAR_DEBUG;
14
+
15
+ function warn(msg, meta) {
16
+ if (!enabled()) return;
17
+ if (logger && typeof logger.warn === 'function') {
18
+ logger.warn(`[onekite-calendar] ${msg}`, meta || '');
19
+ }
20
+ }
21
+
22
+ function error(msg, meta) {
23
+ if (!enabled()) return;
24
+ if (logger && typeof logger.error === 'function') {
25
+ logger.error(`[onekite-calendar] ${msg}`, meta || '');
26
+ }
27
+ }
28
+
29
+ module.exports = { warn, error };
package/lib/scheduler.js CHANGED
@@ -3,6 +3,7 @@
3
3
  const meta = require.main.require('./src/meta');
4
4
  const db = require.main.require('./src/database');
5
5
  const dbLayer = require('./db');
6
+ const log = require('./log');
6
7
 
7
8
  let timer = null;
8
9
 
@@ -15,7 +16,7 @@ function getSetting(settings, key, fallback) {
15
16
 
16
17
  // Pending holds: short lock after a user creates a request (defaults to 5 minutes)
17
18
  async function expirePending() {
18
- const settings = await meta.settings.get('calendar-onekite');
19
+ const settings = await meta.settings.get('onekite-calendar');
19
20
  const holdMins = parseInt(getSetting(settings, 'pendingHoldMinutes', '5'), 10) || 5;
20
21
  const now = Date.now();
21
22
 
@@ -43,7 +44,7 @@ async function expirePending() {
43
44
  // - We send a reminder after `paymentHoldMinutes` (default 60)
44
45
  // - We expire (and remove) after `2 * paymentHoldMinutes`
45
46
  async function processAwaitingPayment() {
46
- const settings = await meta.settings.get('calendar-onekite');
47
+ const settings = await meta.settings.get('onekite-calendar');
47
48
  const holdMins = parseInt(
48
49
  getSetting(settings, 'paymentHoldMinutes', getSetting(settings, 'holdMinutes', '60')),
49
50
  10
@@ -83,11 +84,7 @@ async function processAwaitingPayment() {
83
84
  }
84
85
  } catch (err) {
85
86
  // eslint-disable-next-line no-console
86
- console.warn('[calendar-onekite] Failed to send email (scheduler)', {
87
- template,
88
- toEmail,
89
- err: String((err && err.message) || err),
90
- });
87
+ log.warn('Failed to send email (scheduler)', { template, toEmail, err: String((err && err.message) || err) });
91
88
  }
92
89
  }
93
90
 
@@ -111,7 +108,7 @@ async function processAwaitingPayment() {
111
108
 
112
109
  if (!r.reminderSent && now >= reminderAt && now < expireAt) {
113
110
  // Send reminder once (guarded across clustered NodeBB processes)
114
- const reminderKey = 'calendar-onekite:email:reminderSent';
111
+ const reminderKey = 'onekite-calendar:email:reminderSent';
115
112
  const first = await db.setAdd(reminderKey, rid);
116
113
  if (!first) {
117
114
  // another process already sent it
@@ -122,7 +119,7 @@ async function processAwaitingPayment() {
122
119
  }
123
120
  const u = await user.getUserFields(r.uid, ['username', 'email']);
124
121
  if (u && u.email) {
125
- await sendEmail('calendar-onekite_reminder', u.email, 'Location matériel - Rappel', {
122
+ await sendEmail('onekite-calendar_reminder', u.email, 'Location matériel - Rappel', {
126
123
  username: u.username,
127
124
  itemName: (Array.isArray(r.itemNames) ? r.itemNames.join(', ') : (r.itemName || '')),
128
125
  itemNames: (Array.isArray(r.itemNames) ? r.itemNames : (r.itemName ? [r.itemName] : [])),
@@ -141,12 +138,12 @@ async function processAwaitingPayment() {
141
138
  if (now >= expireAt) {
142
139
  // Expire: remove reservation so it disappears from calendar and frees items
143
140
  // Guard email send across clustered NodeBB processes
144
- const expiredKey = 'calendar-onekite:email:expiredSent';
141
+ const expiredKey = 'onekite-calendar:email:expiredSent';
145
142
  const firstExpired = await db.setAdd(expiredKey, rid);
146
143
  const shouldEmail = !!firstExpired;
147
144
  const u = await user.getUserFields(r.uid, ['username', 'email']);
148
145
  if (shouldEmail && u && u.email) {
149
- await sendEmail('calendar-onekite_expired', u.email, 'Location matériel - Rappel', {
146
+ await sendEmail('onekite-calendar_expired', u.email, 'Location matériel - Rappel', {
150
147
  username: u.username,
151
148
  itemName: (Array.isArray(r.itemNames) ? r.itemNames.join(', ') : (r.itemName || '')),
152
149
  itemNames: (Array.isArray(r.itemNames) ? r.itemNames : (r.itemName ? [r.itemName] : [])),
package/lib/widgets.js CHANGED
@@ -40,8 +40,8 @@ widgets.defineWidgets = async function (widgetData) {
40
40
  }
41
41
 
42
42
  list.push({
43
- widget: 'calendar-onekite-twoweeks',
44
- name: 'Calendrier OneKite (2 semaines)',
43
+ widget: 'onekite-calendar-twoweeks',
44
+ name: 'Calendrier Onekite',
45
45
  description: 'Affiche la semaine courante + la semaine suivante (FullCalendar via CDN).',
46
46
  content: '',
47
47
  });
@@ -54,7 +54,7 @@ widgets.renderTwoWeeksWidget = async function (data) {
54
54
  const id = makeDomId();
55
55
  const calUrl = widgetCalendarUrl();
56
56
  const apiBase = forumBaseUrl();
57
- const eventsEndpoint = `${apiBase}/api/v3/plugins/calendar-onekite/events`;
57
+ const eventsEndpoint = `${apiBase}/api/v3/plugins/onekite-calendar/events`;
58
58
 
59
59
  const idJson = JSON.stringify(id);
60
60
  const calUrlJson = JSON.stringify(calUrl);
@@ -63,7 +63,7 @@ widgets.renderTwoWeeksWidget = async function (data) {
63
63
  const html = `
64
64
  <div class="onekite-twoweeks">
65
65
  <div class="d-flex justify-content-between align-items-center mb-1">
66
- <div style="font-weight: 600;">Calendrier (2 semaines)</div>
66
+ <div style="font-weight: 600;">Calendrier</div>
67
67
  <a href="${escapeHtml(calUrl)}" class="btn btn-sm btn-outline-secondary" style="line-height: 1.1;">Ouvrir</a>
68
68
  </div>
69
69
  <div id="${escapeHtml(id)}"></div>
@@ -137,21 +137,130 @@ widgets.renderTwoWeeksWidget = async function (data) {
137
137
  },
138
138
  navLinks: false,
139
139
  eventTimeFormat: { hour: '2-digit', minute: '2-digit', hour12: false },
140
+ eventContent: function(arg) {
141
+ // Render a simple dot instead of text.
142
+ const wrap = document.createElement('span');
143
+ wrap.className = 'onekite-dot';
144
+ wrap.setAttribute('aria-label', arg.event.title || '');
145
+ // Mark the node so eventDidMount can find it.
146
+ wrap.setAttribute('data-onekite-dot', '1');
147
+ return { domNodes: [wrap] };
148
+ },
140
149
  events: function(info, successCallback, failureCallback) {
141
150
  const qs = new URLSearchParams({ start: info.startStr, end: info.endStr });
142
- fetch(eventsEndpoint + '?' + qs.toString(), { credentials: 'same-origin' })
151
+ const url1 = eventsEndpoint + '?' + qs.toString();
152
+ fetch(url1, { credentials: 'same-origin' })
143
153
  .then((r) => r.json())
144
154
  .then((json) => successCallback(json || []))
145
155
  .catch((e) => failureCallback(e));
146
156
  },
157
+ eventDidMount: function(info) {
158
+ // Native tooltip + click popup (Bootbox when available).
159
+ const title = info.event && info.event.title ? String(info.event.title) : '';
160
+ const start = info.event && info.event.start ? info.event.start : null;
161
+ const end = info.event && info.event.end ? info.event.end : null;
162
+ const fmt = (d) => {
163
+ try {
164
+ return new Intl.DateTimeFormat('fr-FR', { dateStyle: 'medium' }).format(d);
165
+ } catch (e) {
166
+ return d ? d.toISOString().slice(0, 10) : '';
167
+ }
168
+ };
169
+ const period = start ? (end ? (fmt(start) + ' → ' + fmt(end)) : fmt(start)) : '';
170
+ const body = [title, period].filter(Boolean).join('\n');
171
+ if (info.el) {
172
+ // Make dot color match the FullCalendar event colors.
173
+ try {
174
+ const dot = info.el.querySelector && info.el.querySelector('[data-onekite-dot="1"]');
175
+ if (dot) {
176
+ // Prefer explicit event colors, otherwise fall back to computed styles.
177
+ const bg = (info.event && (info.event.backgroundColor || info.event.borderColor)) || '';
178
+ if (bg) {
179
+ dot.style.backgroundColor = bg;
180
+ } else {
181
+ const cs = window.getComputedStyle(info.el);
182
+ const bgs = cs && cs.backgroundColor;
183
+ if (bgs && bgs !== 'rgba(0, 0, 0, 0)' && bgs !== 'transparent') {
184
+ dot.style.backgroundColor = bgs;
185
+ }
186
+ }
187
+ }
188
+ } catch (e) {
189
+ // ignore
190
+ }
191
+
192
+ info.el.title = body;
193
+ info.el.style.cursor = 'pointer';
194
+ info.el.addEventListener('click', function(ev) {
195
+ ev.preventDefault();
196
+ ev.stopPropagation();
197
+ const html = '<div style="white-space:pre-line;">' + (body || '').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;') + '</div>';
198
+ if (window.bootbox && typeof window.bootbox.alert === 'function') {
199
+ window.bootbox.alert({ message: html });
200
+ } else {
201
+ // Fallback
202
+ alert(body);
203
+ }
204
+ }, { passive: false });
205
+ }
206
+ },
207
+ // On mobile, users can tap the calendar background to open the full page.
147
208
  dateClick: function() {
148
209
  window.location.href = calUrl;
149
210
  },
150
- eventClick: function() {
151
- window.location.href = calUrl;
211
+ // Do not redirect on eventClick: we show a popup in eventDidMount.
212
+ eventClick: function(info) {
213
+ try { if (info && info.jsEvent) info.jsEvent.preventDefault(); } catch (e) {}
152
214
  },
153
215
  });
154
216
 
217
+ // Mobile: swipe left/right to change week (2-week view).
218
+ (function enableSwipe() {
219
+ try {
220
+ if (!('ontouchstart' in window)) return;
221
+ const target = el; // calendar root
222
+ let sx = 0;
223
+ let sy = 0;
224
+ let st = 0;
225
+ let tracking = false;
226
+
227
+ target.addEventListener('touchstart', function (e) {
228
+ if (!e.touches || e.touches.length !== 1) return;
229
+ const t = e.touches[0];
230
+ sx = t.clientX;
231
+ sy = t.clientY;
232
+ st = Date.now();
233
+ tracking = true;
234
+ }, { passive: true });
235
+
236
+ target.addEventListener('touchend', function (e) {
237
+ if (!tracking) return;
238
+ tracking = false;
239
+ const changed = e.changedTouches && e.changedTouches[0];
240
+ if (!changed) return;
241
+ const dx = changed.clientX - sx;
242
+ const dy = changed.clientY - sy;
243
+ const adx = Math.abs(dx);
244
+ const ady = Math.abs(dy);
245
+ const dt = Date.now() - st;
246
+
247
+ // Must be a quick-ish horizontal swipe.
248
+ if (dt > 800) return;
249
+ if (adx < 50) return;
250
+ if (ady > adx * 0.75) return;
251
+
252
+ // Move by one week per swipe, even though the view spans 2 weeks.
253
+ if (dx < 0) {
254
+ calendar.incrementDate({ weeks: 1 });
255
+ } else {
256
+ calendar.incrementDate({ weeks: -1 });
257
+ }
258
+ }, { passive: true });
259
+ } catch (e) {
260
+ // ignore
261
+ }
262
+ })();
263
+
155
264
  calendar.render();
156
265
  }
157
266
 
@@ -165,7 +274,18 @@ widgets.renderTwoWeeksWidget = async function (data) {
165
274
  .onekite-twoweeks .fc .fc-button { padding: .2rem .35rem; font-size: .75rem; }
166
275
  .onekite-twoweeks .fc .fc-daygrid-day-number { font-size: .75rem; padding: 2px; }
167
276
  .onekite-twoweeks .fc .fc-col-header-cell-cushion { font-size: .75rem; }
168
- .onekite-twoweeks .fc .fc-event-title { font-size: .72rem; }
277
+ .onekite-twoweeks .fc .fc-event-title { display: none; }
278
+ .onekite-twoweeks .fc .fc-event-time { display: none; }
279
+ .onekite-twoweeks .onekite-dot {
280
+ display: inline-block;
281
+ width: 8px;
282
+ height: 8px;
283
+ border-radius: 50%;
284
+ background: var(--fc-event-bg-color, currentColor);
285
+ border: 1px solid var(--fc-event-border-color, transparent);
286
+ opacity: 0.85;
287
+ margin: 0 2px;
288
+ }
169
289
  </style>
170
290
  `;
171
291
 
package/library.js CHANGED
@@ -1,10 +1,5 @@
1
1
  'use strict';
2
2
 
3
- // We use NodeBB's route helpers for page routes so the rendered page includes
4
- // the normal client bundle (ajaxify/requirejs). The helper signatures differ
5
- // across NodeBB versions, but for NodeBB v4.x the order is:
6
- // setupPageRoute(router, path, middlewaresArray, handler)
7
- // setupAdminPageRoute(router, path, middlewaresArray, handler)
8
3
  const routeHelpers = require.main.require('./src/routes/helpers');
9
4
 
10
5
  const controllers = require('./lib/controllers.js');
@@ -13,6 +8,7 @@ const admin = require('./lib/admin');
13
8
  const scheduler = require('./lib/scheduler');
14
9
  const helloassoWebhook = require('./lib/helloassoWebhook');
15
10
  const widgets = require('./lib/widgets');
11
+ const { NAMESPACE } = require('./lib/constants');
16
12
  const bodyParser = require('body-parser');
17
13
 
18
14
  const Plugin = {};
@@ -23,16 +19,8 @@ const mw = (...fns) => fns.filter(isFn);
23
19
  Plugin.init = async function (params) {
24
20
  const { router, middleware } = params;
25
21
 
26
- // Build middleware arrays safely and always spread them into Express route methods.
27
- // Express will throw if any callback is undefined, so we filter strictly.
28
- // Auth middlewares differ slightly depending on NodeBB configuration.
29
- // In v4, some installs rely on middleware.authenticate rather than exposeUid.
30
- const baseExpose = mw(middleware && (middleware.authenticate || middleware.exposeUid));
31
- const publicExpose = baseExpose;
32
- const publicAuth = mw(middleware && (middleware.authenticate || middleware.exposeUid), middleware && middleware.ensureLoggedIn);
22
+ const publicExpose = mw(middleware && (middleware.authenticate || middleware.exposeUid));
33
23
 
34
- // Robust admin guard: avoid middleware.admin.checkPrivileges() signature differences
35
- // across NodeBB versions. We treat membership in the 'administrators' group as admin.
36
24
  const Groups = require.main.require('./src/groups');
37
25
  async function adminOnly(req, res, next) {
38
26
  try {
@@ -49,6 +37,7 @@ Plugin.init = async function (params) {
49
37
  return next(err);
50
38
  }
51
39
  }
40
+
52
41
  const adminMws = mw(
53
42
  middleware && (middleware.authenticate || middleware.exposeUid),
54
43
  middleware && middleware.ensureLoggedIn,
@@ -56,67 +45,58 @@ Plugin.init = async function (params) {
56
45
  );
57
46
 
58
47
  // Page routes (HTML)
59
- // IMPORTANT: pass an ARRAY for middlewares (even if empty), otherwise
60
- // setupPageRoute will throw "middlewares is not iterable".
61
48
  routeHelpers.setupPageRoute(router, '/calendar', mw(), controllers.renderCalendar);
62
- routeHelpers.setupAdminPageRoute(router, '/admin/plugins/calendar-onekite', mw(), admin.renderAdmin);
63
49
 
64
- // Public API (JSON) — NodeBB 4.x only (v3 API)
65
- router.get('/api/v3/plugins/calendar-onekite/events', ...publicExpose, api.getEvents);
66
- router.get('/api/v3/plugins/calendar-onekite/items', ...publicExpose, api.getItems);
67
- router.get('/api/v3/plugins/calendar-onekite/capabilities', ...publicExpose, api.getCapabilities);
50
+ // Admin page route
51
+ routeHelpers.setupAdminPageRoute(router, `/admin/plugins/${NAMESPACE}`, mw(), admin.renderAdmin);
52
+
53
+ // Public API (JSON) — NodeBB 4.x (v3 API)
54
+ router.get(`/api/v3/plugins/${NAMESPACE}/events`, ...publicExpose, api.getEvents);
55
+ router.get(`/api/v3/plugins/${NAMESPACE}/items`, ...publicExpose, api.getItems);
56
+ router.get(`/api/v3/plugins/${NAMESPACE}/capabilities`, ...publicExpose, api.getCapabilities);
68
57
 
69
- router.post('/api/v3/plugins/calendar-onekite/reservations', ...publicExpose, api.createReservation);
70
- router.get('/api/v3/plugins/calendar-onekite/reservations/:rid', ...publicExpose, api.getReservationDetails);
71
- router.put('/api/v3/plugins/calendar-onekite/reservations/:rid/approve', ...publicExpose, api.approveReservation);
72
- router.put('/api/v3/plugins/calendar-onekite/reservations/:rid/refuse', ...publicExpose, api.refuseReservation);
73
- router.put('/api/v3/plugins/calendar-onekite/reservations/:rid/cancel', ...publicExpose, api.cancelReservation);
58
+ router.post(`/api/v3/plugins/${NAMESPACE}/reservations`, ...publicExpose, api.createReservation);
59
+ router.get(`/api/v3/plugins/${NAMESPACE}/reservations/:rid`, ...publicExpose, api.getReservationDetails);
60
+ router.put(`/api/v3/plugins/${NAMESPACE}/reservations/:rid/approve`, ...publicExpose, api.approveReservation);
61
+ router.put(`/api/v3/plugins/${NAMESPACE}/reservations/:rid/refuse`, ...publicExpose, api.refuseReservation);
62
+ router.put(`/api/v3/plugins/${NAMESPACE}/reservations/:rid/cancel`, ...publicExpose, api.cancelReservation);
74
63
 
75
- router.post('/api/v3/plugins/calendar-onekite/special-events', ...publicExpose, api.createSpecialEvent);
76
- router.get('/api/v3/plugins/calendar-onekite/special-events/:eid', ...publicExpose, api.getSpecialEventDetails);
77
- router.delete('/api/v3/plugins/calendar-onekite/special-events/:eid', ...publicExpose, api.deleteSpecialEvent);
64
+ router.post(`/api/v3/plugins/${NAMESPACE}/special-events`, ...publicExpose, api.createSpecialEvent);
65
+ router.get(`/api/v3/plugins/${NAMESPACE}/special-events/:eid`, ...publicExpose, api.getSpecialEventDetails);
66
+ router.delete(`/api/v3/plugins/${NAMESPACE}/special-events/:eid`, ...publicExpose, api.deleteSpecialEvent);
78
67
 
79
68
  // Admin API (JSON)
80
- const adminBases = ['/api/v3/admin/plugins/calendar-onekite'];
69
+ const base = `/api/v3/admin/plugins/${NAMESPACE}`;
81
70
 
82
- adminBases.forEach((base) => {
83
- router.get(`${base}/settings`, ...adminMws, admin.getSettings);
84
- router.put(`${base}/settings`, ...adminMws, admin.saveSettings);
71
+ router.get(`${base}/settings`, ...adminMws, admin.getSettings);
72
+ router.put(`${base}/settings`, ...adminMws, admin.saveSettings);
85
73
 
86
- router.get(`${base}/pending`, ...adminMws, admin.listPending);
87
- router.put(`${base}/reservations/:rid/approve`, ...adminMws, admin.approveReservation);
88
- router.put(`${base}/reservations/:rid/refuse`, ...adminMws, admin.refuseReservation);
74
+ router.get(`${base}/pending`, ...adminMws, admin.listPending);
75
+ router.put(`${base}/reservations/:rid/approve`, ...adminMws, admin.approveReservation);
76
+ router.put(`${base}/reservations/:rid/refuse`, ...adminMws, admin.refuseReservation);
89
77
 
90
- router.post(`${base}/purge`, ...adminMws, admin.purgeByYear);
91
- router.get(`${base}/debug`, ...adminMws, admin.debugHelloAsso);
92
- // Accounting / exports
93
- router.get(`${base}/accounting`, ...adminMws, admin.getAccounting);
94
- router.get(`${base}/accounting.csv`, ...adminMws, admin.exportAccountingCsv);
95
- router.post(`${base}/accounting/purge`, ...adminMws, admin.purgeAccounting);
78
+ router.post(`${base}/purge`, ...adminMws, admin.purgeByYear);
96
79
 
97
- // Purge special events by year
98
- router.post(`${base}/special-events/purge`, ...adminMws, admin.purgeSpecialEventsByYear);
99
- });
80
+ router.get(`${base}/accounting`, ...adminMws, admin.getAccounting);
81
+ router.get(`${base}/accounting.csv`, ...adminMws, admin.exportAccountingCsv);
82
+ router.post(`${base}/accounting/purge`, ...adminMws, admin.purgeAccounting);
100
83
 
101
- // HelloAsso callback endpoint (hardened)
102
- // - Only accepts POST
103
- // - Verifies x-ha-signature (HMAC SHA-256) using the configured client secret
104
- // - Basic replay protection
105
- // NOTE: we capture the raw body for signature verification.
84
+ router.post(`${base}/special-events/purge`, ...adminMws, admin.purgeSpecialEventsByYear);
85
+
86
+ // HelloAsso webhook endpoint (signature verified)
106
87
  const helloassoJson = bodyParser.json({
107
88
  verify: (req, _res, buf) => {
108
89
  req.rawBody = buf;
109
90
  },
110
91
  type: ['application/json', 'application/*+json'],
111
92
  });
112
- // Accept webhook on both legacy root path and namespaced plugin path.
113
- // Some reverse proxies block unknown root paths, so /plugins/... is recommended.
93
+
114
94
  router.post('/helloasso', helloassoJson, helloassoWebhook.handler);
115
- router.post('/plugins/calendar-onekite/helloasso', helloassoJson, helloassoWebhook.handler);
95
+ router.post(`/plugins/${NAMESPACE}/helloasso`, helloassoJson, helloassoWebhook.handler);
116
96
 
117
- // Optional: health checks
97
+ // Optional health checks
118
98
  router.get('/helloasso', (req, res) => res.json({ ok: true }));
119
- router.get('/plugins/calendar-onekite/helloasso', (req, res) => res.json({ ok: true }));
99
+ router.get(`/plugins/${NAMESPACE}/helloasso`, (req, res) => res.json({ ok: true }));
120
100
 
121
101
  scheduler.start();
122
102
  };
@@ -124,24 +104,20 @@ Plugin.init = async function (params) {
124
104
  Plugin.addAdminNavigation = async function (header) {
125
105
  header.plugins = header.plugins || [];
126
106
  header.plugins.push({
127
- route: '/plugins/calendar-onekite',
107
+ route: `/plugins/${NAMESPACE}`,
128
108
  icon: 'fa-calendar',
129
- name: 'Calendar OneKite',
109
+ name: 'Onekite Calendar',
130
110
  });
131
111
  return header;
132
112
  };
133
113
 
134
-
135
114
  // Ensure our transactional emails always get a subject.
136
- // NodeBB's Emailer.sendToEmail signature expects (template, email, language, params),
137
- // so plugins typically inject/modify the subject via this hook.
138
115
  Plugin.emailModify = async function (data) {
139
116
  try {
140
117
  if (!data || !data.template) return data;
141
118
  const tpl = String(data.template);
142
- if (!tpl.startsWith('calendar-onekite_')) return data;
119
+ if (!tpl.startsWith('onekite-calendar_')) return data;
143
120
 
144
- // If the caller provided a subject (we pass it in params.subject), copy it to data.subject.
145
121
  const provided = data.params && data.params.subject ? String(data.params.subject) : '';
146
122
  if (provided && (!data.subject || !String(data.subject).trim())) {
147
123
  data.subject = provided;
@@ -154,4 +130,4 @@ Plugin.emailModify = async function (data) {
154
130
  Plugin.defineWidgets = widgets.defineWidgets;
155
131
  Plugin.renderTwoWeeksWidget = widgets.renderTwoWeeksWidget;
156
132
 
157
- module.exports = Plugin;
133
+ module.exports = Plugin;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-onekite-calendar",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "description": "FullCalendar-based equipment reservation workflow with admin approval & HelloAsso payment for NodeBB",
5
5
  "main": "library.js",
6
6
  "license": "MIT",
package/plugin.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "nodebb-plugin-onekite-calendar",
3
- "name": "Calendar OneKite",
3
+ "name": "Onekite Calendar",
4
4
  "description": "Equipment reservation calendar (FullCalendar) with admin approval & HelloAsso checkout",
5
5
  "url": "https://www.onekite.com/calendar",
6
6
  "hooks": [
@@ -21,7 +21,7 @@
21
21
  "method": "defineWidgets"
22
22
  },
23
23
  {
24
- "hook": "filter:widget.render:calendar-onekite-twoweeks",
24
+ "hook": "filter:widget.render:onekite-calendar-twoweeks",
25
25
  "method": "renderTwoWeeksWidget"
26
26
  }
27
27
  ],
@@ -30,14 +30,8 @@
30
30
  },
31
31
  "templates": "./templates",
32
32
  "modules": {
33
- "../admin/plugins/calendar-onekite.js": "./public/admin.js",
34
- "admin/plugins/calendar-onekite": "./public/admin.js"
33
+ "admin/plugins/onekite-calendar": "./public/admin.js",
34
+ "forum/onekite-calendar": "./public/client.js"
35
35
  },
36
- "scripts": [
37
- "public/client.js"
38
- ],
39
- "acpScripts": [
40
- "public/admin.js"
41
- ],
42
- "version": "1.0.2"
36
+ "version": "1.1.0"
43
37
  }