nodebb-plugin-onekite-calendar 1.0.7 → 1.0.9

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/admin.js CHANGED
@@ -18,6 +18,20 @@ function formatFR(tsOrIso) {
18
18
  return `${dd}/${mm}/${yyyy}`;
19
19
  }
20
20
 
21
+ function buildHelloAssoItemName(baseLabel, itemNames, start, end) {
22
+ const base = String(baseLabel || 'Réservation matériel Onekite').trim();
23
+ const items = Array.isArray(itemNames) ? itemNames.map((s) => String(s || '').trim()).filter(Boolean) : [];
24
+ const range = (start && end) ? `Du ${formatFR(start)} au ${formatFR(end)}` : '';
25
+ const lines = [base];
26
+ items.forEach((it) => lines.push(`• ${it}`));
27
+ if (range) lines.push(range);
28
+ let out = lines.join('\n').trim();
29
+ if (out.length > 250) {
30
+ out = out.slice(0, 249).trimEnd() + '…';
31
+ }
32
+ return out;
33
+ }
34
+
21
35
  async function sendEmail(template, toEmail, subject, data) {
22
36
  // Prefer sending by uid (NodeBB core expects uid in various places)
23
37
  const uid = data && Number.isInteger(data.uid) ? data.uid : null;
@@ -95,7 +109,7 @@ const admin = {};
95
109
 
96
110
  admin.renderAdmin = async function (req, res) {
97
111
  res.render('admin/plugins/calendar-onekite', {
98
- title: 'Calendar OneKite',
112
+ title: 'Calendar Onekite',
99
113
  });
100
114
  };
101
115
 
@@ -173,9 +187,13 @@ admin.approveReservation = async function (req, res) {
173
187
  // User return/back/error URLs must be real pages; webhook uses the plugin endpoint.
174
188
  callbackUrl: returnUrl,
175
189
  webhookUrl: webhookUrl,
176
- itemName: 'Réservation matériel OneKite',
190
+ itemName: buildHelloAssoItemName('Réservation matériel Onekite', r.itemNames || (r.itemName ? [r.itemName] : []), r.start, r.end),
177
191
  containsDonation: false,
178
- metadata: { reservationId: String(rid) },
192
+ metadata: {
193
+ reservationId: String(rid),
194
+ items: (Array.isArray(r.itemNames) ? r.itemNames : (r.itemName ? [r.itemName] : [])).filter(Boolean),
195
+ dateRange: `Du ${formatFR(r.start)} au ${formatFR(r.end)}`,
196
+ },
179
197
  });
180
198
  paymentUrl = intent && (intent.paymentUrl || intent.redirectUrl)
181
199
  ? (intent.paymentUrl || intent.redirectUrl)
package/lib/api.js CHANGED
@@ -133,6 +133,21 @@ function formatFR(tsOrIso) {
133
133
  return `${dd}/${mm}/${yyyy}`;
134
134
  }
135
135
 
136
+ function buildHelloAssoItemName(baseLabel, itemNames, start, end) {
137
+ const base = String(baseLabel || 'Réservation matériel Onekite').trim();
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();
144
+ // HelloAsso constraint: itemName max 250 chars
145
+ if (out.length > 250) {
146
+ out = out.slice(0, 249).trimEnd() + '…';
147
+ }
148
+ return out;
149
+ }
150
+
136
151
  function toTs(v) {
137
152
  if (v === undefined || v === null || v === '') return NaN;
138
153
  // Accept milliseconds timestamps passed as strings or numbers.
@@ -797,9 +812,13 @@ api.approveReservation = async function (req, res) {
797
812
  // Can be overridden via ACP setting `helloassoCallbackUrl`.
798
813
  callbackUrl: normalizeReturnUrl(meta),
799
814
  webhookUrl: normalizeCallbackUrl(settings2.helloassoCallbackUrl, meta),
800
- itemName: 'Réservation matériel OneKite',
815
+ itemName: buildHelloAssoItemName('Réservation matériel Onekite', r.itemNames || (r.itemName ? [r.itemName] : []), r.start, r.end),
801
816
  containsDonation: false,
802
- metadata: { reservationId: String(rid) },
817
+ metadata: {
818
+ reservationId: String(rid),
819
+ items: (Array.isArray(r.itemNames) ? r.itemNames : (r.itemName ? [r.itemName] : [])).filter(Boolean),
820
+ dateRange: `Du ${formatFR(r.start)} au ${formatFR(r.end)}`,
821
+ },
803
822
  });
804
823
  const paymentUrl = intent && (intent.paymentUrl || intent.redirectUrl) ? (intent.paymentUrl || intent.redirectUrl) : (typeof intent === 'string' ? intent : null);
805
824
  const checkoutIntentId = intent && intent.checkoutIntentId ? String(intent.checkoutIntentId) : null;
package/lib/discord.js CHANGED
@@ -88,7 +88,7 @@ function buildReservationMessage(kind, reservation) {
88
88
  function buildWebhookPayload(kind, reservation) {
89
89
  // Discord "regroupe" visuellement les messages consécutifs d'un même auteur.
90
90
  // En utilisant un username différent par action, on obtient un message bien distinct.
91
- const webhookUsername = kind === 'paid' ? 'OneKite • Paiement' : 'OneKite • Réservation';
91
+ const webhookUsername = kind === 'paid' ? 'Onekite • Paiement' : 'Onekite • Réservation';
92
92
 
93
93
  const calUrl = 'https://www.onekite.com/calendar';
94
94
  const username = reservation && reservation.username ? String(reservation.username) : '';
@@ -124,7 +124,7 @@ function buildWebhookPayload(kind, reservation) {
124
124
  ? 'Un paiement a été reçu pour une réservation.'
125
125
  : 'Une nouvelle demande de réservation a été créée.',
126
126
  fields,
127
- footer: { text: 'OneKite • Calendrier' },
127
+ footer: { text: 'Onekite • Calendrier' },
128
128
  timestamp: new Date().toISOString(),
129
129
  },
130
130
  ],
package/lib/widgets.js CHANGED
@@ -41,7 +41,7 @@ widgets.defineWidgets = async function (widgetData) {
41
41
 
42
42
  list.push({
43
43
  widget: 'calendar-onekite-twoweeks',
44
- name: 'Calendrier OneKite (2 semaines)',
44
+ name: 'Calendrier',
45
45
  description: 'Affiche la semaine courante + la semaine suivante (FullCalendar via CDN).',
46
46
  content: '',
47
47
  });
@@ -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>
@@ -117,6 +117,33 @@ widgets.renderTwoWeeksWidget = async function (data) {
117
117
 
118
118
  await ensureFullCalendar();
119
119
 
120
+ // Basic lightweight tooltip (no dependencies, works on hover + tap)
121
+ const tip = document.createElement('div');
122
+ tip.className = 'onekite-cal-tooltip';
123
+ tip.style.display = 'none';
124
+ document.body.appendChild(tip);
125
+
126
+ function setTipContent(html) {
127
+ tip.innerHTML = html;
128
+ }
129
+
130
+ function showTipAt(x, y) {
131
+ tip.style.left = Math.max(8, x + 12) + 'px';
132
+ tip.style.top = Math.max(8, y + 12) + 'px';
133
+ tip.style.display = 'block';
134
+ }
135
+
136
+ function hideTip() {
137
+ tip.style.display = 'none';
138
+ }
139
+
140
+ document.addEventListener('click', (e) => {
141
+ // Close when clicking outside an event
142
+ if (!e.target || !e.target.closest || !e.target.closest('.fc-event')) {
143
+ hideTip();
144
+ }
145
+ }, { passive: true });
146
+
120
147
  // Define a 2-week dayGrid view
121
148
  const calendar = new window.FullCalendar.Calendar(el, {
122
149
  initialView: 'dayGridTwoWeek',
@@ -137,6 +164,19 @@ widgets.renderTwoWeeksWidget = async function (data) {
137
164
  },
138
165
  navLinks: false,
139
166
  eventTimeFormat: { hour: '2-digit', minute: '2-digit', hour12: false },
167
+ // Render as a colored dot (like FullCalendar list dots)
168
+ eventContent: function(arg) {
169
+ const bg = (arg.event.backgroundColor || (arg.event.extendedProps && arg.event.extendedProps.backgroundColor) || '').trim();
170
+ const border = (arg.event.borderColor || '').trim();
171
+ const color = bg || border || '#3788d8';
172
+ const wrap = document.createElement('span');
173
+ wrap.className = 'onekite-dot-wrap';
174
+ const dot = document.createElement('span');
175
+ dot.className = 'onekite-dot';
176
+ dot.style.backgroundColor = color;
177
+ wrap.appendChild(dot);
178
+ return { domNodes: [wrap] };
179
+ },
140
180
  events: function(info, successCallback, failureCallback) {
141
181
  const qs = new URLSearchParams({ start: info.startStr, end: info.endStr });
142
182
  fetch(eventsEndpoint + '?' + qs.toString(), { credentials: 'same-origin' })
@@ -144,15 +184,71 @@ widgets.renderTwoWeeksWidget = async function (data) {
144
184
  .then((json) => successCallback(json || []))
145
185
  .catch((e) => failureCallback(e));
146
186
  },
147
- dateClick: function() {
148
- window.location.href = calUrl;
149
- },
150
- eventClick: function() {
151
- window.location.href = calUrl;
187
+ dateClick: function() { window.location.href = calUrl; },
188
+ eventDidMount: function(info) {
189
+ try {
190
+ const ev = info.event;
191
+ const ep = ev.extendedProps || {};
192
+ const title = (ep.itemNameLine || ep.title || ev.title || '').toString();
193
+ const status = (ep.status || ep.type || '').toString();
194
+ const start = ev.start ? new Date(ev.start) : null;
195
+ const end = ev.end ? new Date(ev.end) : null;
196
+ const pad2 = (n) => String(n).padStart(2, '0');
197
+ const fmt = (d) => d ? (pad2(d.getDate()) + '/' + pad2(d.getMonth() + 1) + '/' + String(d.getFullYear()).slice(-2)) : '';
198
+ const range = (start && end) ? ('Du ' + fmt(start) + ' au ' + fmt(end)) : '';
199
+ const html = '' +
200
+ '<div style="font-weight:600; margin-bottom:2px;">' + escapeHtml(title) + '</div>' +
201
+ (range ? ('<div style="opacity:.85">' + escapeHtml(range) + '</div>') : '') +
202
+ (status ? ('<div style="opacity:.75; margin-top:2px; font-size:.85em;">' + escapeHtml(status) + '</div>') : '');
203
+
204
+ // Hover (desktop)
205
+ info.el.addEventListener('mouseenter', (e) => {
206
+ setTipContent(html);
207
+ const rect = info.el.getBoundingClientRect();
208
+ showTipAt(rect.left + window.scrollX, rect.top + window.scrollY);
209
+ }, { passive: true });
210
+ info.el.addEventListener('mouseleave', hideTip, { passive: true });
211
+
212
+ // Tap/click (mobile)
213
+ info.el.addEventListener('click', (e) => {
214
+ e.preventDefault();
215
+ e.stopPropagation();
216
+ setTipContent(html);
217
+ const pt = (e.touches && e.touches[0]) ? e.touches[0] : e;
218
+ showTipAt((pt.clientX || 0) + window.scrollX, (pt.clientY || 0) + window.scrollY);
219
+ });
220
+ } catch (e) {}
152
221
  },
153
222
  });
154
223
 
155
224
  calendar.render();
225
+
226
+ // Mobile swipe (left/right) to navigate weeks
227
+ try {
228
+ let touchStartX = null;
229
+ let touchStartY = null;
230
+ el.addEventListener('touchstart', (e) => {
231
+ const t = e.touches && e.touches[0];
232
+ if (!t) return;
233
+ touchStartX = t.clientX;
234
+ touchStartY = t.clientY;
235
+ }, { passive: true });
236
+ el.addEventListener('touchend', (e) => {
237
+ const t = e.changedTouches && e.changedTouches[0];
238
+ if (!t || touchStartX === null || touchStartY === null) return;
239
+ const dx = t.clientX - touchStartX;
240
+ const dy = t.clientY - touchStartY;
241
+ touchStartX = null;
242
+ touchStartY = null;
243
+ if (Math.abs(dx) < 55) return;
244
+ if (Math.abs(dx) < Math.abs(dy) * 1.2) return; // mostly vertical
245
+ if (dx < 0) {
246
+ calendar.next();
247
+ } else {
248
+ calendar.prev();
249
+ }
250
+ }, { passive: true });
251
+ } catch (e) {}
156
252
  }
157
253
 
158
254
  // Widgets can be rendered after ajaxify; delay a tick.
@@ -165,7 +261,23 @@ widgets.renderTwoWeeksWidget = async function (data) {
165
261
  .onekite-twoweeks .fc .fc-button { padding: .2rem .35rem; font-size: .75rem; }
166
262
  .onekite-twoweeks .fc .fc-daygrid-day-number { font-size: .75rem; padding: 2px; }
167
263
  .onekite-twoweeks .fc .fc-col-header-cell-cushion { font-size: .75rem; }
168
- .onekite-twoweeks .fc .fc-event-title { font-size: .72rem; }
264
+ .onekite-twoweeks .fc .fc-event { background: transparent; border: none; }
265
+ .onekite-twoweeks .fc .fc-event-main { color: inherit; }
266
+ .onekite-twoweeks .fc .fc-event-title { display:none; }
267
+ .onekite-dot-wrap { display:flex; align-items:center; justify-content:center; width: 100%; }
268
+ .onekite-dot { width: 10px; height: 10px; border-radius: 999px; display:inline-block; }
269
+ .onekite-cal-tooltip {
270
+ position: absolute;
271
+ z-index: 99999;
272
+ max-width: 260px;
273
+ background: rgba(0,0,0,.92);
274
+ color: #fff;
275
+ padding: 8px 10px;
276
+ border-radius: 10px;
277
+ font-size: 0.9rem;
278
+ box-shadow: 0 8px 24px rgba(0,0,0,.25);
279
+ pointer-events: none;
280
+ }
169
281
  </style>
170
282
  `;
171
283
 
package/library.js CHANGED
@@ -126,7 +126,7 @@ Plugin.addAdminNavigation = async function (header) {
126
126
  header.plugins.push({
127
127
  route: '/plugins/calendar-onekite',
128
128
  icon: 'fa-calendar',
129
- name: 'Calendar OneKite',
129
+ name: 'Calendar Onekite',
130
130
  });
131
131
  return header;
132
132
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-onekite-calendar",
3
- "version": "1.0.7",
3
+ "version": "1.0.9",
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.7"
42
+ "version": "1.0.9"
43
43
  }
package/public/client.js CHANGED
@@ -4,7 +4,7 @@ define('forum/calendar-onekite', ['alerts', 'bootbox', 'hooks'], function (alert
4
4
  'use strict';
5
5
 
6
6
  // Ensure small UI tweaks are applied even when themes override bootstrap defaults.
7
- (function ensureOneKiteStyles() {
7
+ (function ensureOnekiteStyles() {
8
8
  try {
9
9
  if (document.getElementById('onekite-inline-styles')) return;
10
10
  const style = document.createElement('style');
@@ -2,7 +2,7 @@
2
2
 
3
3
  <div class="row">
4
4
  <div class="col-lg-9">
5
- <h1>Calendar OneKite</h1>
5
+ <h1>Calendar Onekite</h1>
6
6
 
7
7
  <ul class="nav nav-tabs mt-3" role="tablist">
8
8
  <li class="nav-item" role="presentation">