Branding customization

This commit is contained in:
2026-03-30 14:35:30 -04:00
parent 53eaf1c76d
commit a4e605297a
7 changed files with 295 additions and 43 deletions
+141 -7
View File
@@ -1,11 +1,11 @@
{% extends "base.html" %}
{% block title %}System Settings — TechDesk{% endblock %}
{% block title %}System Settings — {{ branding.app_name }}{% endblock %}
{% block content %}
<div class="page-header" style="margin-bottom:28px;">
<div>
<h1 class="page-title">System Settings</h1>
<p class="page-subtitle">Manage application-wide configuration flags.</p>
<p class="page-subtitle">Manage application-wide configuration and branding.</p>
</div>
</div>
@@ -18,7 +18,103 @@
{% endfor %}
{% endwith %}
<div class="card" style="max-width:640px;">
<!-- Branding -->
<div class="card mb-4" style="max-width:680px;">
<div class="card-header" style="padding:16px 20px;border-bottom:1px solid var(--border);">
<h5 class="mb-0" style="font-size:15px;font-weight:600;">
<i class="bi bi-palette me-2"></i>Branding &amp; Appearance
</h5>
</div>
<div class="card-body" style="padding:20px;">
<form method="POST" action="{{ url_for('admin.settings') }}" enctype="multipart/form-data">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<input type="hidden" name="form_type" value="branding">
<input type="hidden" name="remove_logo" id="remove-logo-input" value="0">
<!-- Logo preview + upload -->
<div class="mb-4 d-flex align-items-center gap-4">
<div id="logo-preview-wrap"
style="width:64px;height:64px;border-radius:12px;background:{{ branding_settings.primary_color }};display:flex;align-items:center;justify-content:center;flex-shrink:0;overflow:hidden;box-shadow:0 2px 8px rgba(0,0,0,.12);">
{% if branding_settings.logo_stored_name %}
<img id="logo-preview-img"
src="{{ url_for('auth.serve_logo', filename=branding_settings.logo_stored_name) }}"
alt="Logo" style="width:64px;height:64px;object-fit:contain;padding:4px;"/>
{% else %}
<span id="logo-preview-initials"
style="font-family:'Space Mono',monospace;font-weight:700;font-size:18px;color:#fff;">
{{ branding_settings.logo_initials[:2] }}
</span>
{% endif %}
</div>
<div style="flex:1;">
<label class="form-label" style="font-size:12px;font-weight:600;">
Company Logo
<span style="color:var(--muted);font-weight:400;">(PNG, JPG, GIF, WebP — max 2 MB)</span>
</label>
<input type="file" class="form-control form-control-sm" name="logo_file"
id="logo-file-input" accept=".png,.jpg,.jpeg,.gif,.webp"/>
{% if branding_settings.logo_stored_name %}
<div class="mt-2" id="remove-logo-row">
<button type="button" class="btn btn-sm btn-outline-danger" onclick="removeLogo()">
<i class="bi bi-trash me-1"></i>Remove current logo
</button>
</div>
{% endif %}
</div>
</div>
<div class="row g-3 mb-3">
<div class="col-md-6">
<label class="form-label">Application Name</label>
<input type="text" class="form-control" name="app_name"
value="{{ branding_settings.app_name }}" maxlength="60" required/>
<div class="form-text">Shown in the sidebar and browser tab.</div>
</div>
<div class="col-md-6">
<label class="form-label">App Subtitle</label>
<input type="text" class="form-control" name="app_subtitle"
value="{{ branding_settings.app_subtitle }}" maxlength="80"/>
<div class="form-text">Shown below the app name in the sidebar.</div>
</div>
<div class="col-md-6">
<label class="form-label">Company Name</label>
<input type="text" class="form-control" name="company_name"
value="{{ branding_settings.company_name }}" maxlength="100"/>
<div class="form-text">Shown on the login and register pages.</div>
</div>
<div class="col-md-3">
<label class="form-label">Logo Initials</label>
<input type="text" class="form-control" name="logo_initials" id="logo-initials"
value="{{ branding_settings.logo_initials }}" maxlength="3"
oninput="updateInitialsPreview(this.value)"/>
<div class="form-text">Fallback when no logo is uploaded.</div>
</div>
<div class="col-md-3">
<label class="form-label">Primary Colour</label>
<div class="d-flex gap-2 align-items-center">
<input type="color" class="form-control form-control-color" name="primary_color"
id="primary-color-input"
value="{{ branding_settings.primary_color }}"
oninput="updateColorPreview(this.value)"
style="width:48px;height:38px;padding:2px;cursor:pointer;"/>
<input type="text" class="form-control form-control-sm" id="primary-color-text"
value="{{ branding_settings.primary_color }}" maxlength="7"
style="font-family:'Space Mono',monospace;font-size:12px;"
oninput="syncColorPicker(this.value)"/>
</div>
<div class="form-text">Accent colour across the interface.</div>
</div>
</div>
<button type="submit" class="btn btn-primary">
<i class="bi bi-check2 me-2"></i>Save Branding
</button>
</form>
</div>
</div>
<!-- Registration -->
<div class="card" style="max-width:680px;">
<div class="card-header" style="padding:16px 20px;border-bottom:1px solid var(--border);">
<h5 class="mb-0" style="font-size:15px;font-weight:600;">
<i class="bi bi-person-gear me-2"></i>User Registration
@@ -27,13 +123,11 @@
<div class="card-body" style="padding:20px;">
<p style="font-size:13px;color:var(--muted);margin-bottom:20px;">
When disabled, the <code>/auth/register</code> endpoint redirects all visitors to the login
page with an informational message. Existing accounts and admin-created accounts are
unaffected.
page. Existing accounts and admin-created accounts are unaffected.
</p>
<form method="POST" action="{{ url_for('admin.settings') }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<input type="hidden" name="form_type" value="registration">
<div class="d-flex align-items-center justify-content-between p-3"
style="border:1px solid var(--border);border-radius:10px;background:var(--surface);">
<div>
@@ -66,4 +160,44 @@
</form>
</div>
</div>
<script>
document.getElementById('logo-file-input').addEventListener('change', function() {
const file = this.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = e => {
document.getElementById('logo-preview-wrap').innerHTML =
`<img src="${e.target.result}" style="width:64px;height:64px;object-fit:contain;padding:4px;"/>`;
};
reader.readAsDataURL(file);
});
function updateInitialsPreview(val) {
const el = document.getElementById('logo-preview-initials');
if (el) el.textContent = val.substring(0, 2).toUpperCase();
}
function updateColorPreview(hex) {
document.getElementById('primary-color-text').value = hex;
document.getElementById('logo-preview-wrap').style.background = hex;
}
function syncColorPicker(val) {
if (/^#[0-9a-fA-F]{6}$/.test(val)) {
document.getElementById('primary-color-input').value = val;
document.getElementById('logo-preview-wrap').style.background = val;
}
}
function removeLogo() {
if (!confirm('Remove the current logo?')) return;
document.getElementById('remove-logo-input').value = '1';
const initials = document.getElementById('logo-initials').value.substring(0, 2).toUpperCase();
document.getElementById('logo-preview-wrap').innerHTML =
`<span id="logo-preview-initials" style="font-family:'Space Mono',monospace;font-weight:700;font-size:18px;color:#fff;">${initials}</span>`;
const row = document.getElementById('remove-logo-row');
if (row) row.style.display = 'none';
}
</script>
{% endblock %}