nodebb-plugin-ezoic-infinite 1.0.3 → 1.0.6

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/library.js CHANGED
@@ -1,50 +1,100 @@
1
-
2
1
  'use strict';
3
2
 
4
3
  const meta = require.main.require('./src/meta');
5
4
  const groups = require.main.require('./src/groups');
5
+ const db = require.main.require('./src/database');
6
6
 
7
- const Plugin = {};
8
-
9
- Plugin.init = async ({ router, middleware }) => {
10
- router.get('/admin/plugins/ezoic-infinite', middleware.admin.buildHeader, renderAdmin);
11
- router.get('/api/admin/plugins/ezoic-infinite', renderAdmin);
12
- };
7
+ const SETTINGS_KEY = 'ezoic-infinite';
8
+ const plugin = {};
13
9
 
14
- async function renderAdmin(req, res) {
15
- const settings = await meta.settings.get('ezoic-infinite');
10
+ function normalizeExcludedGroups(value) {
11
+ if (!value) return [];
12
+ if (Array.isArray(value)) return value;
13
+ return String(value).split(',').map(s => s.trim()).filter(Boolean);
14
+ }
16
15
 
17
- let groupNames = [];
18
- try {
19
- groupNames = await groups.getGroupsFromSet('groups:createtime', 0, -1);
20
- } catch (e) {}
16
+ function parseBool(v, def = false) {
17
+ if (v === undefined || v === null || v === '') return def;
18
+ if (typeof v === 'boolean') return v;
19
+ const s = String(v).toLowerCase();
20
+ return s === '1' || s === 'true' || s === 'on' || s === 'yes';
21
+ }
21
22
 
22
- let groupList = [];
23
- try {
24
- groupList = await groups.getGroupsData(groupNames);
25
- } catch (e) {
26
- groupList = groupNames.map(name => ({ name }));
23
+ async function getAllGroups() {
24
+ let names = await db.getSortedSetRange('groups:createtime', 0, -1);
25
+ if (!names || !names.length) {
26
+ names = await db.getSortedSetRange('groups:visible:createtime', 0, -1);
27
27
  }
28
+ const filtered = names.filter(name => !groups.isPrivilegeGroup(name));
29
+ const data = await groups.getGroupsData(filtered);
30
+ // Sort alphabetically for ACP usability
31
+ data.sort((a, b) => String(a.name).localeCompare(String(b.name), 'fr', { sensitivity: 'base' }));
32
+ return data;
33
+ }
34
+ async function getSettings() {
35
+ const s = await meta.settings.get(SETTINGS_KEY);
36
+ return {
37
+ // Between-post ads (simple blocks)
38
+ enableBetweenAds: parseBool(s.enableBetweenAds, true),
39
+ placeholderIds: (s.placeholderIds || '').trim(),
40
+ intervalPosts: Math.max(1, parseInt(s.intervalPosts, 10) || 6),
28
41
 
29
- res.render('admin/plugins/ezoic-infinite', {
30
- settings,
31
- groups: groupList,
32
- });
42
+ // "Ad message" between replies (looks like a post)
43
+ enableMessageAds: parseBool(s.enableMessageAds, false),
44
+ messagePlaceholderIds: (s.messagePlaceholderIds || '').trim(),
45
+ messageIntervalPosts: Math.max(1, parseInt(s.messageIntervalPosts, 10) || 3),
46
+
47
+ excludedGroups: normalizeExcludedGroups(s.excludedGroups),
48
+ };
33
49
  }
34
50
 
35
- Plugin.addAdminNavigation = async (header) => {
51
+ async function isUserExcluded(uid, excludedGroups) {
52
+ if (!uid || !excludedGroups.length) return false;
53
+ const userGroups = await groups.getUserGroups([uid]);
54
+ return (userGroups[0] || []).some(g => excludedGroups.includes(g.name));
55
+ }
56
+
57
+ plugin.addAdminNavigation = async (header) => {
36
58
  header.plugins = header.plugins || [];
37
59
  header.plugins.push({
38
60
  route: '/plugins/ezoic-infinite',
39
- icon: 'fa-bullhorn',
40
- name: 'Ezoic Infinite',
61
+ icon: 'fa-ad',
62
+ name: 'Ezoic Infinite Ads'
41
63
  });
42
64
  return header;
43
65
  };
44
66
 
45
- Plugin.addConfig = async (config) => {
46
- config.ezoicInfinite = await meta.settings.get('ezoic-infinite');
47
- return config;
67
+ plugin.init = async ({ router, middleware }) => {
68
+ async function render(req, res) {
69
+ const settings = await getSettings();
70
+ const allGroups = await getAllGroups();
71
+
72
+ res.render('admin/plugins/ezoic-infinite', {
73
+ title: 'Ezoic Infinite Ads',
74
+ ...settings,
75
+ enableBetweenAds_checked: settings.enableBetweenAds ? 'checked' : '',
76
+ enableMessageAds_checked: settings.enableMessageAds ? 'checked' : '',
77
+ allGroups,
78
+ });
79
+ }
80
+
81
+ router.get('/admin/plugins/ezoic-infinite', middleware.admin.buildHeader, render);
82
+ router.get('/api/admin/plugins/ezoic-infinite', render);
83
+
84
+ router.get('/api/plugins/ezoic-infinite/config', middleware.buildHeader, async (req, res) => {
85
+ const settings = await getSettings();
86
+ const excluded = await isUserExcluded(req.uid, settings.excludedGroups);
87
+
88
+ res.json({
89
+ excluded,
90
+ enableBetweenAds: settings.enableBetweenAds,
91
+ placeholderIds: settings.placeholderIds,
92
+ intervalPosts: settings.intervalPosts,
93
+ enableMessageAds: settings.enableMessageAds,
94
+ messagePlaceholderIds: settings.messagePlaceholderIds,
95
+ messageIntervalPosts: settings.messageIntervalPosts,
96
+ });
97
+ });
48
98
  };
49
99
 
50
- module.exports = Plugin;
100
+ module.exports = plugin;
package/package.json CHANGED
@@ -1,9 +1,19 @@
1
1
  {
2
2
  "name": "nodebb-plugin-ezoic-infinite",
3
- "version": "1.0.3",
4
- "description": "Ezoic Infinite (rollback to 0.8.2 behaviour)",
3
+ "version": "1.0.6",
4
+ "description": "Ezoic ads with infinite scroll using a pool of placeholder IDs",
5
5
  "main": "library.js",
6
6
  "license": "MIT",
7
+ "keywords": [
8
+ "nodebb",
9
+ "nodebb-plugin",
10
+ "ezoic",
11
+ "ads",
12
+ "infinite-scroll"
13
+ ],
14
+ "engines": {
15
+ "node": ">=18"
16
+ },
7
17
  "nbbpm": {
8
18
  "compatibility": "^4.0.0"
9
19
  }
package/plugin.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "id": "nodebb-plugin-ezoic-infinite",
3
- "name": "Ezoic Infinite",
3
+ "name": "NodeBB Ezoic Infinite Ads",
4
+ "description": "Ezoic ads with infinite scroll using a pool of placeholder IDs",
4
5
  "library": "./library.js",
5
6
  "hooks": [
6
7
  {
@@ -10,10 +11,6 @@
10
11
  {
11
12
  "hook": "filter:admin.header.build",
12
13
  "method": "addAdminNavigation"
13
- },
14
- {
15
- "hook": "filter:config.get",
16
- "method": "addConfig"
17
14
  }
18
15
  ],
19
16
  "staticDirs": {
@@ -25,5 +22,8 @@
25
22
  "scripts": [
26
23
  "public/client.js"
27
24
  ],
28
- "templates": "public/templates"
25
+ "templates": "public/templates",
26
+ "css": [
27
+ "public/style.css"
28
+ ]
29
29
  }
package/public/admin.js CHANGED
@@ -1,35 +1,29 @@
1
+ /* globals ajaxify */
2
+ 'use strict';
1
3
 
2
- /* global $, socket, app */
3
- $(function () {
4
- const ns = 'ezoic-infinite';
5
- const form = $('.ezoic-infinite-settings');
4
+ (function () {
5
+ function init() {
6
+ const $form = $('.ezoic-infinite-settings');
7
+ if (!$form.length) return;
6
8
 
7
- socket.emit('admin.settings.get', { hash: ns }, (err, data) => {
8
- if (!data) return;
9
- Object.keys(data).forEach(k => {
10
- const el = form.find('[name="' + k + '"]');
11
- if (!el.length) return;
12
- if (el.attr('type') === 'checkbox') el.prop('checked', data[k]);
13
- else el.val(data[k]);
14
- });
15
-
16
- if (data.excludedGroups) {
17
- data.excludedGroups.split(',').forEach(g =>
18
- form.find('option[value="' + g + '"]').prop('selected', true)
19
- );
20
- }
21
- });
9
+ require(['settings', 'alerts'], function (Settings, alerts) {
10
+ Settings.load('ezoic-infinite', $form);
22
11
 
23
- $('.ezoic-save').on('click', () => {
24
- const values = {};
25
- form.serializeArray().forEach(o => values[o.name] = o.value);
26
- values.enableBetweenAds = form.find('[name=enableBetweenAds]').is(':checked');
27
- values.enableMessageAds = form.find('[name=enableMessageAds]').is(':checked');
28
- values.excludedGroups = (form.find('[name=excludedGroups]').val() || []).join(',');
12
+ $('#save').off('click.ezoicInfinite').on('click.ezoicInfinite', function (e) {
13
+ e.preventDefault();
29
14
 
30
- socket.emit('admin.settings.set', { hash: ns, values }, err => {
31
- if (err) app.alertError(err.message);
32
- else app.alertSuccess('Enregistré');
15
+ Settings.save('ezoic-infinite', $form, function () {
16
+ // Toast vert (NodeBB core)
17
+ if (alerts && typeof alerts.success === 'function') {
18
+ alerts.success('Enregistré');
19
+ } else if (window.app && typeof window.app.alertSuccess === 'function') {
20
+ window.app.alertSuccess('Enregistré');
21
+ }
22
+ });
23
+ });
33
24
  });
34
- });
35
- });
25
+ }
26
+
27
+ $(document).ready(init);
28
+ $(window).on('action:ajaxify.end', init);
29
+ })();
package/public/client.js CHANGED
@@ -1,6 +1,259 @@
1
+ 'use strict';
1
2
 
2
- /* global config */
3
+ /* globals ajaxify */
3
4
  (function () {
4
- if (!config || !config.ezoicInfinite) return;
5
- // Same logic as stable 0.8.x
5
+ if (window.ezoicInfiniteLoaded) return;
6
+ window.ezoicInfiniteLoaded = true;
7
+
8
+ let cachedConfig;
9
+ let lastFetch = 0;
10
+ let debounceTimer;
11
+
12
+ let inFlight = false;
13
+ let rerunRequested = false;
14
+
15
+ // per page state
16
+ let pageKey = null;
17
+
18
+ // separate pools/state
19
+ let usedBetween = new Set(); // between topics list
20
+ let usedMessage = new Set(); // between replies
21
+ let fifoBetween = [];
22
+ let fifoMessage = [];
23
+
24
+ function getPageKey() {
25
+ try {
26
+ if (ajaxify && ajaxify.data) {
27
+ if (ajaxify.data.tid) return 'topic:' + ajaxify.data.tid;
28
+ if (ajaxify.data.cid) return 'cid:' + ajaxify.data.cid + ':' + window.location.pathname;
29
+ }
30
+ } catch (e) {}
31
+ return window.location.pathname;
32
+ }
33
+
34
+ function isTopicPage() {
35
+ try { return !!(ajaxify && ajaxify.data && ajaxify.data.tid); } catch (e) {}
36
+ return /^\/topic\//.test(window.location.pathname);
37
+ }
38
+
39
+ function isCategoryTopicList() {
40
+ return document.querySelectorAll('li[component="category/topic"]').length > 0 && !isTopicPage();
41
+ }
42
+
43
+ function parsePool(raw) {
44
+ if (!raw) return [];
45
+ // accept newline, comma, space, semicolon
46
+ const arr = String(raw).split(/[\n,;\s]+/)
47
+ .map(x => parseInt(x, 10))
48
+ .filter(n => Number.isFinite(n) && n > 0);
49
+ // unique while preserving order
50
+ return Array.from(new Set(arr));
51
+ }
52
+
53
+ async function fetchConfig() {
54
+ if (cachedConfig && Date.now() - lastFetch < 10000) return cachedConfig;
55
+ const base = (window.config && window.config.relative_path) ? window.config.relative_path : '';
56
+ const res = await fetch(base + '/api/plugins/ezoic-infinite/config', { credentials: 'same-origin' });
57
+ const json = await res.json();
58
+ cachedConfig = json;
59
+ lastFetch = Date.now();
60
+ return json;
61
+ }
62
+
63
+ function cleanupForNewPage() {
64
+ document.querySelectorAll('.ezoic-ad').forEach(el => el.remove());
65
+ usedBetween = new Set();
66
+ usedMessage = new Set();
67
+ fifoBetween = [];
68
+ fifoMessage = [];
69
+ }
70
+
71
+ function destroyPlaceholder(id) {
72
+ try {
73
+ window.ezstandalone = window.ezstandalone || {};
74
+ window.ezstandalone.cmd = window.ezstandalone.cmd || [];
75
+ if (typeof window.ezstandalone.destroyPlaceholders === 'function') {
76
+ window.ezstandalone.destroyPlaceholders(id);
77
+ return;
78
+ }
79
+ window.ezstandalone.cmd.push(function () {
80
+ try { window.ezstandalone.destroyPlaceholders(id); } catch (e) {}
81
+ });
82
+ } catch (e) {}
83
+ }
84
+
85
+ function ensureUniquePlaceholder(id) {
86
+ const existing = document.getElementById('ezoic-pub-ad-placeholder-' + id);
87
+ if (!existing) return;
88
+ const wrap = existing.closest('.ezoic-ad');
89
+ if (wrap) wrap.remove();
90
+ else existing.remove();
91
+ destroyPlaceholder(id);
92
+ }
93
+
94
+ function callShowAdsSingle(id) {
95
+ if (!id) return;
96
+
97
+ const now = Date.now();
98
+ window.__ezoicLastSingle = window.__ezoicLastSingle || {};
99
+ const last = window.__ezoicLastSingle[id] || 0;
100
+ if (now - last < 1200) return;
101
+ window.__ezoicLastSingle[id] = now;
102
+
103
+ window.ezstandalone = window.ezstandalone || {};
104
+ window.ezstandalone.cmd = window.ezstandalone.cmd || [];
105
+
106
+ const run = function () {
107
+ try {
108
+ if (typeof window.ezstandalone.showAds === 'function') {
109
+ window.ezstandalone.showAds(id);
110
+ return true;
111
+ }
112
+ } catch (e) {}
113
+ return false;
114
+ };
115
+
116
+ window.ezstandalone.cmd.push(function () { run(); });
117
+
118
+ let tries = 0;
119
+ (function tick() {
120
+ tries++;
121
+ if (run() || tries >= 8) return;
122
+ setTimeout(tick, 800);
123
+ })();
124
+ }
125
+
126
+ function pickNextId(pool, usedSet) {
127
+ for (const id of pool) if (!usedSet.has(id)) return id;
128
+ return null;
129
+ }
130
+
131
+ function recycle(fifo, usedSet, selector) {
132
+ fifo.sort((a, b) => a.after - b.after);
133
+ while (fifo.length) {
134
+ const old = fifo.shift();
135
+ const el = document.querySelector(selector(old));
136
+ if (!el) continue;
137
+ el.remove();
138
+ usedSet.delete(old.id);
139
+ destroyPlaceholder(old.id);
140
+ return old.id;
141
+ }
142
+ return null;
143
+ }
144
+
145
+ function insertAfter(targetEl, id, cls, afterVal) {
146
+ ensureUniquePlaceholder(id);
147
+ const wrap = document.createElement('div');
148
+ wrap.className = 'ezoic-ad ' + cls;
149
+ wrap.setAttribute('data-ezoic-id', String(id));
150
+ wrap.setAttribute('data-ezoic-after', String(afterVal));
151
+ wrap.innerHTML = '<div class="ezoic-ad-inner"><div id="ezoic-pub-ad-placeholder-' + id + '"></div></div>';
152
+ targetEl.insertAdjacentElement('afterend', wrap);
153
+ }
154
+
155
+ function injectBetweenTopics(cfg) {
156
+ if (!cfg.enableBetweenAds) return;
157
+ const interval = Math.max(1, parseInt(cfg.intervalPosts, 10) || 6);
158
+ const pool = parsePool(cfg.placeholderIds);
159
+ if (!pool.length) return;
160
+
161
+ const items = Array.from(document.querySelectorAll('li[component="category/topic"]'));
162
+ if (!items.length) return;
163
+
164
+ items.forEach((li, idx) => {
165
+ const pos = idx + 1;
166
+ if (pos % interval !== 0) return;
167
+ if (idx === items.length - 1) return;
168
+
169
+ const next = li.nextElementSibling;
170
+ if (next && next.classList && next.classList.contains('ezoic-ad-between')) return;
171
+
172
+ let id = pickNextId(pool, usedBetween);
173
+ if (!id) {
174
+ id = recycle(fifoBetween, usedBetween, (old) => '.ezoic-ad-between[data-ezoic-id="' + old.id + '"][data-ezoic-after="' + old.after + '"]');
175
+ if (!id) return; // pool empty and nothing recyclable => stop
176
+ }
177
+
178
+ usedBetween.add(id);
179
+ fifoBetween.push({ id, after: pos });
180
+
181
+ insertAfter(li, id, 'ezoic-ad-between', pos);
182
+ callShowAdsSingle(id);
183
+ });
184
+ }
185
+
186
+ function injectBetweenMessages(cfg) {
187
+ if (!cfg.enableMessageAds) return;
188
+ const interval = Math.max(1, parseInt(cfg.messageIntervalPosts, 10) || 3);
189
+ const pool = parsePool(cfg.messagePlaceholderIds);
190
+ if (!pool.length) return;
191
+
192
+ const posts = Array.from(document.querySelectorAll('[component="post"][data-pid]'));
193
+ if (!posts.length) return;
194
+
195
+ posts.forEach((post, idx) => {
196
+ const no = idx + 1;
197
+ if (no % interval !== 0) return;
198
+ if (idx === posts.length - 1) return;
199
+
200
+ const next = post.nextElementSibling;
201
+ if (next && next.classList && next.classList.contains('ezoic-ad-message')) return;
202
+
203
+ let id = pickNextId(pool, usedMessage);
204
+ if (!id) {
205
+ id = recycle(fifoMessage, usedMessage, (old) => '.ezoic-ad-message[data-ezoic-id="' + old.id + '"][data-ezoic-after="' + old.after + '"]');
206
+ if (!id) return;
207
+ }
208
+
209
+ usedMessage.add(id);
210
+ fifoMessage.push({ id, after: no });
211
+
212
+ insertAfter(post, id, 'ezoic-ad-message', no);
213
+ callShowAdsSingle(id);
214
+ });
215
+ }
216
+
217
+ async function run() {
218
+ if (inFlight) { rerunRequested = true; return; }
219
+ inFlight = true;
220
+
221
+ try {
222
+ const key = getPageKey();
223
+ if (pageKey !== key) {
224
+ pageKey = key;
225
+ cleanupForNewPage();
226
+ }
227
+
228
+ const cfg = await fetchConfig();
229
+ if (!cfg || cfg.excluded) return;
230
+
231
+ if (isTopicPage()) injectBetweenMessages(cfg);
232
+ else if (isCategoryTopicList()) injectBetweenTopics(cfg);
233
+ } catch (e) {
234
+ // silent
235
+ } finally {
236
+ inFlight = false;
237
+ if (rerunRequested) {
238
+ rerunRequested = false;
239
+ setTimeout(run, 50);
240
+ }
241
+ }
242
+ }
243
+
244
+ function scheduleRun() {
245
+ clearTimeout(debounceTimer);
246
+ debounceTimer = setTimeout(run, 150);
247
+ }
248
+
249
+ document.addEventListener('DOMContentLoaded', function () {
250
+ run();
251
+ setTimeout(run, 1200);
252
+ });
253
+
254
+ window.addEventListener('action:ajaxify.end', scheduleRun);
255
+ window.addEventListener('action:posts.loaded', scheduleRun);
256
+ window.addEventListener('action:topic.loaded', scheduleRun);
257
+ window.addEventListener('action:topics.loaded', scheduleRun);
258
+ window.addEventListener('action:category.loaded', scheduleRun);
6
259
  })();
@@ -0,0 +1,3 @@
1
+ .ezoic-ad{height:auto !important; min-height:0 !important; padding:0 !important; margin:0.5rem 0;}
2
+ .ezoic-ad .ezoic-ad-inner{padding:0;margin:0;}
3
+ .ezoic-ad .ezoic-ad-inner > div{padding:0;margin:0;}
@@ -1,78 +1,59 @@
1
- <div class="row">
2
- <div class="col-lg-9">
3
- <div class="card">
4
- <div class="card-header">
5
- <strong>Ezoic Infinite</strong>
6
- </div>
7
- <div class="card-body">
8
- <form class="ezoic-infinite-settings">
9
- <div class="mb-3">
10
- <label class="form-label">Groupes exclus</label>
11
- <select multiple class="form-control" name="excludedGroups" size="8">
12
- <!-- BEGIN groups -->
13
- <option value="{groups.name}">{groups.name}</option>
14
- <!-- END groups -->
15
- </select>
16
- <p class="form-text">Aucune publicité ne sera affichée pour ces groupes.</p>
17
- </div>
1
+ <div class="acp-page-container">
2
+ <h2>Ezoic - Publicités Infinite Scroll Ads</h2>
18
3
 
19
- <hr />
4
+ <form class="ezoic-infinite-settings" role="form">
5
+ <h4 class="mt-3">Pubs entre les posts (bloc simple)</h4>
20
6
 
21
- <h5>Publicités entre les topics (liste des sujets)</h5>
22
-
23
- <div class="form-check mb-3">
24
- <input class="form-check-input" type="checkbox" name="enableBetweenAds" id="enableBetweenAds">
25
- <label class="form-check-label" for="enableBetweenAds">Activer</label>
26
- </div>
27
-
28
- <div class="mb-3">
29
- <label class="form-label">Intervalle (après chaque N topics)</label>
30
- <input type="number" class="form-control" name="intervalTopics" min="1" step="1">
31
- </div>
7
+ <div class="form-check mb-3">
8
+ <input class="form-check-input" type="checkbox" id="enableBetweenAds" name="enableBetweenAds" {enableBetweenAds_checked}>
9
+ <label class="form-check-label" for="enableBetweenAds">Activer les pubs entre les posts</label>
10
+ </div>
32
11
 
33
- <div class="mb-3">
34
- <label class="form-label">Pool d'IDs placeholder (un par ligne)</label>
35
- <textarea class="form-control" name="placeholderIds" rows="5"></textarea>
36
- </div>
12
+ <div class="mb-3">
13
+ <label class="form-label" for="placeholderIds">Pool dIDs Ezoic (entre posts)</label>
14
+ <textarea id="placeholderIds" name="placeholderIds" class="form-control" rows="4">{placeholderIds}</textarea>
15
+ <p class="form-text">Un ID par ligne (ou séparé par virgules/espaces). Le nombre d’IDs = nombre max de pubs simultanées.</p>
16
+ </div>
37
17
 
38
- <hr />
18
+ <div class="mb-3">
19
+ <label class="form-label" for="intervalPosts">Afficher une pub tous les N posts</label>
20
+ <input type="number" id="intervalPosts" name="intervalPosts" class="form-control" value="{intervalPosts}" min="1">
21
+ </div>
39
22
 
40
- <h5>Publicités entre les messages (dans un topic)</h5>
23
+ <hr/>
41
24
 
42
- <div class="form-check mb-3">
43
- <input class="form-check-input" type="checkbox" name="enableMessageAds" id="enableMessageAds">
44
- <label class="form-check-label" for="enableMessageAds">Activer</label>
45
- </div>
25
+ <h4 class="mt-3">Pubs “message” entre les réponses</h4>
26
+ <p class="form-text">Insère un bloc qui ressemble à un post, toutes les N réponses (dans une page topic).</p>
46
27
 
47
- <div class="mb-3">
48
- <label class="form-label">Intervalle (après chaque N messages)</label>
49
- <input type="number" class="form-control" name="messageIntervalPosts" min="1" step="1">
50
- </div>
28
+ <div class="form-check mb-3">
29
+ <input class="form-check-input" type="checkbox" id="enableMessageAds" name="enableMessageAds" {enableMessageAds_checked}>
30
+ <label class="form-check-label" for="enableMessageAds">Activer les pubs “message”</label>
31
+ </div>
51
32
 
52
- <div class="mb-3">
53
- <label class="form-label">Pool d'IDs placeholder messages (un par ligne)</label>
54
- <textarea class="form-control" name="messagePlaceholderIds" rows="5"></textarea>
55
- </div>
33
+ <div class="mb-3">
34
+ <label class="form-label" for="messagePlaceholderIds">Pool dIDs Ezoic (message)</label>
35
+ <textarea id="messagePlaceholderIds" name="messagePlaceholderIds" class="form-control" rows="4">{messagePlaceholderIds}</textarea>
36
+ <p class="form-text">Pool séparé recommandé pour éviter la réutilisation d’IDs. IMPORTANT : ne réutilise pas les mêmes IDs dans les deux pools.</p>
37
+ </div>
56
38
 
57
- <button type="button" class="btn btn-primary ezoic-infinite-save">
58
- <i class="fa fa-save"></i> Enregistrer
59
- </button>
60
- </form>
61
- </div>
39
+ <div class="mb-3">
40
+ <label class="form-label" for="messageIntervalPosts">Afficher un “message pub” tous les N messages</label>
41
+ <input type="number" id="messageIntervalPosts" name="messageIntervalPosts" class="form-control" value="{messageIntervalPosts}" min="1">
62
42
  </div>
63
- </div>
64
43
 
65
- <div class="col-lg-3">
66
- <div class="card">
67
- <div class="card-header">
68
- <strong>Aide</strong>
69
- </div>
70
- <div class="card-body">
71
- <p>Placeholder Ezoic :</p>
72
- <pre class="mb-0">&lt;div id="ezoic-pub-ad-placeholder-123"&gt;&lt;/div&gt;</pre>
73
- <hr />
74
- <p class="mb-0">Les IDs doivent exister côté Ezoic (ad units configurées).</p>
75
- </div>
44
+ <hr/>
45
+
46
+ <h4 class="mt-3">Exclusions</h4>
47
+ <div class="mb-3">
48
+ <label class="form-label" for="excludedGroups">Groupes exclus</label>
49
+ <select id="excludedGroups" name="excludedGroups" class="form-select" multiple>
50
+ <!-- BEGIN allGroups -->
51
+ <option value="{allGroups.name}">{allGroups.name}</option>
52
+ <!-- END allGroups -->
53
+ </select>
54
+ <p class="form-text">Si l’utilisateur appartient à un de ces groupes, aucune pub n’est injectée.</p>
76
55
  </div>
77
- </div>
56
+
57
+ <button id="save" class="btn btn-primary">Enregistrer</button>
58
+ </form>
78
59
  </div>