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 +21 -3
- package/lib/api.js +21 -2
- package/lib/discord.js +2 -2
- package/lib/widgets.js +120 -8
- package/library.js +1 -1
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/public/client.js +1 -1
- package/templates/admin/plugins/calendar-onekite.tpl +1 -1
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
|
|
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
|
|
190
|
+
itemName: buildHelloAssoItemName('Réservation matériel Onekite', r.itemNames || (r.itemName ? [r.itemName] : []), r.start, r.end),
|
|
177
191
|
containsDonation: false,
|
|
178
|
-
metadata: {
|
|
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
|
|
815
|
+
itemName: buildHelloAssoItemName('Réservation matériel Onekite', r.itemNames || (r.itemName ? [r.itemName] : []), r.start, r.end),
|
|
801
816
|
containsDonation: false,
|
|
802
|
-
metadata: {
|
|
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' ? '
|
|
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: '
|
|
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
|
|
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
|
|
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
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
|
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
package/package.json
CHANGED
package/plugin.json
CHANGED
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
|
|
7
|
+
(function ensureOnekiteStyles() {
|
|
8
8
|
try {
|
|
9
9
|
if (document.getElementById('onekite-inline-styles')) return;
|
|
10
10
|
const style = document.createElement('style');
|