nodebb-plugin-calendar-onekite 11.1.72 → 11.1.73

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.
Files changed (3) hide show
  1. package/library.js +185 -0
  2. package/package.json +1 -1
  3. package/plugin.json +11 -1
package/library.js CHANGED
@@ -19,6 +19,191 @@ const Plugin = {};
19
19
  const isFn = (fn) => typeof fn === 'function';
20
20
  const mw = (...fns) => fns.filter(isFn);
21
21
 
22
+ function escapeHtml(str) {
23
+ return String(str)
24
+ .replace(/&/g, '&')
25
+ .replace(/</g, '&lt;')
26
+ .replace(/>/g, '&gt;')
27
+ .replace(/"/g, '&quot;')
28
+ .replace(/'/g, '&#39;');
29
+ }
30
+
31
+ // --- Widgets ---------------------------------------------------------------
32
+ // Docs: https://docs.nodebb.org/development/widgets/
33
+ Plugin.defineWidgets = async function (widgets, callback) {
34
+ try {
35
+ widgets = widgets || [];
36
+ widgets.push({
37
+ widget: 'calendar-onekite-mini',
38
+ name: 'Calendrier OneKite (mini)',
39
+ description: 'Mini calendrier (mois en cours) avec indicateurs de réservations/évènements.',
40
+ // Widget settings form (ACP Widgets editor)
41
+ content: [
42
+ '<div class="form-group">',
43
+ ' <label>Titre</label>',
44
+ ' <input type="text" class="form-control" name="title" placeholder="Calendrier" />',
45
+ '</div>',
46
+ '<div class="form-group">',
47
+ ' <label>Afficher les évènements</label>',
48
+ ' <select class="form-control" name="showSpecial">',
49
+ ' <option value="1">Oui</option>',
50
+ ' <option value="0">Non</option>',
51
+ ' </select>',
52
+ '</div>',
53
+ ].join(''),
54
+ });
55
+
56
+ if (typeof callback === 'function') {
57
+ return callback(null, widgets);
58
+ }
59
+ return widgets;
60
+ } catch (err) {
61
+ if (typeof callback === 'function') {
62
+ return callback(err);
63
+ }
64
+ throw err;
65
+ }
66
+ };
67
+
68
+ Plugin.renderMiniWidget = async function (hookData, callback) {
69
+ try {
70
+ const widget = hookData.widget || {};
71
+ const data = widget.data || {};
72
+ const title = (data.title || 'Calendrier').toString();
73
+ const showSpecial = String(data.showSpecial ?? '1') !== '0';
74
+
75
+ const widgetId = `calendar-onekite-mini-${Date.now()}-${Math.floor(Math.random() * 1e9)}`;
76
+
77
+ // Note: We render client-side to avoid extra server-side queries. The widget fetches
78
+ // events via the existing public API endpoint.
79
+ widget.html = `
80
+ <div class="calendar-onekite-widget" id="${widgetId}">
81
+ <div class="calendar-onekite-widget__header">
82
+ <strong>${escapeHtml(title)}</strong>
83
+ <a class="calendar-onekite-widget__link" href="/calendar">Ouvrir</a>
84
+ </div>
85
+ <div class="calendar-onekite-widget__month" data-show-special="${showSpecial ? '1' : '0'}"></div>
86
+ <div class="calendar-onekite-widget__legend">
87
+ <span class="calendar-onekite-dot calendar-onekite-dot--pending"></span> en attente
88
+ <span class="calendar-onekite-dot calendar-onekite-dot--paid"></span> payée
89
+ ${showSpecial ? '<span class="calendar-onekite-dot calendar-onekite-dot--special"></span> évènement' : ''}
90
+ </div>
91
+ </div>
92
+ <style>
93
+ .calendar-onekite-widget{border:1px solid rgba(0,0,0,.1);border-radius:8px;padding:10px}
94
+ .calendar-onekite-widget__header{display:flex;justify-content:space-between;align-items:center;margin-bottom:8px}
95
+ .calendar-onekite-widget__link{font-size:12px}
96
+ .calendar-onekite-widget__grid{display:grid;grid-template-columns:repeat(7,1fr);gap:4px}
97
+ .calendar-onekite-widget__cell{position:relative;aspect-ratio:1/1;border-radius:6px;display:flex;align-items:center;justify-content:center;font-size:12px;user-select:none}
98
+ .calendar-onekite-widget__cell--muted{opacity:.35}
99
+ .calendar-onekite-widget__cell--today{outline:2px solid rgba(0,0,0,.2)}
100
+ .calendar-onekite-badges{position:absolute;bottom:2px;left:50%;transform:translateX(-50%);display:flex;gap:2px}
101
+ .calendar-onekite-dot{display:inline-block;width:8px;height:8px;border-radius:50%;margin:0 4px 0 10px;vertical-align:middle}
102
+ .calendar-onekite-dot--pending{background:#0d6efd}
103
+ .calendar-onekite-dot--paid{background:#198754}
104
+ .calendar-onekite-dot--special{background:#fd7e14}
105
+ .calendar-onekite-badge{width:6px;height:6px;border-radius:50%}
106
+ .calendar-onekite-badge--pending{background:#0d6efd}
107
+ .calendar-onekite-badge--paid{background:#198754}
108
+ .calendar-onekite-badge--special{background:#fd7e14}
109
+ .calendar-onekite-widget__legend{margin-top:8px;font-size:12px;opacity:.8}
110
+ </style>
111
+ <script>
112
+ (function(){
113
+ const widgetEl = document.getElementById(${JSON.stringify(widgetId)});
114
+ if (!widgetEl) return;
115
+
116
+ const monthEl = widgetEl.querySelector('.calendar-onekite-widget__month');
117
+ const showSpecial = monthEl.getAttribute('data-show-special') === '1';
118
+
119
+ const now = new Date();
120
+ const year = now.getFullYear();
121
+ const month = now.getMonth();
122
+ const first = new Date(year, month, 1);
123
+ const last = new Date(year, month + 1, 0);
124
+ const start = new Date(year, month, 1);
125
+ start.setDate(start.getDate() - ((start.getDay() + 6) % 7)); // Monday-start
126
+ const end = new Date(start);
127
+ end.setDate(end.getDate() + 41);
128
+
129
+ function iso(d){ return d.toISOString(); }
130
+ function ymd(d){ return d.toISOString().slice(0,10); }
131
+
132
+ const apiUrl = window.location.origin + '/api/v3/plugins/calendar-onekite/events?from=' + encodeURIComponent(iso(start)) + '&to=' + encodeURIComponent(iso(end));
133
+
134
+ fetch(apiUrl, { credentials: 'same-origin' })
135
+ .then(r => r.ok ? r.json() : Promise.reject(r))
136
+ .then(payload => {
137
+ const events = Array.isArray(payload) ? payload : (payload.events || payload.data || []);
138
+ const dayMap = new Map();
139
+ for (const ev of events) {
140
+ if (!ev || !ev.start) continue;
141
+ const d = new Date(ev.start);
142
+ const key = ymd(d);
143
+ const props = ev.extendedProps || {};
144
+ const isSpecial = props.type === 'special' || ev.type === 'special' || (ev.classNames || []).includes('onekite-special');
145
+ if (isSpecial && !showSpecial) continue;
146
+ const status = props.status || ev.status || '';
147
+ const bucket = dayMap.get(key) || { pending: 0, paid: 0, special: 0 };
148
+ if (isSpecial) bucket.special++;
149
+ else if (status === 'paid') bucket.paid++;
150
+ else bucket.pending++;
151
+ dayMap.set(key, bucket);
152
+ }
153
+ render(dayMap);
154
+ })
155
+ .catch(() => render(new Map()));
156
+
157
+ function render(dayMap){
158
+ const grid = document.createElement('div');
159
+ grid.className = 'calendar-onekite-widget__grid';
160
+ const labels = ['L','M','M','J','V','S','D'];
161
+ for (const l of labels){
162
+ const el = document.createElement('div');
163
+ el.className = 'calendar-onekite-widget__cell calendar-onekite-widget__cell--muted';
164
+ el.textContent = l;
165
+ grid.appendChild(el);
166
+ }
167
+ for (let i=0;i<42;i++){
168
+ const d = new Date(start);
169
+ d.setDate(start.getDate()+i);
170
+ const cell = document.createElement('a');
171
+ cell.href = '/calendar';
172
+ cell.className = 'calendar-onekite-widget__cell';
173
+ if (d.getMonth() !== month) cell.classList.add('calendar-onekite-widget__cell--muted');
174
+ if (ymd(d) === ymd(now)) cell.classList.add('calendar-onekite-widget__cell--today');
175
+ cell.textContent = d.getDate();
176
+ const b = dayMap.get(ymd(d));
177
+ if (b && (b.pending || b.paid || b.special)){
178
+ const badges = document.createElement('div');
179
+ badges.className = 'calendar-onekite-badges';
180
+ if (b.pending) { const s=document.createElement('span'); s.className='calendar-onekite-badge calendar-onekite-badge--pending'; badges.appendChild(s); }
181
+ if (b.paid) { const s=document.createElement('span'); s.className='calendar-onekite-badge calendar-onekite-badge--paid'; badges.appendChild(s); }
182
+ if (showSpecial && b.special) { const s=document.createElement('span'); s.className='calendar-onekite-badge calendar-onekite-badge--special'; badges.appendChild(s); }
183
+ cell.appendChild(badges);
184
+ }
185
+ grid.appendChild(cell);
186
+ }
187
+ monthEl.innerHTML = '';
188
+ monthEl.appendChild(grid);
189
+ }
190
+ })();
191
+ </script>
192
+ `;
193
+
194
+ hookData.widget = widget;
195
+ if (typeof callback === 'function') {
196
+ return callback(null, hookData);
197
+ }
198
+ return hookData;
199
+ } catch (err) {
200
+ if (typeof callback === 'function') {
201
+ return callback(err);
202
+ }
203
+ throw err;
204
+ }
205
+ };
206
+
22
207
  Plugin.init = async function (params) {
23
208
  const { router, middleware } = params;
24
209
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-calendar-onekite",
3
- "version": "11.1.72",
3
+ "version": "11.1.73",
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
@@ -8,6 +8,16 @@
8
8
  "hook": "static:app.load",
9
9
  "method": "init"
10
10
  },
11
+ {
12
+ "hook": "filter:widgets.getWidgets",
13
+ "method": "defineWidgets",
14
+ "callbacked": true
15
+ },
16
+ {
17
+ "hook": "filter:widget.render:calendar-onekite-mini",
18
+ "method": "renderMiniWidget",
19
+ "callbacked": true
20
+ },
11
21
  {
12
22
  "hook": "filter:admin.header.build",
13
23
  "method": "addAdminNavigation"
@@ -27,5 +37,5 @@
27
37
  "acpScripts": [
28
38
  "public/admin.js"
29
39
  ],
30
- "version": "1.0.46"
40
+ "version": "1.0.48"
31
41
  }