nodebb-plugin-onekite-calendar 1.0.9 → 1.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/lib/api.js CHANGED
@@ -134,13 +134,21 @@ function formatFR(tsOrIso) {
134
134
  }
135
135
 
136
136
  function buildHelloAssoItemName(baseLabel, itemNames, start, end) {
137
- const base = String(baseLabel || 'Réservation matériel Onekite').trim();
137
+ const base = String(baseLabel || '').trim();
138
138
  const items = Array.isArray(itemNames) ? itemNames.map((s) => String(s || '').trim()).filter(Boolean) : [];
139
- const range = (start && end) ? `Du ${formatFR(start)} au ${formatFR(end)}` : '';
140
- const lines = [base];
141
- items.forEach((it) => lines.push(`• ${it}`));
142
- if (range) lines.push(range);
143
- let out = lines.join('\n').trim();
139
+ const range = (start && end) ? ('Du ' + formatFR(start) + ' au ' + formatFR(end)) : '';
140
+
141
+ // IMPORTANT:
142
+ // On the public HelloAsso checkout page, line breaks are not always rendered consistently.
143
+ // Build a single-line label using bullet separators.
144
+ let out = '';
145
+ if (base) out += base;
146
+ if (items.length) out += (out ? ' — ' : '') + items.map((it) => '• ' + it).join(' ');
147
+ if (range) out += (out ? ' — ' : '') + range;
148
+
149
+ out = String(out || '').trim();
150
+ if (!out) out = 'Réservation matériel';
151
+
144
152
  // HelloAsso constraint: itemName max 250 chars
145
153
  if (out.length > 250) {
146
154
  out = out.slice(0, 249).trimEnd() + '…';
@@ -812,7 +820,7 @@ api.approveReservation = async function (req, res) {
812
820
  // Can be overridden via ACP setting `helloassoCallbackUrl`.
813
821
  callbackUrl: normalizeReturnUrl(meta),
814
822
  webhookUrl: normalizeCallbackUrl(settings2.helloassoCallbackUrl, meta),
815
- itemName: buildHelloAssoItemName('Réservation matériel Onekite', r.itemNames || (r.itemName ? [r.itemName] : []), r.start, r.end),
823
+ itemName: buildHelloAssoItemName('', r.itemNames || (r.itemName ? [r.itemName] : []), r.start, r.end),
816
824
  containsDonation: false,
817
825
  metadata: {
818
826
  reservationId: String(rid),
package/lib/widgets.js CHANGED
@@ -137,9 +137,19 @@ widgets.renderTwoWeeksWidget = async function (data) {
137
137
  tip.style.display = 'none';
138
138
  }
139
139
 
140
- document.addEventListener('click', (e) => {
140
+ // Client-side escaping (the server-side helper is not in scope here)
141
+ function escapeHtml(s) {
142
+ return String(s || '')
143
+ .replace(/&/g, '&')
144
+ .replace(/</g, '&lt;')
145
+ .replace(/>/g, '&gt;')
146
+ .replace(/"/g, '&quot;')
147
+ .replace(/'/g, '&#39;');
148
+ }
149
+
150
+ document.addEventListener('pointerdown', (e) => {
141
151
  // Close when clicking outside an event
142
- if (!e.target || !e.target.closest || !e.target.closest('.fc-event')) {
152
+ if (!e.target || !e.target.closest || (!e.target.closest('.fc-event') && !e.target.closest('.onekite-cal-tooltip'))) {
143
153
  hideTip();
144
154
  }
145
155
  }, { passive: true });
@@ -164,10 +174,51 @@ widgets.renderTwoWeeksWidget = async function (data) {
164
174
  },
165
175
  navLinks: false,
166
176
  eventTimeFormat: { hour: '2-digit', minute: '2-digit', hour12: false },
167
- // Render as a colored dot (like FullCalendar list dots)
177
+ // Force day number to be numeric only (avoid '1 janvier' style labels)
178
+ dayCellContent: function(arg) {
179
+ try {
180
+ const t = String(arg.dayNumberText || '');
181
+ const m = t.match(/^\d+/);
182
+ return { html: m ? m[0] : t };
183
+ } catch (e) {
184
+ return { html: String(arg.dayNumberText || '') };
185
+ }
186
+ },
187
+ eventClassNames: function(arg) {
188
+ try {
189
+ const ev = arg && arg.event;
190
+ if (!ev) return ['onekite-singleday'];
191
+ if (ev.extendedProps && (ev.extendedProps.multiDay === true || ev.extendedProps.days > 1)) return ['onekite-multiday'];
192
+ if (ev.start && ev.end) {
193
+ const diff = (new Date(ev.end)).getTime() - (new Date(ev.start)).getTime();
194
+ return [diff > 86400000 ? 'onekite-multiday' : 'onekite-singleday'];
195
+ }
196
+ } catch (e) {}
197
+ return ['onekite-singleday'];
198
+ },
199
+ // Render: dot for single-day, pill for multi-day
168
200
  eventContent: function(arg) {
169
- const bg = (arg.event.backgroundColor || (arg.event.extendedProps && arg.event.extendedProps.backgroundColor) || '').trim();
170
- const border = (arg.event.borderColor || '').trim();
201
+ const ev = arg && arg.event;
202
+ const isMulti = (function(){
203
+ try {
204
+ if (!ev || !ev.start || !ev.end) return false;
205
+ if (ev.extendedProps && (ev.extendedProps.multiDay === true || ev.extendedProps.days > 1)) return true;
206
+ const s = new Date(ev.start);
207
+ const e = new Date(ev.end);
208
+ const diff = e.getTime() - s.getTime();
209
+ return diff > 86400000; // strictly more than 1 day
210
+ } catch (e) { return false; }
211
+ })();
212
+
213
+ if (isMulti) {
214
+ const spacer = document.createElement('span');
215
+ spacer.className = 'onekite-pill-spacer';
216
+ spacer.appendChild(document.createTextNode('\u00A0'));
217
+ return { domNodes: [spacer] };
218
+ }
219
+
220
+ const bg = (ev.backgroundColor || (ev.extendedProps && ev.extendedProps.backgroundColor) || '').trim();
221
+ const border = (ev.borderColor || '').trim();
171
222
  const color = bg || border || '#3788d8';
172
223
  const wrap = document.createElement('span');
173
224
  wrap.className = 'onekite-dot-wrap';
@@ -188,6 +239,21 @@ widgets.renderTwoWeeksWidget = async function (data) {
188
239
  eventDidMount: function(info) {
189
240
  try {
190
241
  const ev = info.event;
242
+ // Improve centering for single-day dots by centering the harness,
243
+ // same visual behavior as rentals.
244
+ try {
245
+ const cls = (info.el && info.el.classList) ? info.el.classList : null;
246
+ const isSingle = cls && cls.contains('onekite-singleday');
247
+ const harness = info.el && info.el.closest ? info.el.closest('.fc-daygrid-event-harness') : null;
248
+ if (harness && harness.classList) {
249
+ if (isSingle) {
250
+ harness.classList.add('onekite-harness-dot');
251
+ } else {
252
+ harness.classList.remove('onekite-harness-dot');
253
+ }
254
+ }
255
+ } catch (e) {}
256
+
191
257
  const ep = ev.extendedProps || {};
192
258
  const title = (ep.itemNameLine || ep.title || ev.title || '').toString();
193
259
  const status = (ep.status || ep.type || '').toString();
@@ -202,22 +268,36 @@ widgets.renderTwoWeeksWidget = async function (data) {
202
268
  (status ? ('<div style="opacity:.75; margin-top:2px; font-size:.85em;">' + escapeHtml(status) + '</div>') : '');
203
269
 
204
270
  // Hover (desktop)
205
- info.el.addEventListener('mouseenter', (e) => {
271
+ info.el.addEventListener('pointerenter', () => {
272
+ // Only show on devices that actually hover
273
+ if (window.matchMedia && window.matchMedia('(hover: hover)').matches === false) return;
206
274
  setTipContent(html);
207
275
  const rect = info.el.getBoundingClientRect();
208
- showTipAt(rect.left + window.scrollX, rect.top + window.scrollY);
276
+ showTipAt(rect.left + rect.width / 2 + window.scrollX, rect.top + window.scrollY);
209
277
  }, { passive: true });
210
- info.el.addEventListener('mouseleave', hideTip, { passive: true });
278
+ info.el.addEventListener('pointerleave', hideTip, { passive: true });
211
279
 
212
- // Tap/click (mobile)
213
- info.el.addEventListener('click', (e) => {
214
- e.preventDefault();
215
- e.stopPropagation();
280
+ // Tap (mobile) / click (fallback)
281
+ const openTipFromPoint = (pt) => {
216
282
  setTipContent(html);
217
- const pt = (e.touches && e.touches[0]) ? e.touches[0] : e;
218
283
  showTipAt((pt.clientX || 0) + window.scrollX, (pt.clientY || 0) + window.scrollY);
284
+ };
285
+
286
+ info.el.addEventListener('pointerdown', (e) => {
287
+ // On touch devices, FullCalendar can swallow click events during swipe;
288
+ // pointerdown is more reliable.
289
+ if (e && e.preventDefault) e.preventDefault();
290
+ if (e && e.stopPropagation) e.stopPropagation();
291
+ openTipFromPoint(e);
219
292
  });
220
- } catch (e) {}
293
+
294
+ info.el.addEventListener('touchstart', (e) => {
295
+ const t = e.touches && e.touches[0];
296
+ if (!t) return;
297
+ if (e && e.stopPropagation) e.stopPropagation();
298
+ openTipFromPoint(t);
299
+ }, { passive: true });
300
+ } catch (e) {}
221
301
  },
222
302
  });
223
303
 
@@ -261,11 +341,43 @@ widgets.renderTwoWeeksWidget = async function (data) {
261
341
  .onekite-twoweeks .fc .fc-button { padding: .2rem .35rem; font-size: .75rem; }
262
342
  .onekite-twoweeks .fc .fc-daygrid-day-number { font-size: .75rem; padding: 2px; }
263
343
  .onekite-twoweeks .fc .fc-col-header-cell-cushion { font-size: .75rem; }
264
- .onekite-twoweeks .fc .fc-event { background: transparent; border: none; }
344
+
345
+ /* Single-day: show only a dot */
346
+ .onekite-twoweeks .fc .fc-event.onekite-singleday { background: transparent !important; border: none !important; }
347
+ .onekite-twoweeks .fc .fc-daygrid-event.onekite-singleday,
348
+ .onekite-twoweeks .fc .fc-daygrid-block-event.onekite-singleday {
349
+ background: transparent !important;
350
+ border: none !important;
351
+ box-shadow: none !important;
352
+ }
353
+ .onekite-twoweeks .fc .fc-daygrid-block-event.onekite-singleday .fc-event-main { padding: 0 !important; }
354
+
355
+ /* Multi-day: keep FullCalendar bar, but hide text and make it a pill */
356
+ .onekite-twoweeks .fc .fc-event.onekite-multiday {
357
+ border-radius: 999px !important;
358
+ overflow: hidden;
359
+ }
360
+ .onekite-twoweeks .fc .fc-daygrid-event.onekite-multiday,
361
+ .onekite-twoweeks .fc .fc-daygrid-block-event.onekite-multiday {
362
+ border-radius: 999px !important;
363
+ box-shadow: none !important;
364
+ }
365
+ .onekite-twoweeks .fc .fc-event.onekite-multiday .fc-event-main { padding: 0 !important; }
366
+ .onekite-twoweeks .fc .fc-event.onekite-multiday .fc-event-title,
367
+ .onekite-twoweeks .fc .fc-event.onekite-multiday .fc-event-time { display:none !important; }
368
+
369
+ .onekite-twoweeks .fc .fc-daygrid-event-harness { margin-top: 2px; }
370
+ /* Center single-day dots inside the day cell (match rentals) */
371
+ .onekite-twoweeks .fc .fc-daygrid-event-harness.onekite-harness-dot {
372
+ display: flex;
373
+ justify-content: center;
374
+ }
375
+ .onekite-twoweeks .fc .fc-daygrid-event { padding: 0 !important; }
265
376
  .onekite-twoweeks .fc .fc-event-main { color: inherit; }
266
377
  .onekite-twoweeks .fc .fc-event-title { display:none; }
267
378
  .onekite-dot-wrap { display:flex; align-items:center; justify-content:center; width: 100%; }
268
379
  .onekite-dot { width: 10px; height: 10px; border-radius: 999px; display:inline-block; }
380
+ .onekite-pill-spacer { display:block; height: 10px; line-height: 10px; }
269
381
  .onekite-cal-tooltip {
270
382
  position: absolute;
271
383
  z-index: 99999;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-onekite-calendar",
3
- "version": "1.0.9",
3
+ "version": "1.0.11",
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
@@ -39,5 +39,5 @@
39
39
  "acpScripts": [
40
40
  "public/admin.js"
41
41
  ],
42
- "version": "1.0.9"
42
+ "version": "1.0.11"
43
43
  }