aboutsummaryrefslogtreecommitdiff
path: root/internal/admin/static/editor.js
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-06-29 22:31:11 -0400
committerhistoria <[not public]>2026-06-29 22:31:11 -0400
commit069d3c1c81d71042706372df153254487a765dfc (patch)
treed83a4a3954d2b8304f49d83e365f4c2b075b91eb /internal/admin/static/editor.js
parent6c5271fb085b4571a10f106391974c249cd84317 (diff)
downloadthehouseoficarus-069d3c1c81d71042706372df153254487a765dfc.tar.gz
feat: wip web admin for mapping and crud
Diffstat (limited to 'internal/admin/static/editor.js')
-rw-r--r--internal/admin/static/editor.js174
1 files changed, 174 insertions, 0 deletions
diff --git a/internal/admin/static/editor.js b/internal/admin/static/editor.js
new file mode 100644
index 0000000..d9ec36f
--- /dev/null
+++ b/internal/admin/static/editor.js
@@ -0,0 +1,174 @@
+var editorType = '';
+var editorFields = [];
+var currentID = null;
+var allIDs = [];
+
+function initEditor(type, fields) {
+ editorType = type;
+ editorFields = fields;
+ loadList();
+ var hash = window.location.hash.substring(1);
+ if (hash) { loadItem(hash); }
+}
+
+function loadList() {
+ API.get('/api/' + editorType).then(function(data) {
+ allIDs = data.ids || data || [];
+ renderList('');
+ }).catch(function(e) {
+ notify('Failed to load list: ' + e.message, 'error');
+ });
+}
+
+function renderList(filter) {
+ var el = $('#listEntries');
+ var f = filter.toLowerCase();
+ var filtered = allIDs.filter(function(id) { return !f || String(id).toLowerCase().indexOf(f) >= 0; });
+ el.innerHTML = filtered.map(function(id) {
+ return '<div class="item' + (id === currentID ? ' active' : '') + '" onclick="loadItem(\'' + esc(id) + '\')">' + esc(id) + '</div>';
+ }).join('');
+}
+
+function filterList(val) {
+ renderList(val);
+}
+
+function loadItem(id) {
+ currentID = id;
+ renderList('');
+ window.location.hash = id;
+ API.get('/api/' + editorType + '/' + encodeURIComponent(id)).then(function(data) {
+ renderEditor(id, data);
+ }).catch(function(e) {
+ $('#editorMain').innerHTML = '<p style="color:var(--danger)">Failed to load: ' + esc(e.message) + '</p>';
+ });
+}
+
+function renderEditor(id, data) {
+ var obj = data;
+ var html = '<h2>' + esc(editorType.charAt(0).toUpperCase() + editorType.slice(1)) + ': ' + esc(id) + '</h2>';
+
+ html += '<div class="tabs"><button class="tab active" onclick="switchTab(event,\'fields\')">Fields</button>';
+ html += '<button class="tab" onclick="switchTab(event,\'yaml\')">Raw YAML</button></div>';
+
+ html += '<div class="tab-content" id="tabFields">';
+ html += '<div class="form-grid">';
+ editorFields.forEach(function(f) {
+ var val = getNested(obj, f.key);
+ val = val === undefined || val === null ? '' : val;
+ var cls = f.type === 'textarea' ? 'form-group full' : 'form-group';
+ if (f.type === 'textarea') {
+ var displayVal;
+ if (f.encode) {
+ displayVal = f.encode(val);
+ } else if (typeof val === 'object') {
+ displayVal = JSON.stringify(val, null, 2);
+ } else {
+ displayVal = String(val);
+ }
+ html += '<div class="'+cls+'"><label>' + esc(f.label) + '</label><textarea id="f_' + esc(f.key) + '" rows="4">' + esc(displayVal) + '</textarea></div>';
+ } else if (f.type === 'checkbox') {
+ html += '<div class="'+cls+'"><label><input type="checkbox" id="f_' + esc(f.key) + '"' + (val ? ' checked' : '') + '> ' + esc(f.label) + '</label></div>';
+ } else if (f.type === 'number') {
+ html += '<div class="'+cls+'"><label>' + esc(f.label) + '</label><input type="number" id="f_' + esc(f.key) + '" value="' + escAttr(String(val)) + '"></div>';
+ } else if (f.type === 'color') {
+ html += '<div class="'+cls+' color-field"><label>' + esc(f.label) + '</label><span class="color-swatch" style="background:#' + escAttr(String(val) || '808080') + '"></span><input id="f_' + esc(f.key) + '" value="' + escAttr(String(val) || '') + '"></div>';
+ } else {
+ html += '<div class="'+cls+'"><label>' + esc(f.label) + '</label><input id="f_' + esc(f.key) + '" value="' + escAttr(String(val)) + '"></div>';
+ }
+ });
+ html += '</div>';
+ html += '</div>';
+
+ html += '<div class="tab-content" id="tabYaml" style="display:none">';
+ html += '<pre>' + esc(obj._raw || JSON.stringify(obj, null, 2)) + '</pre>';
+ html += '</div>';
+
+ html += '<div class="btn-row">';
+ html += '<button class="btn btn-primary" onclick="saveItem(\'' + esc(id) + '\')">Save</button>';
+ html += '<button class="btn btn-danger" onclick="deleteItem(\'' + esc(id) + '\')">Delete</button>';
+ html += '</div>';
+
+ $('#editorMain').innerHTML = html;
+ setTimeout(function() {
+ document.querySelectorAll('.color-field').forEach(function(el) { bindColorField(el); });
+ }, 50);
+}
+
+function saveItem(id) {
+ var data = {};
+ editorFields.forEach(function(f) {
+ var el = $('#f_' + f.key);
+ if (!el) return;
+ var val;
+ if (f.type === 'checkbox') {
+ val = el.checked;
+ } else if (f.type === 'number') {
+ val = parseFloat(el.value) || 0;
+ } else if (f.type === 'textarea') {
+ val = el.value;
+ } else {
+ val = el.value;
+ }
+ if (f.parser) {
+ val = f.parser(val);
+ }
+ setNested(data, f.key, val);
+ });
+ data.id = id;
+
+ API.put('/api/' + editorType + '/' + encodeURIComponent(id), data).then(function() {
+ notify(editorType + ' ' + id + ' saved', 'success');
+ updateUndoBar();
+ }).catch(function(e) { notify('Save failed: ' + e.message, 'error'); });
+}
+
+function deleteItem(id) {
+ if (!confirm('Delete ' + editorType + ' "' + id + '"?')) return;
+ API.del('/api/' + editorType + '/' + encodeURIComponent(id)).then(function() {
+ notify(editorType + ' ' + id + ' deleted', 'success');
+ updateUndoBar();
+ currentID = null;
+ $('#editorMain').innerHTML = '<p style="color:#888;text-align:center;margin-top:60px">Deleted</p>';
+ loadList();
+ }).catch(function(e) { notify('Delete failed: ' + e.message, 'error'); });
+}
+
+function createNew() {
+ var name = prompt(editorType + ' ID:');
+ if (!name) return;
+ API.post('/api/' + editorType, {id: name}).then(function() {
+ notify(editorType + ' ' + name + ' created', 'success');
+ updateUndoBar();
+ loadList();
+ loadItem(name);
+ }).catch(function(e) { notify('Create failed: ' + e.message, 'error'); });
+}
+
+function switchTab(e, tab) {
+ e.target.parentElement.querySelectorAll('.tab').forEach(function(t) { t.classList.remove('active'); });
+ e.target.classList.add('active');
+ var tabs = $('#editorMain').querySelectorAll('.tab-content');
+ tabs.forEach(function(t) { t.style.display = 'none'; });
+ var target = document.getElementById('tab' + tab.charAt(0).toUpperCase() + tab.slice(1));
+ if (target) target.style.display = 'block';
+}
+
+function getNested(obj, path) {
+ return path.split('.').reduce(function(o, k) { return o && o[k] !== undefined ? o[k] : undefined; }, obj);
+}
+
+function setNested(obj, path, val) {
+ var keys = path.split('.');
+ var last = keys.pop();
+ var target = keys.reduce(function(o, k) {
+ if (!o[k]) o[k] = {};
+ return o[k];
+ }, obj);
+ target[last] = val;
+}
+
+function esc(s) { return String(s || '').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;'); }
+function escAttr(s) { return String(s || '').replace(/&/g,'&amp;').replace(/"/g,'&quot;'); }
+
+window.refreshPage = function() { loadList(); if (currentID) loadItem(currentID); };