@intranefr/superbackend 1.6.7 → 1.7.8
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/.beads/.br_history/issues.20260314_212352_900045509.jsonl +0 -0
- package/.beads/.br_history/issues.20260314_212352_900045509.jsonl.meta.json +1 -0
- package/.beads/.br_history/issues.20260314_212353_087140743.jsonl +1 -0
- package/.beads/.br_history/issues.20260314_212353_087140743.jsonl.meta.json +1 -0
- package/.beads/.br_history/issues.20260314_212353_285881504.jsonl +2 -0
- package/.beads/.br_history/issues.20260314_212353_285881504.jsonl.meta.json +1 -0
- package/.beads/.br_history/issues.20260314_212353_473915419.jsonl +3 -0
- package/.beads/.br_history/issues.20260314_212353_473915419.jsonl.meta.json +1 -0
- package/.beads/.br_history/issues.20260314_212353_659476307.jsonl +4 -0
- package/.beads/.br_history/issues.20260314_212353_659476307.jsonl.meta.json +1 -0
- package/.beads/.br_history/issues.20260314_212353_869998925.jsonl +5 -0
- package/.beads/.br_history/issues.20260314_212353_869998925.jsonl.meta.json +1 -0
- package/.beads/.br_history/issues.20260314_212354_054785029.jsonl +6 -0
- package/.beads/.br_history/issues.20260314_212354_054785029.jsonl.meta.json +1 -0
- package/.beads/.br_history/issues.20260314_213336_175893691.jsonl +7 -0
- package/.beads/.br_history/issues.20260314_213336_175893691.jsonl.meta.json +1 -0
- package/.beads/.br_history/issues.20260314_213336_338509797.jsonl +7 -0
- package/.beads/.br_history/issues.20260314_213336_338509797.jsonl.meta.json +1 -0
- package/.beads/.br_history/issues.20260314_213336_515443192.jsonl +7 -0
- package/.beads/.br_history/issues.20260314_213336_515443192.jsonl.meta.json +1 -0
- package/.beads/.br_history/issues.20260314_213336_676417592.jsonl +7 -0
- package/.beads/.br_history/issues.20260314_213336_676417592.jsonl.meta.json +1 -0
- package/.beads/.br_history/issues.20260314_213336_839182422.jsonl +7 -0
- package/.beads/.br_history/issues.20260314_213336_839182422.jsonl.meta.json +1 -0
- package/.beads/.br_history/issues.20260314_213337_004349113.jsonl +7 -0
- package/.beads/.br_history/issues.20260314_213337_004349113.jsonl.meta.json +1 -0
- package/.beads/.br_history/issues.20260314_213337_179824080.jsonl +7 -0
- package/.beads/.br_history/issues.20260314_213337_179824080.jsonl.meta.json +1 -0
- package/.beads/.br_history/issues.20260314_213701_705075332.jsonl +7 -0
- package/.beads/.br_history/issues.20260314_213701_705075332.jsonl.meta.json +1 -0
- package/.beads/.br_history/issues.20260314_213706_783128702.jsonl +8 -0
- package/.beads/.br_history/issues.20260314_213706_783128702.jsonl.meta.json +1 -0
- package/.beads/config.yaml +4 -0
- package/.beads/issues.jsonl +8 -0
- package/.beads/metadata.json +4 -0
- package/.env.example +8 -0
- package/autochangelog/.env.example +36 -0
- package/autochangelog/README.md +412 -0
- package/autochangelog/config/database.js +27 -0
- package/autochangelog/package.json +47 -0
- package/autochangelog/public/landing.html +581 -0
- package/autochangelog/server.js +104 -0
- package/autochangelog/src/app.js +181 -0
- package/autochangelog/src/config/database.js +26 -0
- package/autochangelog/src/controllers/auth.js +488 -0
- package/autochangelog/src/controllers/changelog.js +682 -0
- package/autochangelog/src/controllers/project.js +580 -0
- package/autochangelog/src/controllers/repository.js +780 -0
- package/autochangelog/src/middleware/auth.js +386 -0
- package/autochangelog/src/models/Changelog.js +443 -0
- package/autochangelog/src/models/Project.js +226 -0
- package/autochangelog/src/models/Repository.js +366 -0
- package/autochangelog/src/models/User.js +223 -0
- package/autochangelog/src/routes/auth.routes.js +32 -0
- package/autochangelog/src/routes/changelog.routes.js +42 -0
- package/autochangelog/src/routes/github-auth.routes.js +102 -0
- package/autochangelog/src/routes/project.routes.js +50 -0
- package/autochangelog/src/routes/repository.routes.js +54 -0
- package/autochangelog/src/services/changelog.js +722 -0
- package/autochangelog/src/services/github.js +243 -0
- package/autochangelog/utils/logger.js +77 -0
- package/autochangelog/views/404.ejs +18 -0
- package/autochangelog/views/dashboard.ejs +596 -0
- package/autochangelog/views/index.ejs +231 -0
- package/autochangelog/views/layouts/main.ejs +44 -0
- package/autochangelog/views/login.ejs +104 -0
- package/autochangelog/views/partials/footer.ejs +20 -0
- package/autochangelog/views/partials/navbar.ejs +51 -0
- package/autochangelog/views/register.ejs +109 -0
- package/autochangelog-cli/README.md +266 -0
- package/autochangelog-cli/bin/autochangelog +120 -0
- package/autochangelog-cli/package.json +46 -0
- package/autochangelog-cli/src/cli/commands/auth.js +291 -0
- package/autochangelog-cli/src/cli/commands/changelog.js +619 -0
- package/autochangelog-cli/src/cli/commands/project.js +427 -0
- package/autochangelog-cli/src/cli/commands/repo.js +557 -0
- package/autochangelog-cli/src/cli/commands/stats.js +706 -0
- package/autochangelog-cli/src/cli/utils/config.js +277 -0
- package/autochangelog-cli/src/cli/utils/errors.js +307 -0
- package/autochangelog-cli/src/cli/utils/logger.js +75 -0
- package/autochangelog-cli/src/cli/utils/output.js +357 -0
- package/package.json +9 -3
- package/plugins/supercli/README.md +108 -0
- package/plugins/supercli/plugin.json +123 -0
- package/server.js +1 -1
- package/src/cli/api.js +380 -0
- package/src/cli/direct/agent-utils.js +61 -0
- package/src/cli/direct/cli-utils.js +112 -0
- package/src/cli/direct/data-seeding.js +307 -0
- package/src/cli/direct/db-admin.js +84 -0
- package/src/cli/direct/db-advanced.js +372 -0
- package/src/cli/direct/db-utils.js +558 -0
- package/src/cli/direct/help.js +195 -0
- package/src/cli/direct/migration.js +107 -0
- package/src/cli/direct/rbac-advanced.js +132 -0
- package/src/cli/direct/resources-additional.js +400 -0
- package/src/cli/direct/resources-cms-advanced.js +173 -0
- package/src/cli/direct/resources-cms.js +247 -0
- package/src/cli/direct/resources-core.js +253 -0
- package/src/cli/direct/resources-execution.js +367 -0
- package/src/cli/direct/resources-health.js +152 -0
- package/src/cli/direct/resources-integrations.js +182 -0
- package/src/cli/direct/resources-logs.js +204 -0
- package/src/cli/direct/resources-org-rbac.js +187 -0
- package/src/cli/direct/resources-system.js +236 -0
- package/src/cli/direct.js +556 -0
- package/src/controllers/admin.controller.js +4 -0
- package/src/controllers/auth.controller.js +148 -1
- package/src/controllers/waitingList.controller.js +130 -1
- package/src/models/RbacRole.js +1 -1
- package/src/models/User.js +39 -5
- package/src/routes/auth.routes.js +6 -0
- package/src/routes/waitingList.routes.js +12 -2
- package/src/routes/waitingListAdmin.routes.js +3 -0
- package/src/services/email.service.js +1 -0
- package/src/services/github.service.js +255 -0
- package/src/services/rateLimiter.service.js +29 -1
- package/src/services/waitingListJson.service.js +32 -3
- package/views/admin-waiting-list.ejs +386 -3
|
@@ -71,18 +71,43 @@
|
|
|
71
71
|
</div>
|
|
72
72
|
|
|
73
73
|
<!-- Entries table (admin) -->
|
|
74
|
-
<div class="bg-white rounded-lg shadow p-6 mb-8">
|
|
74
|
+
<div class="bg-white rounded-lg shadow p-6 mb-8 relative">
|
|
75
75
|
<div class="flex flex-col gap-4 sm:flex-row sm:items-end sm:justify-between">
|
|
76
76
|
<div>
|
|
77
77
|
<h2 class="text-xl font-bold text-gray-900">Entries</h2>
|
|
78
78
|
<p id="entries-subtitle" class="text-sm text-gray-600 mt-1">Loading...</p>
|
|
79
79
|
</div>
|
|
80
80
|
<div class="flex gap-2">
|
|
81
|
+
<button id="btn-export-csv" class="bg-green-500 text-white px-4 py-2 rounded hover:bg-green-600 flex items-center gap-2">
|
|
82
|
+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
83
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
|
|
84
|
+
</svg>
|
|
85
|
+
Export CSV
|
|
86
|
+
</button>
|
|
81
87
|
<button id="btn-copy-visible" class="bg-gray-100 text-gray-800 px-4 py-2 rounded hover:bg-gray-200">Copy visible</button>
|
|
82
88
|
<button id="btn-refresh-entries" class="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600">Refresh</button>
|
|
83
89
|
</div>
|
|
84
90
|
</div>
|
|
85
91
|
|
|
92
|
+
<!-- Loading Overlay -->
|
|
93
|
+
<div id="entries-loading" class="hidden absolute inset-0 bg-white bg-opacity-75 flex items-center justify-center z-10 rounded-lg">
|
|
94
|
+
<div class="flex items-center gap-2 text-gray-600">
|
|
95
|
+
<svg class="animate-spin h-5 w-5 text-blue-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
|
96
|
+
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
|
97
|
+
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
|
98
|
+
</svg>
|
|
99
|
+
<span>Loading...</span>
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
|
|
103
|
+
<!-- Type Filter Cards -->
|
|
104
|
+
<div class="mt-6">
|
|
105
|
+
<h3 class="text-sm font-medium text-gray-700 mb-3">Filter by Type</h3>
|
|
106
|
+
<div id="type-filter-cards" class="flex flex-wrap gap-3">
|
|
107
|
+
<!-- Cards will be rendered here -->
|
|
108
|
+
</div>
|
|
109
|
+
</div>
|
|
110
|
+
|
|
86
111
|
<div class="grid grid-cols-1 sm:grid-cols-4 gap-4 mt-4">
|
|
87
112
|
<div>
|
|
88
113
|
<label class="block text-sm font-medium text-gray-700 mb-1">Status</label>
|
|
@@ -111,6 +136,13 @@
|
|
|
111
136
|
<button id="btn-entries-apply" class="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600">Apply</button>
|
|
112
137
|
<button id="btn-entries-reset" class="bg-gray-100 text-gray-800 px-4 py-2 rounded hover:bg-gray-200">Reset</button>
|
|
113
138
|
<div class="flex-1"></div>
|
|
139
|
+
<button id="btn-bulk-remove" class="bg-red-500 text-white px-4 py-2 rounded hover:bg-red-600 disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2" disabled>
|
|
140
|
+
<svg id="bulk-remove-spinner" class="animate-spin h-4 w-4 hidden" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
|
141
|
+
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
|
142
|
+
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
|
143
|
+
</svg>
|
|
144
|
+
Delete Selected (<span id="selected-count">0</span>)
|
|
145
|
+
</button>
|
|
114
146
|
<button id="btn-entries-prev" class="bg-gray-100 text-gray-800 px-4 py-2 rounded hover:bg-gray-200">Prev</button>
|
|
115
147
|
<button id="btn-entries-next" class="bg-gray-100 text-gray-800 px-4 py-2 rounded hover:bg-gray-200">Next</button>
|
|
116
148
|
</div>
|
|
@@ -119,6 +151,9 @@
|
|
|
119
151
|
<table class="min-w-full divide-y divide-gray-200">
|
|
120
152
|
<thead class="bg-gray-50">
|
|
121
153
|
<tr>
|
|
154
|
+
<th class="px-4 py-3 text-center text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
155
|
+
<input type="checkbox" id="select-all-entries" class="rounded border-gray-300 text-blue-600 focus:ring-blue-500">
|
|
156
|
+
</th>
|
|
122
157
|
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Created</th>
|
|
123
158
|
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Email</th>
|
|
124
159
|
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Type</th>
|
|
@@ -166,6 +201,48 @@
|
|
|
166
201
|
<pre id="subscribe-response" class="bg-gray-100 p-4 rounded overflow-auto max-h-96"></pre>
|
|
167
202
|
</div>
|
|
168
203
|
</div>
|
|
204
|
+
|
|
205
|
+
<!-- Integration -->
|
|
206
|
+
<div class="bg-white rounded-lg shadow p-6">
|
|
207
|
+
<details class="group">
|
|
208
|
+
<summary class="flex items-center justify-between cursor-pointer list-none">
|
|
209
|
+
<div>
|
|
210
|
+
<h2 class="text-xl font-bold text-gray-900">Integration</h2>
|
|
211
|
+
<p class="text-sm text-gray-600 mt-1">cURL example for external apps</p>
|
|
212
|
+
</div>
|
|
213
|
+
<span class="transition-transform group-open:rotate-180">
|
|
214
|
+
<svg class="w-6 h-6 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
215
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
|
|
216
|
+
</svg>
|
|
217
|
+
</span>
|
|
218
|
+
</summary>
|
|
219
|
+
|
|
220
|
+
<div class="mt-6">
|
|
221
|
+
<div class="bg-gray-900 rounded-lg p-4 overflow-x-auto">
|
|
222
|
+
<div class="flex items-center justify-between mb-2">
|
|
223
|
+
<span class="text-xs font-mono text-gray-400">bash</span>
|
|
224
|
+
<button id="btn-copy-curl" class="text-xs bg-gray-700 hover:bg-gray-600 text-white px-3 py-1 rounded flex items-center gap-1">
|
|
225
|
+
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
226
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"></path>
|
|
227
|
+
</svg>
|
|
228
|
+
Copy
|
|
229
|
+
</button>
|
|
230
|
+
</div>
|
|
231
|
+
<pre id="curl-example" class="text-green-400 font-mono text-sm whitespace-pre-wrap break-all"></pre>
|
|
232
|
+
</div>
|
|
233
|
+
|
|
234
|
+
<div class="mt-4 text-sm text-gray-600">
|
|
235
|
+
<p class="mb-2"><strong class="text-gray-900">Endpoint:</strong> <code class="bg-gray-100 px-2 py-1 rounded">POST /api/waiting-list/subscribe</code></p>
|
|
236
|
+
<p class="mb-2"><strong class="text-gray-900">Fields:</strong></p>
|
|
237
|
+
<ul class="list-disc list-inside space-y-1 ml-2">
|
|
238
|
+
<li><code class="bg-gray-100 px-2 py-0.5 rounded text-sm">email</code> (required) - User email address</li>
|
|
239
|
+
<li><code class="bg-gray-100 px-2 py-0.5 rounded text-sm">type</code> (required) - Interest type (e.g., buyer, seller, both)</li>
|
|
240
|
+
<li><code class="bg-gray-100 px-2 py-0.5 rounded text-sm">referralSource</code> (optional) - Traffic source (default: website)</li>
|
|
241
|
+
</ul>
|
|
242
|
+
</div>
|
|
243
|
+
</div>
|
|
244
|
+
</details>
|
|
245
|
+
</div>
|
|
169
246
|
</div>
|
|
170
247
|
</div>
|
|
171
248
|
|
|
@@ -178,6 +255,27 @@
|
|
|
178
255
|
const WAITING_LIST_SUBSCRIBE_PATH = '/api/waiting-list/subscribe';
|
|
179
256
|
const WAITING_LIST_ADMIN_LIST_PATH = '/api/admin/waiting-list';
|
|
180
257
|
|
|
258
|
+
// Generate cURL example with all fields
|
|
259
|
+
function generateCurlExample() {
|
|
260
|
+
const baseUrl = window.location.origin;
|
|
261
|
+
const curlCommand = `curl -X POST ${baseUrl}/api/waiting-list/subscribe \\
|
|
262
|
+
-H "Content-Type: application/json" \\
|
|
263
|
+
-d '{
|
|
264
|
+
"email": "user@example.com",
|
|
265
|
+
"type": "buyer",
|
|
266
|
+
"referralSource": "website"
|
|
267
|
+
}'`;
|
|
268
|
+
return curlCommand;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Render cURL example on page load
|
|
272
|
+
function renderCurlExample() {
|
|
273
|
+
const curlEl = document.getElementById('curl-example');
|
|
274
|
+
if (curlEl) {
|
|
275
|
+
curlEl.textContent = generateCurlExample();
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
181
279
|
function showToast(message, type = 'success') {
|
|
182
280
|
const container = document.getElementById('toast-container');
|
|
183
281
|
const toast = document.createElement('div');
|
|
@@ -272,8 +370,175 @@
|
|
|
272
370
|
|
|
273
371
|
let entriesState = { offset: 0, total: 0, limit: 50 };
|
|
274
372
|
let lastEntries = [];
|
|
373
|
+
let selectedType = null; // Track selected type filter
|
|
374
|
+
let selectedEntryIds = new Set(); // Track selected entry IDs for bulk delete
|
|
375
|
+
let isLoading = false;
|
|
376
|
+
|
|
377
|
+
function showLoading(show) {
|
|
378
|
+
const loadingEl = document.getElementById('entries-loading');
|
|
379
|
+
if (loadingEl) {
|
|
380
|
+
loadingEl.classList.toggle('hidden', !show);
|
|
381
|
+
}
|
|
382
|
+
isLoading = show;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
async function updateBulkDeleteButton() {
|
|
386
|
+
const btn = document.getElementById('btn-bulk-remove');
|
|
387
|
+
const countEl = document.getElementById('selected-count');
|
|
388
|
+
if (btn && countEl) {
|
|
389
|
+
const count = selectedEntryIds.size;
|
|
390
|
+
countEl.textContent = count;
|
|
391
|
+
btn.disabled = count === 0 || isLoading;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function toggleEntrySelection(entryId, isChecked) {
|
|
396
|
+
if (isChecked) {
|
|
397
|
+
selectedEntryIds.add(entryId);
|
|
398
|
+
} else {
|
|
399
|
+
selectedEntryIds.delete(entryId);
|
|
400
|
+
}
|
|
401
|
+
updateBulkDeleteButton();
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function toggleSelectAll(isChecked) {
|
|
405
|
+
if (isChecked) {
|
|
406
|
+
// Select all visible entries
|
|
407
|
+
lastEntries.forEach(entry => {
|
|
408
|
+
if (entry && entry.id) {
|
|
409
|
+
selectedEntryIds.add(entry.id);
|
|
410
|
+
}
|
|
411
|
+
});
|
|
412
|
+
} else {
|
|
413
|
+
// Deselect only visible entries
|
|
414
|
+
lastEntries.forEach(entry => {
|
|
415
|
+
if (entry && entry.id) {
|
|
416
|
+
selectedEntryIds.delete(entry.id);
|
|
417
|
+
}
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
updateBulkDeleteButton();
|
|
421
|
+
|
|
422
|
+
// Update individual checkboxes to match select-all state
|
|
423
|
+
document.querySelectorAll('.entry-checkbox').forEach(cb => {
|
|
424
|
+
cb.checked = isChecked;
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
async function loadTypes() {
|
|
429
|
+
try {
|
|
430
|
+
const res = await fetch(`${API_BASE}/api/admin/waiting-list/types`, {
|
|
431
|
+
headers: { 'Accept': 'application/json' }
|
|
432
|
+
});
|
|
433
|
+
const data = await res.json();
|
|
434
|
+
|
|
435
|
+
if (!res.ok) {
|
|
436
|
+
console.error('Failed to load types:', data?.error);
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const container = document.getElementById('type-filter-cards');
|
|
441
|
+
if (!container) return;
|
|
442
|
+
|
|
443
|
+
const types = data?.types || [];
|
|
444
|
+
|
|
445
|
+
// Render ALL card
|
|
446
|
+
let html = `
|
|
447
|
+
<button
|
|
448
|
+
class="type-filter-card px-4 py-3 rounded-lg border-2 transition-all ${
|
|
449
|
+
selectedType === null
|
|
450
|
+
? 'border-blue-500 bg-blue-50 shadow-md'
|
|
451
|
+
: 'border-gray-200 bg-white hover:border-gray-300'
|
|
452
|
+
}"
|
|
453
|
+
data-type=""
|
|
454
|
+
>
|
|
455
|
+
<div class="text-sm font-medium text-gray-600">ALL</div>
|
|
456
|
+
<div class="text-2xl font-bold text-gray-900 mt-1">${data?.total || 0}</div>
|
|
457
|
+
</button>
|
|
458
|
+
`;
|
|
459
|
+
|
|
460
|
+
// Render type cards
|
|
461
|
+
types.forEach(({ type, count }) => {
|
|
462
|
+
const isSelected = selectedType === type;
|
|
463
|
+
const safeType = String(type)
|
|
464
|
+
.replaceAll('&', '&')
|
|
465
|
+
.replaceAll('<', '<')
|
|
466
|
+
.replaceAll('>', '>')
|
|
467
|
+
.replaceAll('"', '"')
|
|
468
|
+
.replaceAll("'", ''');
|
|
469
|
+
|
|
470
|
+
html += `
|
|
471
|
+
<button
|
|
472
|
+
class="type-filter-card px-4 py-3 rounded-lg border-2 transition-all ${
|
|
473
|
+
isSelected
|
|
474
|
+
? 'border-blue-500 bg-blue-50 shadow-md'
|
|
475
|
+
: 'border-gray-200 bg-white hover:border-gray-300'
|
|
476
|
+
}"
|
|
477
|
+
data-type="${safeType}"
|
|
478
|
+
>
|
|
479
|
+
<div class="text-sm font-medium text-gray-600">${safeType}</div>
|
|
480
|
+
<div class="text-2xl font-bold text-gray-900 mt-1">${count}</div>
|
|
481
|
+
</button>
|
|
482
|
+
`;
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
container.innerHTML = html;
|
|
486
|
+
|
|
487
|
+
// Bind click events
|
|
488
|
+
container.querySelectorAll('.type-filter-card').forEach((card) => {
|
|
489
|
+
card.addEventListener('click', () => handleTypeFilterClick(card));
|
|
490
|
+
});
|
|
491
|
+
} catch (e) {
|
|
492
|
+
console.error('Error loading types:', e);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function handleTypeFilterClick(card) {
|
|
497
|
+
const type = card.getAttribute('data-type');
|
|
498
|
+
selectedType = type || null;
|
|
499
|
+
|
|
500
|
+
// Update UI - highlight selected card
|
|
501
|
+
document.querySelectorAll('.type-filter-card').forEach((c) => {
|
|
502
|
+
c.classList.remove('border-blue-500', 'bg-blue-50', 'shadow-md');
|
|
503
|
+
c.classList.add('border-gray-200', 'hover:border-gray-300');
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
card.classList.remove('border-gray-200', 'hover:border-gray-300');
|
|
507
|
+
card.classList.add('border-blue-500', 'bg-blue-50', 'shadow-md');
|
|
508
|
+
|
|
509
|
+
// Update hidden input and reload entries
|
|
510
|
+
const typeInput = document.getElementById('entries-type');
|
|
511
|
+
if (typeInput) typeInput.value = selectedType || '';
|
|
512
|
+
|
|
513
|
+
// Reset pagination and reload
|
|
514
|
+
entriesState.offset = 0;
|
|
515
|
+
loadEntries();
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
function exportCsv() {
|
|
519
|
+
// Get current filter values
|
|
520
|
+
const status = document.getElementById('entries-status')?.value?.trim();
|
|
521
|
+
const type = document.getElementById('entries-type')?.value?.trim();
|
|
522
|
+
const email = document.getElementById('entries-email')?.value?.trim();
|
|
523
|
+
|
|
524
|
+
// Build query string with current filters
|
|
525
|
+
const params = new URLSearchParams();
|
|
526
|
+
if (status) params.set('status', status);
|
|
527
|
+
if (type) params.set('type', type);
|
|
528
|
+
if (email) params.set('email', email);
|
|
529
|
+
|
|
530
|
+
// Create download link and trigger it
|
|
531
|
+
const downloadUrl = `${API_BASE}/api/admin/waiting-list/export-csv?${params.toString()}`;
|
|
532
|
+
|
|
533
|
+
// Open in new tab to trigger download
|
|
534
|
+
window.open(downloadUrl, '_blank');
|
|
535
|
+
|
|
536
|
+
showToast('CSV export started', 'success');
|
|
537
|
+
}
|
|
275
538
|
|
|
276
539
|
async function loadEntries() {
|
|
540
|
+
if (isLoading) return; // Prevent concurrent loads
|
|
541
|
+
|
|
277
542
|
const status = document.getElementById('entries-status')?.value?.trim();
|
|
278
543
|
const type = document.getElementById('entries-type')?.value?.trim();
|
|
279
544
|
const email = document.getElementById('entries-email')?.value?.trim();
|
|
@@ -281,16 +546,20 @@
|
|
|
281
546
|
|
|
282
547
|
const subtitle = document.getElementById('entries-subtitle');
|
|
283
548
|
const tbody = document.getElementById('entries-tbody');
|
|
549
|
+
|
|
550
|
+
showLoading(true);
|
|
284
551
|
if (subtitle) subtitle.textContent = 'Loading...';
|
|
285
552
|
if (tbody) tbody.innerHTML = '';
|
|
286
553
|
|
|
287
554
|
try {
|
|
555
|
+
// Add timestamp to bypass browser cache
|
|
288
556
|
const url = `${API_BASE}${WAITING_LIST_ADMIN_LIST_PATH}${qs({
|
|
289
557
|
status: status || undefined,
|
|
290
558
|
type: type || undefined,
|
|
291
559
|
email: email || undefined,
|
|
292
560
|
limit,
|
|
293
561
|
offset: entriesState.offset,
|
|
562
|
+
_t: Date.now() // Cache buster
|
|
294
563
|
})}`;
|
|
295
564
|
|
|
296
565
|
const res = await fetch(url, { headers: { 'Accept': 'application/json' } });
|
|
@@ -332,9 +601,19 @@
|
|
|
332
601
|
const statusCell = escapeHtml(e?.status);
|
|
333
602
|
const refCell = escapeHtml(e?.referralSource || '');
|
|
334
603
|
const rawEmail = String(e?.email || '');
|
|
604
|
+
const entryId = e?.id || '';
|
|
605
|
+
const isChecked = selectedEntryIds.has(entryId) ? 'checked' : '';
|
|
335
606
|
|
|
336
607
|
return `
|
|
337
|
-
<tr>
|
|
608
|
+
<tr class="${isChecked ? 'bg-blue-50' : ''}">
|
|
609
|
+
<td class="px-4 py-3 text-sm text-center">
|
|
610
|
+
<input
|
|
611
|
+
type="checkbox"
|
|
612
|
+
class="entry-checkbox rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
|
613
|
+
data-entry-id="${escapeHtml(entryId)}"
|
|
614
|
+
${isChecked}
|
|
615
|
+
>
|
|
616
|
+
</td>
|
|
338
617
|
<td class="px-4 py-3 text-sm text-gray-900 whitespace-nowrap">${createdAt}</td>
|
|
339
618
|
<td class="px-4 py-3 text-sm text-gray-900">${emailCell}</td>
|
|
340
619
|
<td class="px-4 py-3 text-sm text-gray-700">${typeCell}</td>
|
|
@@ -347,6 +626,16 @@
|
|
|
347
626
|
`;
|
|
348
627
|
}).join('');
|
|
349
628
|
|
|
629
|
+
// Bind checkbox events
|
|
630
|
+
tbody.querySelectorAll('.entry-checkbox').forEach((cb) => {
|
|
631
|
+
cb.addEventListener('change', (e) => {
|
|
632
|
+
const entryId = e.target.getAttribute('data-entry-id');
|
|
633
|
+
toggleEntrySelection(entryId, e.target.checked);
|
|
634
|
+
// Highlight row
|
|
635
|
+
e.target.closest('tr').classList.toggle('bg-blue-50', e.target.checked);
|
|
636
|
+
});
|
|
637
|
+
});
|
|
638
|
+
|
|
350
639
|
tbody.querySelectorAll('[data-copy-email]').forEach((btn) => {
|
|
351
640
|
btn.addEventListener('click', () => copyText(btn.getAttribute('data-copy-email') || ''));
|
|
352
641
|
});
|
|
@@ -357,9 +646,68 @@
|
|
|
357
646
|
const nextBtn = document.getElementById('btn-entries-next');
|
|
358
647
|
if (prevBtn) prevBtn.disabled = usedOffset <= 0;
|
|
359
648
|
if (nextBtn) nextBtn.disabled = usedOffset + usedLimit >= total;
|
|
649
|
+
|
|
650
|
+
// Update select-all checkbox state based on current selection
|
|
651
|
+
const selectAllCb = document.getElementById('select-all-entries');
|
|
652
|
+
if (selectAllCb && entries.length > 0) {
|
|
653
|
+
const allVisibleSelected = entries.every(e => selectedEntryIds.has(e.id));
|
|
654
|
+
selectAllCb.checked = allVisibleSelected;
|
|
655
|
+
selectAllCb.indeterminate = !allVisibleSelected && entries.some(e => selectedEntryIds.has(e.id));
|
|
656
|
+
}
|
|
360
657
|
} catch (e) {
|
|
361
658
|
showToast(e.message || 'Failed to load entries', 'error');
|
|
362
659
|
if (subtitle) subtitle.textContent = 'Failed to load.';
|
|
660
|
+
} finally {
|
|
661
|
+
showLoading(false);
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
async function bulkRemoveEntries() {
|
|
666
|
+
if (selectedEntryIds.size === 0) {
|
|
667
|
+
showToast('No entries selected', 'error');
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
const count = selectedEntryIds.size;
|
|
672
|
+
if (!confirm(`Are you sure you want to delete ${count} entr${count === 1 ? 'y' : 'ies'}? This action cannot be undone.`)) {
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
try {
|
|
677
|
+
showLoading(true);
|
|
678
|
+
const btn = document.getElementById('btn-bulk-remove');
|
|
679
|
+
const spinner = document.getElementById('bulk-remove-spinner');
|
|
680
|
+
if (btn) btn.disabled = true;
|
|
681
|
+
if (spinner) spinner.classList.remove('hidden');
|
|
682
|
+
|
|
683
|
+
const res = await fetch(`${API_BASE}/api/admin/waiting-list/bulk-remove`, {
|
|
684
|
+
method: 'POST',
|
|
685
|
+
headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
|
|
686
|
+
body: JSON.stringify({ entryIds: Array.from(selectedEntryIds) }),
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
const data = await res.json();
|
|
690
|
+
|
|
691
|
+
if (!res.ok) {
|
|
692
|
+
showToast(data?.error || 'Failed to delete entries', 'error');
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
showToast(`Successfully deleted ${data.removed?.count || 0} entrie(s)`, 'success');
|
|
697
|
+
|
|
698
|
+
// Clear selection and reload
|
|
699
|
+
selectedEntryIds.clear();
|
|
700
|
+
updateBulkDeleteButton();
|
|
701
|
+
loadEntries();
|
|
702
|
+
loadTypes(); // Refresh type counts
|
|
703
|
+
} catch (e) {
|
|
704
|
+
showToast(e.message || 'Failed to delete entries', 'error');
|
|
705
|
+
} finally {
|
|
706
|
+
showLoading(false);
|
|
707
|
+
const btn = document.getElementById('btn-bulk-remove');
|
|
708
|
+
const spinner = document.getElementById('bulk-remove-spinner');
|
|
709
|
+
if (btn) btn.disabled = true; // Will be re-enabled when selection changes
|
|
710
|
+
if (spinner) spinner.classList.add('hidden');
|
|
363
711
|
}
|
|
364
712
|
}
|
|
365
713
|
|
|
@@ -428,7 +776,28 @@
|
|
|
428
776
|
if (refreshBtn) refreshBtn.onclick = loadStats;
|
|
429
777
|
|
|
430
778
|
const refreshEntriesBtn = document.getElementById('btn-refresh-entries');
|
|
431
|
-
if (refreshEntriesBtn) refreshEntriesBtn.onclick =
|
|
779
|
+
if (refreshEntriesBtn) refreshEntriesBtn.onclick = () => {
|
|
780
|
+
selectedType = null; // Reset type filter on refresh
|
|
781
|
+
loadEntries();
|
|
782
|
+
loadTypes(); // Reload types to update counts
|
|
783
|
+
};
|
|
784
|
+
|
|
785
|
+
const exportCsvBtn = document.getElementById('btn-export-csv');
|
|
786
|
+
if (exportCsvBtn) {
|
|
787
|
+
exportCsvBtn.onclick = exportCsv;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
const bulkRemoveBtn = document.getElementById('btn-bulk-remove');
|
|
791
|
+
if (bulkRemoveBtn) {
|
|
792
|
+
bulkRemoveBtn.onclick = bulkRemoveEntries;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
const selectAllCb = document.getElementById('select-all-entries');
|
|
796
|
+
if (selectAllCb) {
|
|
797
|
+
selectAllCb.addEventListener('change', (e) => {
|
|
798
|
+
toggleSelectAll(e.target.checked);
|
|
799
|
+
});
|
|
800
|
+
}
|
|
432
801
|
|
|
433
802
|
const applyEntriesBtn = document.getElementById('btn-entries-apply');
|
|
434
803
|
if (applyEntriesBtn) {
|
|
@@ -445,8 +814,12 @@
|
|
|
445
814
|
document.getElementById('entries-type').value = '';
|
|
446
815
|
document.getElementById('entries-email').value = '';
|
|
447
816
|
document.getElementById('entries-limit').value = '50';
|
|
817
|
+
selectedType = null; // Reset type filter
|
|
818
|
+
selectedEntryIds.clear(); // Clear selection
|
|
448
819
|
entriesState.offset = 0;
|
|
449
820
|
loadEntries();
|
|
821
|
+
loadTypes(); // Reload types to update UI
|
|
822
|
+
updateBulkDeleteButton();
|
|
450
823
|
};
|
|
451
824
|
}
|
|
452
825
|
|
|
@@ -491,6 +864,14 @@
|
|
|
491
864
|
};
|
|
492
865
|
}
|
|
493
866
|
|
|
867
|
+
const copyCurlBtn = document.getElementById('btn-copy-curl');
|
|
868
|
+
if (copyCurlBtn) {
|
|
869
|
+
copyCurlBtn.onclick = () => {
|
|
870
|
+
const curlCommand = generateCurlExample();
|
|
871
|
+
copyText(curlCommand);
|
|
872
|
+
};
|
|
873
|
+
}
|
|
874
|
+
|
|
494
875
|
const form = document.getElementById('subscribe-form');
|
|
495
876
|
if (form) {
|
|
496
877
|
form.onsubmit = async (event) => {
|
|
@@ -533,7 +914,9 @@
|
|
|
533
914
|
|
|
534
915
|
bindEvents();
|
|
535
916
|
loadStats();
|
|
917
|
+
loadTypes();
|
|
536
918
|
loadEntries();
|
|
919
|
+
renderCurlExample();
|
|
537
920
|
</script>
|
|
538
921
|
<script>
|
|
539
922
|
window.addEventListener("keydown", (e) => {
|