Files
broccolini-bot/settings-site/public/js/fields.js

138 lines
4.3 KiB
JavaScript

(function () {
'use strict';
let savedConfig = {};
let pendingChanges = {};
function setSavedConfig(config) {
savedConfig = config;
}
function populateFields(config) {
document.querySelectorAll('[data-key]').forEach(el => {
const key = el.dataset.key;
const value = config[key] || '';
if (el.type === 'checkbox') {
el.checked = value === 'true' || value === true;
} else if (el.type === 'color') {
const num = parseInt(value) || 0;
el.value = '#' + num.toString(16).padStart(6, '0');
} else {
el.value = value;
}
el.addEventListener('change', () => handleFieldChange(el, key));
el.addEventListener('input', () => {
if (el.type === 'text' || el.type === 'number' || el.type === 'password' || el.tagName === 'TEXTAREA') {
handleFieldChange(el, key);
}
});
});
}
function handleFieldChange(el, key) {
let value;
if (el.type === 'checkbox') {
value = el.checked ? 'true' : 'false';
} else if (el.type === 'color') {
value = '0x' + el.value.slice(1).toUpperCase();
} else {
value = el.value;
}
markChanged(key, value);
el.classList.toggle('changed', key in pendingChanges);
}
function initSmartSelects(config) {
document.querySelectorAll('[data-smart]').forEach(el => {
const key = el.dataset.key;
const type = el.dataset.smart;
const value = config[key] || '';
if (type === 'channel') DiscordFields.renderChannelSelect(el, value);
else if (type === 'category') DiscordFields.renderCategorySelect(el, value);
else if (type === 'role') DiscordFields.renderRoleSelect(el, value);
else if (type === 'member') DiscordFields.renderMemberSelect(el, value);
else if (type === 'multi-member') DiscordFields.renderMultiMemberSelect(el, value);
});
}
function markChanged(key, value) {
if (String(value) === String(savedConfig[key] || '')) {
delete pendingChanges[key];
} else {
pendingChanges[key] = value;
}
updateSaveBar();
}
function isChanged(key) {
return key in pendingChanges;
}
function updateSaveBar() {
const bar = document.getElementById('save-bar');
const count = Object.keys(pendingChanges).length;
bar.classList.toggle('visible', count > 0);
document.getElementById('change-count').textContent =
`${count} unsaved change${count !== 1 ? 's' : ''}`;
}
function setupSaveBar() {
updateSaveBar();
}
async function saveConfig(mode) {
const buttons = document.querySelectorAll('#save-bar button');
buttons.forEach(b => b.disabled = true);
try {
if (mode === 'restart' && !confirm('Save changes and restart the bot now?')) {
return;
}
const res = await fetch('/api/config', {
method: 'POST',
credentials: 'same-origin',
headers: Util.csrfHeaders({ 'Content-Type': 'application/json' }),
body: JSON.stringify(pendingChanges)
});
const data = await res.json();
if (data.applied) {
for (const key of data.applied) savedConfig[key] = pendingChanges[key];
pendingChanges = {};
updateSaveBar();
document.querySelectorAll('.changed').forEach(el => el.classList.remove('changed'));
Util.showToast(`${data.applied.length} settings saved.`, 'success');
}
const hasErrors = data.errors && data.errors.length > 0;
if (hasErrors) {
Util.showToast(`Errors: ${data.errors.join(', ')}`, 'error');
}
if (mode === 'restart' && !hasErrors) {
await fetch('/api/restart', {
method: 'POST',
credentials: 'same-origin',
headers: Util.csrfHeaders({ 'Content-Type': 'application/json' }),
body: JSON.stringify({ mode: 'immediate' })
});
Util.showToast('Restart initiated.', 'warning');
} else if (mode === 'restart' && hasErrors) {
Util.showToast(`Restart cancelled — save returned errors: ${data.errors.join(', ')}`, 'warning');
}
} catch (e) {
Util.showToast('Failed to save. Bot may be unreachable.', 'error');
} finally {
buttons.forEach(b => b.disabled = false);
}
}
window.Fields = {
setSavedConfig,
populateFields,
handleFieldChange,
initSmartSelects,
markChanged,
isChanged,
updateSaveBar,
setupSaveBar,
saveConfig
};
})();