librelad 875a60f90f LibrePortal v0.1.0 — initial release
A free, open, self-hosted app platform (GNU AGPLv3): one-click app deploys,
Traefik reverse proxy with automatic SSL, rootless Docker support, gluetun
VPN routing, and a web dashboard to manage it all.

Free & open forever to self-host; optional paid hosted services fund it.
See PROMISE.md.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-21 20:37:54 +01:00

228 lines
8.8 KiB
JavaScript
Executable File
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Config Renderer - Handles all rendering logic
class ConfigRenderer {
constructor() {
this.fieldTypes = {
text: this.renderTextField.bind(this),
number: this.renderNumberField.bind(this),
email: this.renderEmailField.bind(this),
password: this.renderPasswordField.bind(this),
select: this.renderSelectField.bind(this),
checkbox: this.renderCheckboxField.bind(this),
textarea: this.renderTextareaField.bind(this)
};
}
renderSubcategoryWithMaster(masterKey, configItems, displaySubcategory, subcategoryDescription, isAdvanced = false) {
const isEnabled = masterKey.value === 'true';
const sectionId = `master-${masterKey.key}`;
let html = `
<div class="config-category">
<div class="domains-wrapper">
<div class="domains-header">
<div>
<h3>${displaySubcategory}</h3>
<p class="category-description">${subcategoryDescription}</p>
</div>
</div>
<div class="domains-divider"></div>
<div class="git-master-toggle">
<div class="field-group">
<label class="checkbox-label master-toggle">
<input type="checkbox" id="${masterKey.key.toLowerCase()}-toggle" name="${masterKey.key}" value="true" ${isEnabled ? 'checked' : ''} onchange="window.ConfigShared.toggleSection('section-content-${sectionId}', this.checked)">
<span class="checkbox-custom"></span>
<span class="checkbox-text">
${masterKey.title || 'Enable Advanced Configuration'}
<span class="tooltip" title="${masterKey.description || 'Enable advanced configuration options'}"></span>
</span>
</label>
</div>
</div>
<div id="section-content-${sectionId}" class="git-section-content ${isEnabled ? '' : 'hidden'}">
<div class="config-fields">
`;
// Add all other fields (excluding the master toggle)
configItems.filter(item => item.key !== masterKey.key).forEach(item => {
const fieldId = `config-${item.key}`;
html += window.ConfigShared?.generateField(fieldId, item.key, item.value, item.title, item.description, item.options) || '';
});
html += `
</div>
</div>
</div>
<div class="spacer spacer-lg"></div>
</div>
`;
return html;
}
renderSubcategorySection(configItems, displaySubcategory, subcategoryDescription, config = {}) {
let html = `
<div class="config-category">
<div class="domains-wrapper">
<div class="domains-header">
<div>
<h3>${displaySubcategory}</h3>
<p class="category-description">${subcategoryDescription}</p>
</div>
</div>
<div class="domains-divider"></div>
<div class="config-fields">
`;
// Add all fields
configItems.forEach(item => {
const fieldId = `config-${item.key}`;
html += window.ConfigShared?.generateField(fieldId, item.key, item.value, item.title, item.description, item.options, config) || '';
});
html += `
</div>
</div>
<div class="spacer spacer-lg"></div>
</div>
`;
return html;
}
renderRegularSubcategory(configItems, displaySubcategory, subcategoryDescription, config = {}) {
let html = `
<div class="config-category">
<div class="domains-wrapper">
<div class="domains-header">
<div>
<h3>${displaySubcategory}</h3>
<p class="category-description">${subcategoryDescription}</p>
</div>
</div>
<div class="domains-divider"></div>
<div class="config-fields">
`;
// Add all fields
configItems.forEach(item => {
const fieldId = `config-${item.key}`;
html += window.ConfigShared?.generateField(fieldId, item.key, item.value, item.title, item.description, item.options, config) || '';
});
html += `
</div>
</div>
<div class="spacer spacer-lg"></div>
</div>
`;
return html;
}
// Field rendering methods
renderTextField(fieldId, key, value, title, description, options) {
return `
<div class="field-group">
<label for="${fieldId}">${title || this.cleanDescription(key)}</label>
<input type="text" id="${fieldId}" name="${key}" value="${value || ''}" class="form-control" placeholder="${options?.placeholder || ''}">
${description ? `<small class="field-description">${this.cleanDescription(description)}</small>` : ''}
</div>
`;
}
renderNumberField(fieldId, key, value, title, description, options) {
return `
<div class="field-group">
<label for="${fieldId}">${title || this.cleanDescription(key)}</label>
<input type="number" id="${fieldId}" name="${key}" value="${value || ''}" class="form-control" min="${options?.min || ''}" max="${options?.max || ''}" step="${options?.step || '1'}">
${description ? `<small class="field-description">${this.cleanDescription(description)}</small>` : ''}
</div>
`;
}
renderEmailField(fieldId, key, value, title, description, options) {
return `
<div class="field-group">
<label for="${fieldId}">${title || this.cleanDescription(key)}</label>
<input type="email" id="${fieldId}" name="${key}" value="${value || ''}" class="form-control" placeholder="${options?.placeholder || ''}">
${description ? `<small class="field-description">${this.cleanDescription(description)}</small>` : ''}
</div>
`;
}
renderPasswordField(fieldId, key, value, title, description, options) {
return `
<div class="field-group">
<label for="${fieldId}">${title || this.cleanDescription(key)}</label>
<div class="password-input-group">
<input type="password" id="${fieldId}" name="${key}" value="${value || ''}" class="form-control" placeholder="${options?.placeholder || ''}">
<button type="button" class="password-toggle-btn" onclick="window.configRenderer.togglePasswordVisibility('${fieldId}')">
<span class="password-icon">👁️</span>
</button>
</div>
${description ? `<small class="field-description">${this.cleanDescription(description)}</small>` : ''}
</div>
`;
}
renderSelectField(fieldId, key, value, title, description, fieldOptions) {
const options = window.ConfigOptions?.getSelectOptions(key) || [];
return `
<div class="field-group">
<label for="${fieldId}">${title || this.cleanDescription(key)}</label>
<select id="${fieldId}" name="${key}" class="form-control">
${options.map(opt => `<option value="${opt.value}" ${opt.value === value ? 'selected' : ''}>${opt.label}</option>`).join('')}
</select>
${description ? `<small class="field-description">${this.cleanDescription(description)}</small>` : ''}
</div>
`;
}
renderCheckboxField(fieldId, key, value, title, description, options) {
const isChecked = value === 'true' || value === true;
return `
<div class="field-group">
<label class="checkbox-label">
<input type="checkbox" id="${fieldId}" name="${key}" value="true" ${isChecked ? 'checked' : ''}>
<span class="checkbox-custom"></span>
<span class="checkbox-text">${title || this.cleanDescription(key)}</span>
</label>
${description ? `<small class="field-description">${this.cleanDescription(description)}</small>` : ''}
</div>
`;
}
renderTextareaField(fieldId, key, value, title, description, options) {
return `
<div class="field-group">
<label for="${fieldId}">${title || this.cleanDescription(key)}</label>
<textarea id="${fieldId}" name="${key}" class="form-control" rows="${options?.rows || '4'}" placeholder="${options?.placeholder || ''}">${value || ''}</textarea>
${description ? `<small class="field-description">${this.cleanDescription(description)}</small>` : ''}
</div>
`;
}
togglePasswordVisibility(fieldId) {
const input = document.getElementById(fieldId);
const button = document.querySelector(`button[onclick*="${fieldId}"]`);
if (input && button) {
if (input.type === 'password') {
input.type = 'text';
button.innerHTML = '<span class="password-icon">🙈️</span>';
} else {
input.type = 'password';
button.innerHTML = '<span class="password-icon">👁️</span>';
}
}
}
cleanDescription(description) {
if (!description) return '';
return description.replace(/CFG_[A-Z_]+/g, '').replace(/[-_]/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
}
}
// Export for use in other modules
window.ConfigRenderer = ConfigRenderer;