{% extends "base.twig" %}
{% block title %}{{ t('ui.admin.template_editor.page_title_prefix', {}, locale, ['common']) }} - {{ parent() }}{% endblock %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<h2>
<i class="fas fa-file-code"></i>
{{ t('ui.admin.template_editor.heading', {}, locale, ['common']) }}
</h2>
<div class="text-muted small">
{{ t('ui.admin.template_editor.restricted_edits_prefix', {}, locale, ['common']) }} <code>templates/custom</code>
</div>
</div>
<div class="row">
<div class="col-lg-4">
<div class="card mb-3">
<div class="card-header d-flex justify-content-between align-items-center">
<h6 class="mb-0">{{ t('ui.admin.template_editor.custom_templates', {}, locale, ['common']) }}</h6>
<button class="btn btn-sm btn-outline-secondary" onclick="loadTemplates()">
<i class="fas fa-sync"></i>
</button>
</div>
<div class="card-body">
<div class="mb-3">
<input type="text" class="form-control" id="templateSearch" placeholder="{{ t('ui.admin.template_editor.search_templates_placeholder', {}, locale, ['common']) }}">
</div>
<div id="templateList" class="list-group small">
<div class="text-muted">{{ t('ui.admin.template_editor.loading_templates', {}, locale, ['common']) }}</div>
</div>
</div>
</div>
<div class="card">
<div class="card-header">
<h6 class="mb-0">{{ t('ui.admin.template_editor.new_template', {}, locale, ['common']) }}</h6>
</div>
<div class="card-body">
<div class="mb-2">
<label class="form-label">{{ t('ui.admin.template_editor.path_relative_custom', {}, locale, ['common']) }}</label>
<input type="text" class="form-control font-monospace" id="newTemplatePath" placeholder="{{ t('ui.admin.template_editor.new_template_path_placeholder', {}, locale, ['common']) }}">
</div>
<button class="btn btn-sm btn-outline-primary" onclick="createTemplate()">
<i class="fas fa-plus"></i> {{ t('ui.admin.template_editor.create', {}, locale, ['common']) }}
</button>
</div>
</div>
</div>
<div class="col-lg-8">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<div>
<h6 class="mb-1">{{ t('ui.admin.template_editor.editor', {}, locale, ['common']) }}</h6>
<div class="text-muted small" id="currentTemplateLabel">{{ t('ui.admin.template_editor.no_template_selected', {}, locale, ['common']) }}</div>
</div>
<div class="btn-group">
<button class="btn btn-outline-primary btn-sm" id="installTemplateBtn" onclick="installTemplate()" disabled>
<i class="fas fa-download"></i> {{ t('ui.admin.template_editor.install', {}, locale, ['common']) }}
</button>
<button class="btn btn-outline-success btn-sm" id="saveTemplateBtn" onclick="saveTemplate()" disabled>
<i class="fas fa-save"></i> {{ t('ui.common.save', {}, locale, ['common']) }}
</button>
<button class="btn btn-outline-danger btn-sm" id="deleteTemplateBtn" onclick="deleteTemplate()" disabled>
<i class="fas fa-trash"></i> {{ t('ui.common.delete', {}, locale, ['common']) }}
</button>
</div>
</div>
<div class="card-body">
<textarea class="form-control font-monospace" id="templateEditor" rows="20" disabled></textarea>
<div class="form-text mt-2">
{{ t('ui.admin.template_editor.changes_apply_immediately', {}, locale, ['common']) }}
</div>
</div>
</div>
<div id="templateStatus" class="alert d-none mt-3" role="alert"></div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
let templates = [];
let currentTemplate = null;
let isDirty = false;
function apiError(payload, fallback) {
if (window.getApiErrorMessage) {
return window.getApiErrorMessage(payload, fallback);
}
return payload && payload.error ? payload.error : fallback;
}
function uiT(key, fallback, params = {}) {
if (window.t) {
return window.t(key, params, fallback);
}
return fallback;
}
document.addEventListener('DOMContentLoaded', function() {
loadTemplates();
document.getElementById('templateSearch').addEventListener('input', function() {
renderTemplateList(this.value.trim().toLowerCase());
});
document.getElementById('templateEditor').addEventListener('input', function() {
if (currentTemplate) {
isDirty = true;
}
});
});
function setStatus(message, type = 'info') {
const status = document.getElementById('templateStatus');
status.className = `alert alert-${type}`;
status.textContent = message;
status.classList.remove('d-none');
}
function clearStatus() {
const status = document.getElementById('templateStatus');
status.classList.add('d-none');
status.textContent = '';
}
function loadTemplates() {
clearStatus();
fetch('/admin/api/custom-templates')
.then(response => response.json())
.then(data => {
templates = data.templates || [];
renderTemplateList(document.getElementById('templateSearch').value.trim().toLowerCase());
})
.catch(() => {
setStatus(uiT('ui.admin.template_editor.load_templates_failed', 'Failed to load templates.'), 'danger');
});
}
function renderTemplateList(filter = '') {
const list = document.getElementById('templateList');
list.innerHTML = '';
const filtered = templates.filter(t => t.path.toLowerCase().includes(filter));
if (filtered.length === 0) {
list.innerHTML = `<div class="text-muted">${uiT('ui.admin.template_editor.no_templates_found', 'No templates found.')}</div>`;
return;
}
filtered.forEach(t => {
const item = document.createElement('button');
item.type = 'button';
item.className = 'list-group-item list-group-item-action';
item.textContent = t.path;
item.onclick = () => loadTemplate(t.path);
list.appendChild(item);
});
}
function loadTemplate(path) {
if (isDirty && !confirm(uiT('ui.admin.template_editor.unsaved_changes_confirm', 'You have unsaved changes. Continue and discard them?'))) {
return;
}
clearStatus();
fetch(`/admin/api/custom-templates/file?path=${encodeURIComponent(path)}`)
.then(response => {
if (!response.ok) {
throw new Error(uiT('ui.admin.template_editor.load_template_failed', 'Failed to load template'));
}
return response.json();
})
.then(data => {
currentTemplate = data.path;
isDirty = false;
document.getElementById('currentTemplateLabel').textContent = data.path;
const editor = document.getElementById('templateEditor');
editor.value = data.content || '';
const isExample = data.path.endsWith('.twig.example');
editor.disabled = isExample;
document.getElementById('saveTemplateBtn').disabled = isExample;
document.getElementById('deleteTemplateBtn').disabled = isExample;
document.getElementById('installTemplateBtn').disabled = !isExample;
})
.catch(() => {
setStatus(uiT('ui.admin.template_editor.load_template_failed', 'Failed to load template'), 'danger');
});
}
function createTemplate() {
const path = document.getElementById('newTemplatePath').value.trim();
if (!path) {
setStatus(uiT('ui.admin.template_editor.enter_template_path', 'Enter a template path (e.g., systemnews.twig).'), 'warning');
return;
}
currentTemplate = path;
isDirty = true;
document.getElementById('currentTemplateLabel').textContent = path;
const editor = document.getElementById('templateEditor');
editor.value = '';
editor.disabled = false;
document.getElementById('saveTemplateBtn').disabled = false;
document.getElementById('deleteTemplateBtn').disabled = true;
document.getElementById('installTemplateBtn').disabled = true;
}
function saveTemplate() {
if (!currentTemplate) {
return;
}
const content = document.getElementById('templateEditor').value;
fetch('/admin/api/custom-templates/file', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ path: currentTemplate, content })
})
.then(response => response.json().then(data => ({ ok: response.ok, data })))
.then(({ ok, data }) => {
if (!ok) {
throw new Error(apiError(data, uiT('ui.admin.template_editor.save_template_failed', 'Failed to save template')));
}
isDirty = false;
loadTemplates();
const successMessage = window.getApiMessage
? window.getApiMessage(data, uiT('ui.admin.template_editor.template_saved_success', 'Template saved successfully.'))
: uiT('ui.admin.template_editor.template_saved_success', 'Template saved successfully.');
setStatus(successMessage, 'success');
document.getElementById('deleteTemplateBtn').disabled = false;
document.getElementById('installTemplateBtn').disabled = true;
})
.catch(err => {
setStatus(err.message || uiT('ui.admin.template_editor.save_template_failed', 'Failed to save template'), 'danger');
});
}
function deleteTemplate() {
if (!currentTemplate) {
return;
}
if (!confirm(uiT('ui.admin.template_editor.delete_confirm', `Delete ${currentTemplate}? This cannot be undone.`, { template: currentTemplate }))) {
return;
}
fetch(`/admin/api/custom-templates/file?path=${encodeURIComponent(currentTemplate)}`, {
method: 'DELETE'
})
.then(response => response.json().then(data => ({ ok: response.ok, data })))
.then(({ ok, data }) => {
if (!ok) {
throw new Error(apiError(data, uiT('ui.admin.template_editor.delete_template_failed', 'Failed to delete template')));
}
currentTemplate = null;
isDirty = false;
document.getElementById('templateEditor').value = '';
document.getElementById('templateEditor').disabled = true;
document.getElementById('currentTemplateLabel').textContent = uiT('ui.admin.template_editor.no_template_selected', 'No template selected');
document.getElementById('saveTemplateBtn').disabled = true;
document.getElementById('deleteTemplateBtn').disabled = true;
document.getElementById('installTemplateBtn').disabled = true;
loadTemplates();
const successMessage = window.getApiMessage
? window.getApiMessage(data, uiT('ui.admin.template_editor.template_deleted_success', 'Template deleted.'))
: uiT('ui.admin.template_editor.template_deleted_success', 'Template deleted.');
setStatus(successMessage, 'success');
})
.catch(err => {
setStatus(err.message || uiT('ui.admin.template_editor.delete_template_failed', 'Failed to delete template'), 'danger');
});
}
function installTemplate() {
if (!currentTemplate || !currentTemplate.endsWith('.twig.example')) {
return;
}
fetch('/admin/api/custom-templates/install', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ source: currentTemplate })
})
.then(response => response.json().then(data => ({ ok: response.ok, data })))
.then(({ ok, data }) => {
if (!ok) {
throw new Error(apiError(data, uiT('ui.admin.template_editor.install_template_failed', 'Failed to install template')));
}
loadTemplates();
loadTemplate(data.path);
const successMessage = window.getApiMessage
? window.getApiMessage(data, uiT('ui.admin.template_editor.template_installed_success', 'Template installed from example.'))
: uiT('ui.admin.template_editor.template_installed_success', 'Template installed from example.');
setStatus(successMessage, 'success');
})
.catch(err => {
setStatus(err.message || uiT('ui.admin.template_editor.install_template_failed', 'Failed to install template'), 'danger');
});
}
</script>
{% endblock %}
|