assets/js/wizard.js
/* ==========================================================================
Installation wizard
========================================================================== */
/*
|--------------------------------------------------------------------------
| First-time setup
|--------------------------------------------------------------------------
|
| Handles the initial Brivacia installation process, including site
| configuration, validation and creation of the settings file.
|
*/
(() => {
const form = document.getElementById('wizard-form');
const sites = document.getElementById('wizard-sites');
const template = document.getElementById('wizard-site-template');
const message = document.getElementById('wizard-message');
if (!form) return;
const text = {
installing: message?.dataset.installing ?? 'Installing Brivacia...',
installed: message?.dataset.installed ?? 'Brivacia installed.',
error: message?.dataset.error ?? 'Unable to install Brivacia.',
networkError: message?.dataset.networkError ?? 'Network error.'
};
function setMessage(value, status = '') {
if (!message) return;
message.className = status;
message.textContent = value || '';
}
function siteList() {
return sites?.querySelector(':scope > div') || null;
}
/*
|--------------------------------------------------------------------------
| Site list management
|--------------------------------------------------------------------------
|
| Site entries are stored as an indexed array. Whenever an entry is
| removed, field names must be reindexed so PHP receives a continuous
| array structure on submission.
|
*/
function reindexSites() {
siteList()?.querySelectorAll(':scope > fieldset').forEach((fieldset, index) => {
const code = fieldset.querySelector('input[name$="[code]"]');
const domain = fieldset.querySelector('input[name$="[domain]"]');
if (code) code.name = `sites[${index}][code]`;
if (domain) domain.name = `sites[${index}][domain]`;
});
}
function addSite() {
const list = siteList();
if (!list || !template) return;
const wrapper = document.createElement('div');
wrapper.innerHTML = template.innerHTML
.replaceAll('__INDEX__', String(list.children.length))
.trim();
if (wrapper.firstElementChild) {
list.appendChild(wrapper.firstElementChild);
}
}
function removeSite(button) {
const list = siteList();
const fieldset = button.closest('fieldset');
if (!list || !fieldset || list.children.length <= 1) return;
fieldset.remove();
reindexSites();
}
/*
|--------------------------------------------------------------------------
| Form serialization
|--------------------------------------------------------------------------
|
| Convert the wizard form into a clean JSON payload before sending it
| to the installation endpoint.
|
*/
function formToObject() {
const data = new FormData(form);
const result = {};
for (const [name, value] of data.entries()) {
if (!name.startsWith('sites[')) {
result[name] = value;
}
}
form.querySelectorAll('input[type="checkbox"]').forEach((input) => {
result[input.name] = input.checked;
});
result.sites = [];
siteList()?.querySelectorAll(':scope > fieldset').forEach((fieldset) => {
const code = fieldset.querySelector('input[name$="[code]"]')?.value.trim() || '';
const domain = fieldset.querySelector('input[name$="[domain]"]')?.value.trim() || '';
if (code !== '' || domain !== '') {
result.sites.push({ code, domain });
}
});
return result;
}
form.addEventListener('click', (event) => {
const button = event.target.closest('button');
if (!button) return;
if (button.value === 'add-site') {
addSite();
return;
}
if (button.value === 'remove-site') {
removeSite(button);
}
});
form.addEventListener('submit', async (event) => {
event.preventDefault();
setMessage(text.installing, 'warning');
try {
const response = await fetch('/api/settings.php?action=install', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(formToObject())
});
const data = await response.json();
if (!response.ok || !data.ok) {
setMessage(data.error ? `${text.error} ${data.error}` : text.error, 'danger');
return;
}
setMessage(text.installed, 'success');
setTimeout(() => {
document.getElementById('wizard-modal')?.close();
sessionStorage.setItem('brivacia.showPixelModal', '1');
window.location.reload();
}, 700);
} catch {
setMessage(text.networkError, 'danger');
}
});
})();