@mtldev514/retro-portfolio-engine 1.0.0
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/README.md +408 -0
- package/bin/cli.js +103 -0
- package/engine/admin/admin.css +720 -0
- package/engine/admin/admin.html +801 -0
- package/engine/admin/admin_api.py +230 -0
- package/engine/admin/scripts/backup.sh +116 -0
- package/engine/admin/scripts/config_loader.py +180 -0
- package/engine/admin/scripts/init.sh +141 -0
- package/engine/admin/scripts/manager.py +308 -0
- package/engine/admin/scripts/restore.sh +121 -0
- package/engine/admin/scripts/server.py +41 -0
- package/engine/admin/scripts/update.sh +321 -0
- package/engine/admin/scripts/validate_json.py +62 -0
- package/engine/fonts.css +37 -0
- package/engine/index.html +190 -0
- package/engine/js/config-loader.js +370 -0
- package/engine/js/config.js +173 -0
- package/engine/js/counter.js +17 -0
- package/engine/js/effects.js +97 -0
- package/engine/js/i18n.js +68 -0
- package/engine/js/init.js +107 -0
- package/engine/js/media.js +264 -0
- package/engine/js/render.js +282 -0
- package/engine/js/router.js +133 -0
- package/engine/js/sparkle.js +123 -0
- package/engine/js/themes.js +607 -0
- package/engine/style.css +2037 -0
- package/index.js +35 -0
- package/package.json +48 -0
- package/scripts/admin.js +67 -0
- package/scripts/build.js +142 -0
- package/scripts/init.js +237 -0
- package/scripts/post-install.js +16 -0
- package/scripts/serve.js +54 -0
- package/templates/user-portfolio/.github/workflows/deploy.yml +57 -0
- package/templates/user-portfolio/config/app.json +36 -0
- package/templates/user-portfolio/config/categories.json +241 -0
- package/templates/user-portfolio/config/languages.json +15 -0
- package/templates/user-portfolio/config/media-types.json +59 -0
- package/templates/user-portfolio/data/painting.json +3 -0
- package/templates/user-portfolio/data/projects.json +3 -0
- package/templates/user-portfolio/lang/en.json +114 -0
- package/templates/user-portfolio/lang/fr.json +114 -0
|
@@ -0,0 +1,801 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="UTF-8">
|
|
6
|
+
<title>YOUR-CONTROL-PANEL V1.0</title>
|
|
7
|
+
<link rel="stylesheet" href="style.css">
|
|
8
|
+
<link rel="stylesheet" href="admin.css">
|
|
9
|
+
<link rel="stylesheet" href="fonts.css">
|
|
10
|
+
</head>
|
|
11
|
+
|
|
12
|
+
<body class="admin-page">
|
|
13
|
+
|
|
14
|
+
<div class="window">
|
|
15
|
+
<div class="title-bar">
|
|
16
|
+
<span data-i18n="admin_title">PORTFOLIO MANAGER V1.0</span>
|
|
17
|
+
<div class="title-bar-controls">
|
|
18
|
+
<button>_</button>
|
|
19
|
+
<button>[]</button>
|
|
20
|
+
<button onclick="window.location.href='/'">X</button>
|
|
21
|
+
</div>
|
|
22
|
+
</div>
|
|
23
|
+
|
|
24
|
+
<div class="tab-bar">
|
|
25
|
+
<div class="tab active" onclick="showTab('upload', this)" data-i18n="admin_tab_add">Add Media</div>
|
|
26
|
+
<div class="tab" onclick="showTab('manage', this)" data-i18n="admin_tab_manage">Manage Content</div>
|
|
27
|
+
<div class="tab" onclick="showTab('bulkedit', this)" data-i18n="admin_tab_bulk">Bulk Edit</div>
|
|
28
|
+
<div class="tab" onclick="showTab('translations', this)" data-i18n="admin_tab_translations">Translations</div>
|
|
29
|
+
<div class="tab" onclick="showTab('config', this)" data-i18n="admin_tab_config">Configuration</div>
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
<!-- UPLOAD TAB -->
|
|
33
|
+
<div id="upload" class="tab-content active">
|
|
34
|
+
<div id="bulkDropZone" class="bulk-drop-zone">
|
|
35
|
+
<p style="font-size: 14px; font-weight: bold;" data-i18n="admin_drop_title">DROP FILES HERE</p>
|
|
36
|
+
<p class="admin-muted" data-i18n="admin_drop_subtitle">or click to browse — images + audio supported</p>
|
|
37
|
+
<input type="file" id="bulkFileInput" multiple accept="image/*,audio/*" style="display:none;">
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
<div id="bulkQueue" class="bulk-queue" style="display:none;">
|
|
41
|
+
<div class="admin-flex-between" style="margin-bottom: 8px;">
|
|
42
|
+
<b id="bulkCount">0 files queued</b>
|
|
43
|
+
<div style="display: flex; gap: 5px;">
|
|
44
|
+
<button type="button" class="action" style="margin:0; font-size:11px;" onclick="clearBulkQueue()">Clear All</button>
|
|
45
|
+
<button type="button" class="action src-btn-active" style="margin:0; font-size:11px;" id="bulkUploadBtn" onclick="startBulkUpload()">UPLOAD ALL</button>
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
<div id="bulkItems" class="bulk-items"></div>
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
<!-- BULK EDIT TAB -->
|
|
53
|
+
<div id="bulkedit" class="tab-content">
|
|
54
|
+
<div class="admin-flex-between">
|
|
55
|
+
<label>Select Category:</label>
|
|
56
|
+
<select id="bulkEditCategory" onchange="loadBulkEditContent()" style="width: 150px;">
|
|
57
|
+
<option value="">-- Select Category --</option>
|
|
58
|
+
<option value="painting">Painting</option>
|
|
59
|
+
<option value="drawing">Drawing</option>
|
|
60
|
+
<option value="photography">Photography</option>
|
|
61
|
+
<option value="sculpting">Sculpting</option>
|
|
62
|
+
<option value="music">Music</option>
|
|
63
|
+
<option value="projects">Projects</option>
|
|
64
|
+
</select>
|
|
65
|
+
</div>
|
|
66
|
+
<div class="admin-hint">
|
|
67
|
+
Select items below and apply bulk edits to titles and descriptions. Changes apply to the English (EN) version.
|
|
68
|
+
</div>
|
|
69
|
+
<div id="bulkEditList" class="bulk-edit-list">
|
|
70
|
+
<p align="center" class="admin-muted">Select a category to begin bulk editing</p>
|
|
71
|
+
</div>
|
|
72
|
+
<div id="bulkEditActions" style="display: none;">
|
|
73
|
+
<div class="admin-flex-between" style="margin-top: 15px; padding-top: 10px; border-top: 2px solid var(--admin-win-border-dark);">
|
|
74
|
+
<div>
|
|
75
|
+
<span id="bulkEditSelectedCount" style="font-weight: bold;">0 selected</span>
|
|
76
|
+
</div>
|
|
77
|
+
<button class="action src-btn-active" style="margin: 0; font-size: 11px;" onclick="showBulkEditForm()">Edit Selected Items</button>
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
|
|
82
|
+
<!-- MANAGE CONTENT TAB -->
|
|
83
|
+
<div id="manage" class="tab-content">
|
|
84
|
+
<div class="admin-flex-between">
|
|
85
|
+
<label>Filter Category:</label>
|
|
86
|
+
<select id="manageCategory" onchange="loadContent()" style="width: 150px;">
|
|
87
|
+
<option value="all">All Categories</option>
|
|
88
|
+
<option value="painting">Painting</option>
|
|
89
|
+
<option value="drawing">Drawing</option>
|
|
90
|
+
<option value="photography">Photography</option>
|
|
91
|
+
<option value="sculpting">Sculpting</option>
|
|
92
|
+
<option value="music">Music</option>
|
|
93
|
+
<option value="projects">Projects</option>
|
|
94
|
+
</select>
|
|
95
|
+
<button class="action" onclick="loadContent()" style="margin: 0; font-size: 11px;">Refresh List</button>
|
|
96
|
+
</div>
|
|
97
|
+
<div id="contentList" class="admin-content-list">
|
|
98
|
+
<!-- Populated by JS -->
|
|
99
|
+
<p align="center">Loading content...</p>
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
|
|
103
|
+
<!-- TRANSLATIONS TAB -->
|
|
104
|
+
<div id="translations" class="tab-content">
|
|
105
|
+
<div class="admin-flex-between">
|
|
106
|
+
<label>Language:</label>
|
|
107
|
+
<select id="langSelect" onchange="loadTranslations()" style="width: 100px;">
|
|
108
|
+
<option value="en">English</option>
|
|
109
|
+
<option value="fr">Français</option>
|
|
110
|
+
<option value="ht">Haitian</option>
|
|
111
|
+
<option value="mx">Spanish (MX)</option>
|
|
112
|
+
<option value="qc">Québécois</option>
|
|
113
|
+
</select>
|
|
114
|
+
<button class="action" onclick="askAgentForTranslation()"
|
|
115
|
+
style="margin: 0 0 0 10px; font-size: 11px;">Ask Agent for Missing</button>
|
|
116
|
+
</div>
|
|
117
|
+
<div id="translationGrid" class="translation-grid">
|
|
118
|
+
<!-- Populated by JS -->
|
|
119
|
+
</div>
|
|
120
|
+
</div>
|
|
121
|
+
|
|
122
|
+
<!-- CONFIG TAB -->
|
|
123
|
+
<div id="config" class="tab-content">
|
|
124
|
+
<p>Cloudinary status: <span class="admin-connected">CONNECTED</span></p>
|
|
125
|
+
<p>Current Server Port: 5001</p>
|
|
126
|
+
<hr>
|
|
127
|
+
<p style="font-size: 12px;">Need high-res uploads? Make sure your .env is active.</p>
|
|
128
|
+
|
|
129
|
+
<div class="field-row" style="margin-top: 20px;">
|
|
130
|
+
<button onclick="window.location.href='config-manager.html'" style="width: 100%; height: 50px; font-weight: bold; cursor: pointer; background: linear-gradient(to bottom, #0080ff, #0060c0); color: white; border: 2px outset #fff;">
|
|
131
|
+
⚙️ ADVANCED CONFIGURATION MANAGER
|
|
132
|
+
</button>
|
|
133
|
+
<p class="admin-muted" style="margin-top: 5px;">
|
|
134
|
+
* Manage media types, content types, and custom fields through a visual interface
|
|
135
|
+
</p>
|
|
136
|
+
</div>
|
|
137
|
+
|
|
138
|
+
<hr style="margin: 20px 0;">
|
|
139
|
+
|
|
140
|
+
<div class="field-row" style="margin-top: 20px;">
|
|
141
|
+
<button onclick="syncGitHub()" style="width: 100%; height: 40px; font-weight: bold; cursor: pointer;">
|
|
142
|
+
<img src="https://github.githubassets.com/favicons/favicon.svg" width="16"
|
|
143
|
+
style="vertical-align: middle;">
|
|
144
|
+
SYNC GITHUB PROJECTS
|
|
145
|
+
</button>
|
|
146
|
+
<p class="admin-muted" style="margin-top: 5px;">
|
|
147
|
+
* This will fetch repositories for <b>mtldev514</b>.
|
|
148
|
+
Add GITHUB_TOKEN to .env to include private repos.
|
|
149
|
+
</p>
|
|
150
|
+
</div>
|
|
151
|
+
</div>
|
|
152
|
+
|
|
153
|
+
<div class="console" id="console">
|
|
154
|
+
<span data-i18n="admin_kernel_ready">[OS-KERNEL] Ready...</span>
|
|
155
|
+
</div>
|
|
156
|
+
|
|
157
|
+
<div class="status-bar">
|
|
158
|
+
<span data-i18n="admin_logged_as">Logged in as</span> <span data-i18n="admin_user">ADMIN</span> | <span data-i18n="admin_system">System: AI-Enhanced</span> | <span id="current-date"></span>
|
|
159
|
+
</div>
|
|
160
|
+
</div>
|
|
161
|
+
|
|
162
|
+
<script>
|
|
163
|
+
const API_URL = 'http://127.0.0.1:5001';
|
|
164
|
+
|
|
165
|
+
function showTab(tabId, el) {
|
|
166
|
+
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
|
167
|
+
document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
|
|
168
|
+
|
|
169
|
+
if (el) el.classList.add('active');
|
|
170
|
+
document.getElementById(tabId).classList.add('active');
|
|
171
|
+
|
|
172
|
+
if (tabId === 'translations') loadTranslations();
|
|
173
|
+
if (tabId === 'manage') loadContent();
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ── Bulk Upload ──
|
|
177
|
+
const AUDIO_EXTS = ['.mp3', '.wav', '.ogg', '.flac', '.m4a', '.aac'];
|
|
178
|
+
const IMAGE_CATS = ['painting', 'drawing', 'photography', 'sculpting'];
|
|
179
|
+
let bulkFiles = []; // { file, isAudio, title, category }
|
|
180
|
+
|
|
181
|
+
const dropZone = document.getElementById('bulkDropZone');
|
|
182
|
+
const fileInput = document.getElementById('bulkFileInput');
|
|
183
|
+
|
|
184
|
+
dropZone.addEventListener('click', () => fileInput.click());
|
|
185
|
+
dropZone.addEventListener('dragover', (e) => { e.preventDefault(); dropZone.classList.add('drag-over'); });
|
|
186
|
+
dropZone.addEventListener('dragleave', () => dropZone.classList.remove('drag-over'));
|
|
187
|
+
dropZone.addEventListener('drop', (e) => {
|
|
188
|
+
e.preventDefault();
|
|
189
|
+
dropZone.classList.remove('drag-over');
|
|
190
|
+
addFiles(e.dataTransfer.files);
|
|
191
|
+
});
|
|
192
|
+
fileInput.addEventListener('change', (e) => { addFiles(e.target.files); fileInput.value = ''; });
|
|
193
|
+
|
|
194
|
+
function addFiles(fileList) {
|
|
195
|
+
for (const file of fileList) {
|
|
196
|
+
const ext = '.' + file.name.split('.').pop().toLowerCase();
|
|
197
|
+
const isAudio = AUDIO_EXTS.includes(ext);
|
|
198
|
+
const baseName = file.name.replace(/\.[^/.]+$/, '').replace(/[-_]/g, ' ');
|
|
199
|
+
bulkFiles.push({
|
|
200
|
+
file,
|
|
201
|
+
isAudio,
|
|
202
|
+
title: baseName,
|
|
203
|
+
category: isAudio ? 'music' : 'painting'
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
renderBulkQueue();
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function bulkUpdateTitle(idx, val) { bulkFiles[idx].title = val; }
|
|
210
|
+
function bulkUpdateCat(idx, val) { bulkFiles[idx].category = val; }
|
|
211
|
+
function bulkRemove(idx) { bulkFiles.splice(idx, 1); renderBulkQueue(); }
|
|
212
|
+
|
|
213
|
+
function renderBulkQueue() {
|
|
214
|
+
const queue = document.getElementById('bulkQueue');
|
|
215
|
+
const items = document.getElementById('bulkItems');
|
|
216
|
+
const count = document.getElementById('bulkCount');
|
|
217
|
+
|
|
218
|
+
if (bulkFiles.length === 0) {
|
|
219
|
+
queue.style.display = 'none';
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
queue.style.display = 'block';
|
|
223
|
+
count.textContent = `${bulkFiles.length} file${bulkFiles.length > 1 ? 's' : ''} queued`;
|
|
224
|
+
|
|
225
|
+
let html = '';
|
|
226
|
+
bulkFiles.forEach((entry, i) => {
|
|
227
|
+
const thumbContent = entry.isAudio
|
|
228
|
+
? '<span style="font-size:24px;">♫</span>'
|
|
229
|
+
: `<img src="${URL.createObjectURL(entry.file)}">`;
|
|
230
|
+
|
|
231
|
+
let catOptions = '';
|
|
232
|
+
if (entry.isAudio) {
|
|
233
|
+
catOptions = '<option value="music">Music</option>';
|
|
234
|
+
} else {
|
|
235
|
+
catOptions = IMAGE_CATS.map(c =>
|
|
236
|
+
`<option value="${c}"${c === entry.category ? ' selected' : ''}>${c.charAt(0).toUpperCase() + c.slice(1)}</option>`
|
|
237
|
+
).join('');
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
html += `<div class="bulk-item">
|
|
241
|
+
<div class="bulk-thumb">${thumbContent}</div>
|
|
242
|
+
<div class="bulk-fields">
|
|
243
|
+
<input type="text" value="${entry.title.replace(/"/g, '"')}" placeholder="Title" onchange="bulkUpdateTitle(${i}, this.value)">
|
|
244
|
+
<select onchange="bulkUpdateCat(${i}, this.value)"${entry.isAudio ? ' disabled' : ''}>${catOptions}</select>
|
|
245
|
+
</div>
|
|
246
|
+
<span class="bulk-status" id="bulk-status-${i}"></span>
|
|
247
|
+
<button type="button" class="bulk-remove" onclick="bulkRemove(${i})">×</button>
|
|
248
|
+
</div>`;
|
|
249
|
+
});
|
|
250
|
+
items.innerHTML = html;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function clearBulkQueue() {
|
|
254
|
+
bulkFiles = [];
|
|
255
|
+
renderBulkQueue();
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
async function startBulkUpload() {
|
|
259
|
+
if (bulkFiles.length === 0) return;
|
|
260
|
+
|
|
261
|
+
const consoleBox = document.getElementById('console');
|
|
262
|
+
const uploadBtn = document.getElementById('bulkUploadBtn');
|
|
263
|
+
uploadBtn.disabled = true;
|
|
264
|
+
uploadBtn.textContent = 'UPLOADING...';
|
|
265
|
+
|
|
266
|
+
consoleBox.innerHTML += `<br>> Starting bulk upload of ${bulkFiles.length} file(s)...`;
|
|
267
|
+
|
|
268
|
+
const formData = new FormData();
|
|
269
|
+
bulkFiles.forEach((entry, i) => {
|
|
270
|
+
formData.append(`file_${i}`, entry.file);
|
|
271
|
+
formData.append(`title_${i}`, entry.title);
|
|
272
|
+
formData.append(`category_${i}`, entry.category);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
// Mark all as uploading
|
|
276
|
+
bulkFiles.forEach((_, i) => {
|
|
277
|
+
const s = document.getElementById(`bulk-status-${i}`);
|
|
278
|
+
if (s) s.textContent = '...';
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
try {
|
|
282
|
+
const res = await fetch(`${API_URL}/api/upload-bulk`, {
|
|
283
|
+
method: 'POST',
|
|
284
|
+
body: formData
|
|
285
|
+
});
|
|
286
|
+
const result = await res.json();
|
|
287
|
+
|
|
288
|
+
// Mark individual results
|
|
289
|
+
(result.results || []).forEach(r => {
|
|
290
|
+
const idx = bulkFiles.findIndex(f => f.file.name === r.file);
|
|
291
|
+
if (idx >= 0) {
|
|
292
|
+
const s = document.getElementById(`bulk-status-${idx}`);
|
|
293
|
+
if (s) { s.textContent = 'OK'; s.classList.add('con-success'); }
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
(result.errors || []).forEach(r => {
|
|
297
|
+
const idx = bulkFiles.findIndex(f => f.file.name === r.file);
|
|
298
|
+
if (idx >= 0) {
|
|
299
|
+
const s = document.getElementById(`bulk-status-${idx}`);
|
|
300
|
+
if (s) { s.textContent = 'FAIL'; s.classList.add('con-error'); }
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
consoleBox.innerHTML += `<br><span class="con-success">> Bulk upload complete: ${result.uploaded} uploaded, ${result.failed} failed.</span>`;
|
|
305
|
+
|
|
306
|
+
if (result.uploaded > 0) {
|
|
307
|
+
setTimeout(() => {
|
|
308
|
+
bulkFiles = bulkFiles.filter((_, i) => {
|
|
309
|
+
const s = document.getElementById(`bulk-status-${i}`);
|
|
310
|
+
return s && !s.classList.contains('con-success');
|
|
311
|
+
});
|
|
312
|
+
renderBulkQueue();
|
|
313
|
+
}, 2000);
|
|
314
|
+
}
|
|
315
|
+
} catch (err) {
|
|
316
|
+
consoleBox.innerHTML += `<br><span class="con-error">> NETWORK ERROR: ${err.message}</span>`;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
uploadBtn.disabled = false;
|
|
320
|
+
uploadBtn.textContent = 'UPLOAD ALL';
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
async function loadTranslations() {
|
|
324
|
+
const lang = document.getElementById('langSelect').value;
|
|
325
|
+
const grid = document.getElementById('translationGrid');
|
|
326
|
+
grid.innerHTML = 'Loading...';
|
|
327
|
+
|
|
328
|
+
try {
|
|
329
|
+
const res = await fetch(`${API_URL}/api/translations`);
|
|
330
|
+
const all = await res.json();
|
|
331
|
+
const trans = all[lang];
|
|
332
|
+
|
|
333
|
+
let html = '';
|
|
334
|
+
Object.keys(trans).forEach(key => {
|
|
335
|
+
const safeVal = (trans[key] || '').replace(/"/g, '"');
|
|
336
|
+
html += `<div class="trans-item">
|
|
337
|
+
<label style="font-size: 10px;">${key}</label>
|
|
338
|
+
<input type="text" value="${safeVal}" onblur="updateTrans('${lang}', '${key}', this.value)">
|
|
339
|
+
</div>`;
|
|
340
|
+
});
|
|
341
|
+
grid.innerHTML = html;
|
|
342
|
+
} catch (err) {
|
|
343
|
+
grid.innerHTML = 'Error loading translations.';
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
async function updateTrans(lang, key, value) {
|
|
348
|
+
await fetch(`${API_URL}/api/translations/update`, {
|
|
349
|
+
method: 'POST',
|
|
350
|
+
headers: { 'Content-Type': 'application/json' },
|
|
351
|
+
body: JSON.stringify({ lang, key, value })
|
|
352
|
+
});
|
|
353
|
+
document.getElementById('console').innerHTML += `<br>> Translation "${key}" updated for [${lang}].`;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
async function askAgentForTranslation() {
|
|
357
|
+
const consoleBox = document.getElementById('console');
|
|
358
|
+
consoleBox.innerHTML += `<br>> Scanning for missing translation keys...`;
|
|
359
|
+
|
|
360
|
+
try {
|
|
361
|
+
const res = await fetch(`${API_URL}/api/translations/missing`);
|
|
362
|
+
const missing = await res.json();
|
|
363
|
+
|
|
364
|
+
if (Object.keys(missing).length === 0) {
|
|
365
|
+
consoleBox.innerHTML += `<br><span class="con-info">> System status: All languages synchronized.</span>`;
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
let report = "Missing keys found:\\n";
|
|
370
|
+
for (const lang in missing) {
|
|
371
|
+
report += `[${lang.toUpperCase()}]: ${missing[lang].join(', ')}\\n`;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
consoleBox.innerHTML += `<br><span class="con-warn">> AGENT REQUIRED: ${Object.keys(missing).length} languages have missing keys.</span>`;
|
|
375
|
+
consoleBox.innerHTML += `<br><span class="con-hi">> TIP: Ask Antigravity in the chat to 'Translate the missing keys for ${Object.keys(missing).join(', ')}' and they will be fixed instantly!</span>`;
|
|
376
|
+
} catch (err) {
|
|
377
|
+
consoleBox.innerHTML += `<br><span class="con-error">> Error checking missing keys.</span>`;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const IMAGE_CATEGORIES = ['painting', 'drawing', 'photography', 'sculpting'];
|
|
382
|
+
let loadedContent = {}; // cached for pile picker
|
|
383
|
+
|
|
384
|
+
async function loadContent() {
|
|
385
|
+
const cat = document.getElementById('manageCategory').value;
|
|
386
|
+
const list = document.getElementById('contentList');
|
|
387
|
+
list.innerHTML = '<p align="center">Loading content...</p>';
|
|
388
|
+
|
|
389
|
+
try {
|
|
390
|
+
const res = await fetch(`${API_URL}/api/content`);
|
|
391
|
+
const all = await res.json();
|
|
392
|
+
loadedContent = all;
|
|
393
|
+
|
|
394
|
+
let html = '';
|
|
395
|
+
Object.keys(all).forEach(category => {
|
|
396
|
+
const items = all[category];
|
|
397
|
+
if (cat !== 'all' && cat !== category) return;
|
|
398
|
+
if (!items || items.length === 0) return;
|
|
399
|
+
|
|
400
|
+
html += `<h4 class="content-header">${category.toUpperCase()}</h4>`;
|
|
401
|
+
items.forEach(item => {
|
|
402
|
+
const titleStr = (typeof item.title === 'object' && item.title !== null) ? (item.title.en || '') : (item.title || '');
|
|
403
|
+
const id = item.id || titleStr;
|
|
404
|
+
const safeId = id.replace(/"/g, '"');
|
|
405
|
+
const galleryCount = (item.gallery && item.gallery.length) ? item.gallery.length + 1 : 0;
|
|
406
|
+
const pileBadge = galleryCount > 1 ? ` <span class="admin-muted">[${galleryCount} imgs]</span>` : '';
|
|
407
|
+
const isImage = IMAGE_CATEGORIES.includes(category);
|
|
408
|
+
const isPile = galleryCount > 1;
|
|
409
|
+
const pileButtonText = isPile ? 'Merge' : 'Pile';
|
|
410
|
+
const pileButtonTitle = isPile ? 'Merge this pile into another pile' : 'Move this item into another pile';
|
|
411
|
+
html += `<div class="content-row">
|
|
412
|
+
<div style="flex:1"><b>${titleStr}</b>${pileBadge} <span class="admin-muted">(${item.date || 'N/A'})</span></div>
|
|
413
|
+
<div>
|
|
414
|
+
${isImage ? `<button type="button" class="pile-btn" style="font-size:10px" data-category="${category}" data-id="${safeId}" title="${pileButtonTitle}">${pileButtonText}</button>` : ''}
|
|
415
|
+
<button type="button" class="edit-btn" style="font-size:10px" data-category="${category}" data-id="${safeId}">Edit</button>
|
|
416
|
+
<button type="button" class="delete-btn con-error" style="font-size:10px;margin-left:5px" data-category="${category}" data-id="${safeId}">Delete</button>
|
|
417
|
+
<span class="delete-status" style="font-size:9px;margin-left:5px"></span>
|
|
418
|
+
</div>
|
|
419
|
+
</div>`;
|
|
420
|
+
});
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
list.innerHTML = html || '<p align="center">No content found for this category.</p>';
|
|
424
|
+
} catch (err) {
|
|
425
|
+
list.innerHTML = '<p align="center" class="con-error">Error loading content list.</p>';
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// Delegated click handler for content list
|
|
430
|
+
document.getElementById('contentList').addEventListener('click', (e) => {
|
|
431
|
+
const editBtn = e.target.closest('.edit-btn');
|
|
432
|
+
if (editBtn) {
|
|
433
|
+
e.stopPropagation();
|
|
434
|
+
window.location.href = `edit.html?category=${encodeURIComponent(editBtn.dataset.category)}&id=${encodeURIComponent(editBtn.dataset.id)}`;
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
const pileBtn = e.target.closest('.pile-btn');
|
|
439
|
+
if (pileBtn) {
|
|
440
|
+
e.stopPropagation();
|
|
441
|
+
openPilePicker(pileBtn);
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const deleteBtn = e.target.closest('.delete-btn');
|
|
446
|
+
if (deleteBtn) {
|
|
447
|
+
e.stopPropagation();
|
|
448
|
+
const statusSpan = deleteBtn.parentElement.querySelector('.delete-status');
|
|
449
|
+
if (deleteBtn.dataset.armed === 'true') {
|
|
450
|
+
deleteItem(deleteBtn.dataset.category, deleteBtn.dataset.id, statusSpan);
|
|
451
|
+
} else {
|
|
452
|
+
deleteBtn.dataset.armed = 'true';
|
|
453
|
+
deleteBtn.innerText = 'Sure?';
|
|
454
|
+
deleteBtn.style.fontWeight = 'bold';
|
|
455
|
+
statusSpan.innerHTML = '<span class="con-warn">click again to confirm</span>';
|
|
456
|
+
setTimeout(() => {
|
|
457
|
+
deleteBtn.dataset.armed = 'false';
|
|
458
|
+
deleteBtn.innerText = 'Delete';
|
|
459
|
+
deleteBtn.style.fontWeight = 'normal';
|
|
460
|
+
statusSpan.innerHTML = '';
|
|
461
|
+
}, 3000);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
function openPilePicker(btn) {
|
|
467
|
+
// Close any existing picker
|
|
468
|
+
document.querySelectorAll('.pile-picker').forEach(p => p.remove());
|
|
469
|
+
|
|
470
|
+
const category = btn.dataset.category;
|
|
471
|
+
const sourceId = btn.dataset.id;
|
|
472
|
+
|
|
473
|
+
// Get source item to check if it's a pile
|
|
474
|
+
const sourceItem = (loadedContent[category] || []).find(item => {
|
|
475
|
+
const id = item.id || (typeof item.title === 'string' ? item.title : (item.title && item.title.en) || '');
|
|
476
|
+
return id === sourceId;
|
|
477
|
+
});
|
|
478
|
+
const sourceGalleryCount = (sourceItem && sourceItem.gallery && sourceItem.gallery.length) ? sourceItem.gallery.length + 1 : 1;
|
|
479
|
+
const isSourcePile = sourceGalleryCount > 1;
|
|
480
|
+
|
|
481
|
+
const items = (loadedContent[category] || []).filter(item => {
|
|
482
|
+
const id = item.id || (typeof item.title === 'string' ? item.title : (item.title && item.title.en) || '');
|
|
483
|
+
return id !== sourceId;
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
if (items.length === 0) {
|
|
487
|
+
const consoleBox = document.getElementById('console');
|
|
488
|
+
consoleBox.innerHTML += `<br><span class="con-warn">> No other items in ${category} to ${isSourcePile ? 'merge' : 'pile'} into.</span>`;
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
const picker = document.createElement('div');
|
|
493
|
+
picker.className = 'pile-picker';
|
|
494
|
+
|
|
495
|
+
let html = `<div class="pile-picker-title">${isSourcePile ? 'Merge into:' : 'Move into:'}</div>`;
|
|
496
|
+
if (isSourcePile) {
|
|
497
|
+
html += `<div style="padding: 4px 8px; font-size: 9px; background: #fffff0; border-bottom: 1px solid var(--admin-win-border-dark);">This will merge all ${sourceGalleryCount} images into the selected pile</div>`;
|
|
498
|
+
}
|
|
499
|
+
items.forEach(item => {
|
|
500
|
+
const titleStr = (typeof item.title === 'object' && item.title !== null) ? (item.title.en || '') : (item.title || '');
|
|
501
|
+
const id = item.id || titleStr;
|
|
502
|
+
const safeId = id.replace(/"/g, '"');
|
|
503
|
+
const galleryCount = (item.gallery && item.gallery.length) ? item.gallery.length + 1 : 1;
|
|
504
|
+
html += `<div class="pile-picker-item" data-target-id="${safeId}">
|
|
505
|
+
${titleStr} <span class="admin-muted">(${galleryCount} img${galleryCount > 1 ? 's' : ''})</span>
|
|
506
|
+
</div>`;
|
|
507
|
+
});
|
|
508
|
+
picker.innerHTML = html;
|
|
509
|
+
|
|
510
|
+
// Position it near the button
|
|
511
|
+
btn.parentElement.style.position = 'relative';
|
|
512
|
+
btn.parentElement.appendChild(picker);
|
|
513
|
+
|
|
514
|
+
// Handle pick
|
|
515
|
+
picker.addEventListener('click', async (ev) => {
|
|
516
|
+
const pickItem = ev.target.closest('.pile-picker-item');
|
|
517
|
+
if (!pickItem) return;
|
|
518
|
+
|
|
519
|
+
const targetId = pickItem.dataset.targetId;
|
|
520
|
+
picker.innerHTML = '<div class="pile-picker-title">Moving...</div>';
|
|
521
|
+
|
|
522
|
+
try {
|
|
523
|
+
const res = await fetch(`${API_URL}/api/content/move-to-pile`, {
|
|
524
|
+
method: 'POST',
|
|
525
|
+
headers: { 'Content-Type': 'application/json' },
|
|
526
|
+
body: JSON.stringify({ category, sourceId, targetId })
|
|
527
|
+
});
|
|
528
|
+
const result = await res.json();
|
|
529
|
+
if (result.success) {
|
|
530
|
+
document.getElementById('console').innerHTML += `<br><span class="con-success">> Moved into pile (${result.targetGalleryCount + 1} images total).</span>`;
|
|
531
|
+
picker.remove();
|
|
532
|
+
loadContent();
|
|
533
|
+
} else {
|
|
534
|
+
document.getElementById('console').innerHTML += `<br><span class="con-error">> Pile error: ${result.error}</span>`;
|
|
535
|
+
picker.remove();
|
|
536
|
+
}
|
|
537
|
+
} catch (err) {
|
|
538
|
+
document.getElementById('console').innerHTML += `<br><span class="con-error">> Network error: ${err.message}</span>`;
|
|
539
|
+
picker.remove();
|
|
540
|
+
}
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
// Close on outside click
|
|
544
|
+
setTimeout(() => {
|
|
545
|
+
document.addEventListener('click', function closePicker(ev) {
|
|
546
|
+
if (!picker.contains(ev.target) && ev.target !== btn) {
|
|
547
|
+
picker.remove();
|
|
548
|
+
document.removeEventListener('click', closePicker);
|
|
549
|
+
}
|
|
550
|
+
});
|
|
551
|
+
}, 0);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
async function deleteItem(category, id, statusSpan) {
|
|
555
|
+
statusSpan.innerHTML = '<span class="con-hi">deleting...</span>';
|
|
556
|
+
try {
|
|
557
|
+
const res = await fetch(`${API_URL}/api/content/delete`, {
|
|
558
|
+
method: 'POST',
|
|
559
|
+
headers: { 'Content-Type': 'application/json' },
|
|
560
|
+
body: JSON.stringify({ category, id })
|
|
561
|
+
});
|
|
562
|
+
const result = await res.json();
|
|
563
|
+
if (result.success) {
|
|
564
|
+
document.getElementById('console').innerHTML += `<br><span class="con-warn">> Item "${id}" deleted from ${category}.</span>`;
|
|
565
|
+
statusSpan.innerHTML = '<span class="con-success">deleted!</span>';
|
|
566
|
+
setTimeout(() => loadContent(), 500);
|
|
567
|
+
} else {
|
|
568
|
+
statusSpan.innerHTML = `<span class="con-error">error: ${result.error}</span>`;
|
|
569
|
+
}
|
|
570
|
+
} catch (err) {
|
|
571
|
+
statusSpan.innerHTML = '<span class="con-error">network error</span>';
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
async function syncGitHub() {
|
|
576
|
+
const consoleBox = document.getElementById('console');
|
|
577
|
+
consoleBox.innerHTML += `<br>> Initiating GitHub synchronization...`;
|
|
578
|
+
|
|
579
|
+
try {
|
|
580
|
+
const res = await fetch(`${API_URL}/api/github/sync`, { method: 'POST' });
|
|
581
|
+
const result = await res.json();
|
|
582
|
+
|
|
583
|
+
if (result.success) {
|
|
584
|
+
consoleBox.innerHTML += `<br><span class="con-success">> GitHub Sync Successful: ${result.count} repositories fetched.</span>`;
|
|
585
|
+
consoleBox.innerHTML += `<br><span class="con-info">> ${result.count} projects updated in data/projects.json.</span>`;
|
|
586
|
+
} else {
|
|
587
|
+
consoleBox.innerHTML += `<br><span class="con-error">> GitHub Sync Failed: ${result.error}</span>`;
|
|
588
|
+
}
|
|
589
|
+
} catch (err) {
|
|
590
|
+
consoleBox.innerHTML += `<br><span class="con-error">> Network Error during GitHub Sync.</span>`;
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// Auto-switch to Manage tab if returning from edit page
|
|
595
|
+
if (window.location.hash === '#manage') {
|
|
596
|
+
const manageTab = document.querySelectorAll('.tab')[1];
|
|
597
|
+
if (manageTab) showTab('manage', manageTab);
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// ── Bulk Edit ──
|
|
601
|
+
let bulkEditItems = [];
|
|
602
|
+
let bulkEditSelected = [];
|
|
603
|
+
|
|
604
|
+
async function loadBulkEditContent() {
|
|
605
|
+
const category = document.getElementById('bulkEditCategory').value;
|
|
606
|
+
const list = document.getElementById('bulkEditList');
|
|
607
|
+
const actions = document.getElementById('bulkEditActions');
|
|
608
|
+
|
|
609
|
+
if (!category) {
|
|
610
|
+
list.innerHTML = '<p align="center" class="admin-muted">Select a category to begin bulk editing</p>';
|
|
611
|
+
actions.style.display = 'none';
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
list.innerHTML = '<p align="center">Loading...</p>';
|
|
616
|
+
|
|
617
|
+
try {
|
|
618
|
+
const res = await fetch(`${API_URL}/api/content`);
|
|
619
|
+
const all = await res.json();
|
|
620
|
+
bulkEditItems = all[category] || [];
|
|
621
|
+
bulkEditSelected = [];
|
|
622
|
+
|
|
623
|
+
if (bulkEditItems.length === 0) {
|
|
624
|
+
list.innerHTML = '<p align="center" class="admin-muted">No items in this category</p>';
|
|
625
|
+
actions.style.display = 'none';
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
renderBulkEditList();
|
|
630
|
+
actions.style.display = 'block';
|
|
631
|
+
} catch (err) {
|
|
632
|
+
list.innerHTML = '<p align="center" class="con-error">Error loading content</p>';
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
function renderBulkEditList() {
|
|
637
|
+
const list = document.getElementById('bulkEditList');
|
|
638
|
+
let html = '<div style="background: white; border: 2px inset var(--admin-win-border-dark); padding: 10px; max-height: 300px; overflow-y: auto;">';
|
|
639
|
+
|
|
640
|
+
bulkEditItems.forEach((item, index) => {
|
|
641
|
+
const titleStr = (typeof item.title === 'object' && item.title !== null) ? (item.title.en || '') : (item.title || '');
|
|
642
|
+
const descStr = (typeof item.description === 'object' && item.description !== null) ? (item.description.en || '') : (item.description || '');
|
|
643
|
+
const isSelected = bulkEditSelected.includes(index);
|
|
644
|
+
const checkedAttr = isSelected ? ' checked' : '';
|
|
645
|
+
|
|
646
|
+
html += `<div class="bulk-edit-row">
|
|
647
|
+
<input type="checkbox" id="bulk-check-${index}" ${checkedAttr} onchange="toggleBulkEditItem(${index})">
|
|
648
|
+
<label for="bulk-check-${index}" style="flex: 1; cursor: pointer;">
|
|
649
|
+
<b>${titleStr}</b>
|
|
650
|
+
${descStr ? `<span class="admin-muted" style="display: block; font-size: 10px;">${descStr.substring(0, 60)}${descStr.length > 60 ? '...' : ''}</span>` : ''}
|
|
651
|
+
</label>
|
|
652
|
+
</div>`;
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
html += '</div>';
|
|
656
|
+
list.innerHTML = html;
|
|
657
|
+
updateBulkEditCount();
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
function toggleBulkEditItem(index) {
|
|
661
|
+
const idx = bulkEditSelected.indexOf(index);
|
|
662
|
+
if (idx >= 0) {
|
|
663
|
+
bulkEditSelected.splice(idx, 1);
|
|
664
|
+
} else {
|
|
665
|
+
bulkEditSelected.push(index);
|
|
666
|
+
}
|
|
667
|
+
updateBulkEditCount();
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
function updateBulkEditCount() {
|
|
671
|
+
document.getElementById('bulkEditSelectedCount').textContent = `${bulkEditSelected.length} selected`;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
function showBulkEditForm() {
|
|
675
|
+
if (bulkEditSelected.length === 0) {
|
|
676
|
+
alert('Please select at least one item to edit');
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
const category = document.getElementById('bulkEditCategory').value;
|
|
681
|
+
const selectedItems = bulkEditSelected.map(i => bulkEditItems[i]);
|
|
682
|
+
|
|
683
|
+
// Create modal
|
|
684
|
+
const modal = document.createElement('div');
|
|
685
|
+
modal.className = 'bulk-edit-modal';
|
|
686
|
+
modal.innerHTML = `
|
|
687
|
+
<div class="bulk-edit-modal-content window" style="width: 600px;">
|
|
688
|
+
<div class="title-bar">
|
|
689
|
+
<span>BULK EDIT: ${bulkEditSelected.length} items</span>
|
|
690
|
+
<div class="title-bar-controls">
|
|
691
|
+
<button onclick="closeBulkEditModal()">X</button>
|
|
692
|
+
</div>
|
|
693
|
+
</div>
|
|
694
|
+
<div style="padding: 20px;">
|
|
695
|
+
<div class="admin-hint">
|
|
696
|
+
Leave fields blank to keep existing values. Changes apply to English (EN) version only.
|
|
697
|
+
</div>
|
|
698
|
+
<div class="form-group">
|
|
699
|
+
<label>
|
|
700
|
+
<input type="checkbox" id="bulkUpdateTitle"> Update Title:
|
|
701
|
+
</label>
|
|
702
|
+
<input type="text" id="bulkTitleValue" placeholder="New title for all selected items">
|
|
703
|
+
</div>
|
|
704
|
+
<div class="form-group">
|
|
705
|
+
<label>
|
|
706
|
+
<input type="checkbox" id="bulkUpdateDesc"> Update Description:
|
|
707
|
+
</label>
|
|
708
|
+
<textarea id="bulkDescValue" placeholder="New description for all selected items"></textarea>
|
|
709
|
+
</div>
|
|
710
|
+
<div class="button-bar">
|
|
711
|
+
<button type="button" onclick="closeBulkEditModal()">Cancel</button>
|
|
712
|
+
<button type="button" class="save-btn" onclick="applyBulkEdit()">Apply Changes</button>
|
|
713
|
+
</div>
|
|
714
|
+
</div>
|
|
715
|
+
<div class="console short" id="bulkEditConsole">[BULK-EDIT] Ready...</div>
|
|
716
|
+
</div>
|
|
717
|
+
`;
|
|
718
|
+
|
|
719
|
+
document.body.appendChild(modal);
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
window.closeBulkEditModal = function() {
|
|
723
|
+
const modal = document.querySelector('.bulk-edit-modal');
|
|
724
|
+
if (modal) modal.remove();
|
|
725
|
+
};
|
|
726
|
+
|
|
727
|
+
window.applyBulkEdit = async function() {
|
|
728
|
+
const updateTitle = document.getElementById('bulkUpdateTitle').checked;
|
|
729
|
+
const updateDesc = document.getElementById('bulkUpdateDesc').checked;
|
|
730
|
+
const titleValue = document.getElementById('bulkTitleValue').value;
|
|
731
|
+
const descValue = document.getElementById('bulkDescValue').value;
|
|
732
|
+
const category = document.getElementById('bulkEditCategory').value;
|
|
733
|
+
const consoleBox = document.getElementById('bulkEditConsole');
|
|
734
|
+
|
|
735
|
+
if (!updateTitle && !updateDesc) {
|
|
736
|
+
consoleBox.innerHTML += '<br><span class="con-warn">> Please select at least one field to update</span>';
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
consoleBox.innerHTML += `<br>> Applying bulk edit to ${bulkEditSelected.length} items...`;
|
|
741
|
+
|
|
742
|
+
let successCount = 0;
|
|
743
|
+
let errorCount = 0;
|
|
744
|
+
|
|
745
|
+
for (const itemIndex of bulkEditSelected) {
|
|
746
|
+
const item = bulkEditItems[itemIndex];
|
|
747
|
+
const itemId = item.id || (typeof item.title === 'string' ? item.title : (item.title && item.title.en) || '');
|
|
748
|
+
|
|
749
|
+
const updates = {};
|
|
750
|
+
|
|
751
|
+
if (updateTitle && titleValue) {
|
|
752
|
+
const original = item.title;
|
|
753
|
+
if (typeof original === 'object' && original !== null) {
|
|
754
|
+
updates.title = { ...original, en: titleValue };
|
|
755
|
+
} else {
|
|
756
|
+
updates.title = { en: titleValue, fr: titleValue, mx: titleValue, ht: titleValue };
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
if (updateDesc && descValue) {
|
|
761
|
+
const original = item.description;
|
|
762
|
+
if (typeof original === 'object' && original !== null) {
|
|
763
|
+
updates.description = { ...original, en: descValue };
|
|
764
|
+
} else {
|
|
765
|
+
updates.description = { en: descValue, fr: descValue, mx: descValue, ht: descValue };
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
try {
|
|
770
|
+
const res = await fetch(`${API_URL}/api/content/update`, {
|
|
771
|
+
method: 'POST',
|
|
772
|
+
headers: { 'Content-Type': 'application/json' },
|
|
773
|
+
body: JSON.stringify({ category, id: itemId, updates })
|
|
774
|
+
});
|
|
775
|
+
|
|
776
|
+
const result = await res.json();
|
|
777
|
+
if (result.success) {
|
|
778
|
+
successCount++;
|
|
779
|
+
} else {
|
|
780
|
+
errorCount++;
|
|
781
|
+
}
|
|
782
|
+
} catch (err) {
|
|
783
|
+
errorCount++;
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
consoleBox.innerHTML += `<br><span class="con-success">> Bulk edit complete: ${successCount} updated, ${errorCount} failed</span>`;
|
|
788
|
+
|
|
789
|
+
if (successCount > 0) {
|
|
790
|
+
document.getElementById('console').innerHTML += `<br><span class="con-success">> Bulk edited ${successCount} items in ${category}</span>`;
|
|
791
|
+
setTimeout(() => {
|
|
792
|
+
closeBulkEditModal();
|
|
793
|
+
loadBulkEditContent();
|
|
794
|
+
}, 1500);
|
|
795
|
+
}
|
|
796
|
+
};
|
|
797
|
+
|
|
798
|
+
</script>
|
|
799
|
+
</body>
|
|
800
|
+
|
|
801
|
+
</html>
|