forked from UKSOURCE/cms.lams
523 lines
27 KiB
Plaintext
523 lines
27 KiB
Plaintext
<div class="container">
|
|
<div class="d-flex justify-content-between align-items-center mt-4 mb-4">
|
|
<div>
|
|
<h1 class="h3 mb-0" style="color: var(--primary-dark);">
|
|
<%= title %>
|
|
</h1>
|
|
<p class="text-muted mb-0">Edit content displayed in the site footer</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row">
|
|
<div class="col-12">
|
|
<form method="POST" id="footerForm" action="/admin/footer/update">
|
|
<!-- Hidden JSON inputs -->
|
|
<input type="hidden" name="brand" id="brandJson">
|
|
<input type="hidden" name="explore" id="exploreJson">
|
|
<input type="hidden" name="contact" id="contactJson">
|
|
<input type="hidden" name="newsletter" id="newsletterJson">
|
|
<input type="hidden" name="bottom" id="bottomJson">
|
|
|
|
<!-- Tabs -->
|
|
<div class="card shadow-sm border-0 mb-4">
|
|
<div class="card-header bg-white border-bottom">
|
|
<ul class="nav nav-tabs card-header-tabs" id="footerTabs" role="tablist">
|
|
<li class="nav-item" role="presentation">
|
|
<a class="nav-link active" data-bs-toggle="tab" href="#tab-brand-pane" role="tab">
|
|
<i class="fas fa-building me-2"></i>Brand
|
|
</a>
|
|
</li>
|
|
<li class="nav-item" role="presentation">
|
|
<a class="nav-link" data-bs-toggle="tab" href="#tab-explore-pane" role="tab">
|
|
<i class="fas fa-compass me-2"></i>Explore
|
|
</a>
|
|
</li>
|
|
<li class="nav-item" role="presentation">
|
|
<a class="nav-link" data-bs-toggle="tab" href="#tab-contact-pane" role="tab">
|
|
<i class="fas fa-address-card me-2"></i>Contact
|
|
</a>
|
|
</li>
|
|
<li class="nav-item" role="presentation">
|
|
<a class="nav-link" data-bs-toggle="tab" href="#tab-newsletter-pane" role="tab">
|
|
<i class="fas fa-envelope me-2"></i>Newsletter
|
|
</a>
|
|
</li>
|
|
<li class="nav-item" role="presentation">
|
|
<a class="nav-link" data-bs-toggle="tab" href="#tab-bottom-pane" role="tab">
|
|
<i class="fas fa-grip-horizontal me-2"></i>Bottom Bar
|
|
</a>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
|
|
<div class="card-body">
|
|
<div class="tab-content">
|
|
|
|
<!-- ===== BRAND TAB ===== -->
|
|
<div class="tab-pane fade show active" id="tab-brand-pane" role="tabpanel">
|
|
<div class="card border shadow-sm">
|
|
<div class="card-header bg-white">
|
|
<h6 class="mb-0"><i class="fas fa-building me-2"></i>Brand</h6>
|
|
</div>
|
|
<div class="card-body p-4">
|
|
<div class="row g-3">
|
|
<div class="col-md-6">
|
|
<label class="form-label">Logo Image URL</label>
|
|
<div class="input-group">
|
|
<input type="text" class="form-control" id="brandLogoImage"
|
|
value="<%= data.brand?.logo?.image || '' %>">
|
|
<button class="btn btn-outline-primary" type="button"
|
|
onclick="openImageUploader('brandLogoImage', 'footer')">
|
|
<i class="fas fa-upload me-1"></i>Upload
|
|
</button>
|
|
</div>
|
|
<% if (data.brand?.logo?.image) { %>
|
|
<img src="<%= data.brand.logo.image %>"
|
|
class="img-thumbnail uploaded-preview mt-2"
|
|
style="max-height:80px;">
|
|
<% } %>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label">Logo Href</label>
|
|
<input type="text" class="form-control" id="brandLogoHref"
|
|
value="<%= data.brand?.logo?.href || '/' %>">
|
|
</div>
|
|
<div class="col-md-12">
|
|
<label class="form-label">Description</label>
|
|
<textarea class="form-control" id="brandDescription"
|
|
rows="3"><%= data.brand?.description || '' %></textarea>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mt-4">
|
|
<div class="d-flex justify-content-between align-items-center mb-2">
|
|
<h6 class="mb-0">Social Links</h6>
|
|
<button type="button" class="btn btn-outline-primary btn-sm"
|
|
onclick="addSocialLink()">
|
|
<i class="fas fa-plus me-1"></i>Add Social
|
|
</button>
|
|
</div>
|
|
<div id="socialLinksContainer"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ===== EXPLORE TAB ===== -->
|
|
<div class="tab-pane fade" id="tab-explore-pane" role="tabpanel">
|
|
<div class="card border shadow-sm">
|
|
<div class="card-header bg-white d-flex justify-content-between align-items-center">
|
|
<h6 class="mb-0"><i class="fas fa-compass me-2"></i>Explore</h6>
|
|
<button type="button" class="btn btn-outline-primary btn-sm"
|
|
onclick="addExploreLink()">
|
|
<i class="fas fa-plus me-1"></i>Add Link
|
|
</button>
|
|
</div>
|
|
<div class="card-body p-4">
|
|
<div class="row g-3 mb-4">
|
|
<div class="col-md-6">
|
|
<label class="form-label">Heading</label>
|
|
<input type="text" class="form-control" id="exploreHeading"
|
|
value="<%= data.explore?.heading || '' %>">
|
|
</div>
|
|
</div>
|
|
<div id="exploreLinksContainer"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ===== CONTACT TAB ===== -->
|
|
<div class="tab-pane fade" id="tab-contact-pane" role="tabpanel">
|
|
<div class="card border shadow-sm">
|
|
<div class="card-header bg-white">
|
|
<h6 class="mb-0"><i class="fas fa-address-card me-2"></i>Contact</h6>
|
|
</div>
|
|
<div class="card-body p-4">
|
|
<div class="row g-3">
|
|
<div class="col-md-6">
|
|
<label class="form-label">Heading</label>
|
|
<input type="text" class="form-control" id="contactHeading"
|
|
value="<%= data.contact?.heading || '' %>">
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label">Phone</label>
|
|
<input type="text" class="form-control" id="contactPhone"
|
|
value="<%= data.contact?.phone || '' %>">
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label">Email</label>
|
|
<input type="text" class="form-control" id="contactEmail"
|
|
value="<%= data.contact?.email || '' %>">
|
|
</div>
|
|
<div class="col-md-12">
|
|
<label class="form-label">Address</label>
|
|
<input type="text" class="form-control" id="contactAddress"
|
|
value="<%= data.contact?.address || '' %>">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ===== NEWSLETTER TAB ===== -->
|
|
<div class="tab-pane fade" id="tab-newsletter-pane" role="tabpanel">
|
|
<div class="card border shadow-sm">
|
|
<div class="card-header bg-white">
|
|
<h6 class="mb-0"><i class="fas fa-envelope me-2"></i>Newsletter</h6>
|
|
</div>
|
|
<div class="card-body p-4">
|
|
<div class="row g-3">
|
|
<div class="col-md-6">
|
|
<label class="form-label">Heading</label>
|
|
<input type="text" class="form-control" id="newsletterHeading"
|
|
value="<%= data.newsletter?.heading || '' %>">
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label">Button Text</label>
|
|
<input type="text" class="form-control" id="newsletterButtonText"
|
|
value="<%= data.newsletter?.buttonText || '' %>">
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label">Placeholder</label>
|
|
<input type="text" class="form-control" id="newsletterPlaceholder"
|
|
value="<%= data.newsletter?.placeholder || '' %>">
|
|
</div>
|
|
<div class="col-md-12">
|
|
<label class="form-label">Description</label>
|
|
<textarea class="form-control" id="newsletterDescription"
|
|
rows="3"><%= data.newsletter?.description || '' %></textarea>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ===== BOTTOM BAR TAB ===== -->
|
|
<div class="tab-pane fade" id="tab-bottom-pane" role="tabpanel">
|
|
<div class="card border shadow-sm">
|
|
<div class="card-header bg-white d-flex justify-content-between align-items-center">
|
|
<h6 class="mb-0"><i class="fas fa-grip-horizontal me-2"></i>Bottom Bar</h6>
|
|
<button type="button" class="btn btn-outline-primary btn-sm"
|
|
onclick="addBottomLink()">
|
|
<i class="fas fa-plus me-1"></i>Add Link
|
|
</button>
|
|
</div>
|
|
<div class="card-body p-4">
|
|
<div class="row g-3 mb-4">
|
|
<div class="col-md-12">
|
|
<label class="form-label">Copyright Text</label>
|
|
<input type="text" class="form-control" id="bottomCopyright"
|
|
value="<%= data.bottom?.copyright || '' %>">
|
|
</div>
|
|
</div>
|
|
<div id="bottomLinksContainer"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</div><!-- /.tab-content -->
|
|
</div><!-- /.card-body -->
|
|
|
|
<div class="card-footer bg-light d-flex justify-content-end py-3 gap-2">
|
|
<button type="button" class="btn btn-outline-secondary px-4" onclick="resetForm()">
|
|
<i class="fas fa-undo me-2"></i>Reset
|
|
</button>
|
|
<button type="submit" class="btn btn-outline-primary px-4" id="submitBtn">
|
|
<i class="fas fa-save me-2"></i>Save Changes
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
let originalFormData = null;
|
|
|
|
document.addEventListener('DOMContentLoaded', function () {
|
|
originalFormData = <%- JSON.stringify(data) %>;
|
|
populateAll(originalFormData);
|
|
|
|
document.getElementById('footerForm').addEventListener('submit', function (e) {
|
|
e.preventDefault();
|
|
serializeAll();
|
|
this.submit();
|
|
});
|
|
});
|
|
|
|
// ── Populate all sections ────────────────────────────────────────────────
|
|
function populateAll(data) {
|
|
if (!data) return;
|
|
|
|
// Brand
|
|
const b = data.brand || {};
|
|
setVal('brandLogoImage', b.logo?.image);
|
|
setVal('brandLogoHref', b.logo?.href);
|
|
setVal('brandDescription', b.description);
|
|
populateSocialLinks(b.social || []);
|
|
|
|
// Explore
|
|
const ex = data.explore || {};
|
|
setVal('exploreHeading', ex.heading);
|
|
populateExploreLinks(ex.links || []);
|
|
|
|
// Contact
|
|
const c = data.contact || {};
|
|
setVal('contactHeading', c.heading);
|
|
setVal('contactAddress', c.address);
|
|
setVal('contactPhone', c.phone);
|
|
setVal('contactEmail', c.email);
|
|
|
|
// Newsletter
|
|
const n = data.newsletter || {};
|
|
setVal('newsletterHeading', n.heading);
|
|
setVal('newsletterDescription', n.description);
|
|
setVal('newsletterPlaceholder', n.placeholder);
|
|
setVal('newsletterButtonText', n.buttonText);
|
|
|
|
// Bottom
|
|
const bt = data.bottom || {};
|
|
setVal('bottomCopyright', bt.copyright);
|
|
populateBottomLinks(bt.links || []);
|
|
}
|
|
|
|
function setVal(id, val) {
|
|
const el = document.getElementById(id);
|
|
if (el) el.value = val || '';
|
|
}
|
|
|
|
// ── Serialize all sections into hidden JSON inputs ───────────────────────
|
|
function serializeAll() {
|
|
// Brand
|
|
document.getElementById('brandJson').value = JSON.stringify({
|
|
logo: {
|
|
image: v('brandLogoImage'),
|
|
href: v('brandLogoHref'),
|
|
},
|
|
description: v('brandDescription'),
|
|
social: Array.from(document.querySelectorAll('.social-link-item')).map(row => ({
|
|
icon: row.querySelector('[data-field="icon"]').value.trim(),
|
|
href: row.querySelector('[data-field="href"]').value.trim(),
|
|
})),
|
|
});
|
|
|
|
// Explore
|
|
document.getElementById('exploreJson').value = JSON.stringify({
|
|
heading: v('exploreHeading'),
|
|
links: Array.from(document.querySelectorAll('.explore-link-item')).map(row => ({
|
|
label: row.querySelector('[data-field="label"]').value.trim(),
|
|
href: row.querySelector('[data-field="href"]').value.trim(),
|
|
})),
|
|
});
|
|
|
|
// Contact
|
|
document.getElementById('contactJson').value = JSON.stringify({
|
|
heading: v('contactHeading'),
|
|
address: v('contactAddress'),
|
|
phone: v('contactPhone'),
|
|
email: v('contactEmail'),
|
|
});
|
|
|
|
// Newsletter
|
|
document.getElementById('newsletterJson').value = JSON.stringify({
|
|
heading: v('newsletterHeading'),
|
|
description: v('newsletterDescription'),
|
|
placeholder: v('newsletterPlaceholder'),
|
|
buttonText: v('newsletterButtonText'),
|
|
});
|
|
|
|
// Bottom
|
|
document.getElementById('bottomJson').value = JSON.stringify({
|
|
copyright: v('bottomCopyright'),
|
|
links: Array.from(document.querySelectorAll('.bottom-link-item')).map(row => ({
|
|
label: row.querySelector('[data-field="label"]').value.trim(),
|
|
href: row.querySelector('[data-field="href"]').value.trim(),
|
|
})),
|
|
});
|
|
}
|
|
|
|
function v(id) {
|
|
const el = document.getElementById(id);
|
|
return el ? el.value.trim() : '';
|
|
}
|
|
|
|
// ── Social Links ─────────────────────────────────────────────────────────
|
|
function addSocialLink(item = {}) {
|
|
const c = document.getElementById('socialLinksContainer');
|
|
c.insertAdjacentHTML('beforeend', `
|
|
<div class="card mb-2 social-link-item">
|
|
<div class="card-body p-3">
|
|
<div class="row g-2 align-items-center">
|
|
<div class="col-md-4">
|
|
<label class="form-label small">Icon</label>
|
|
<div class="input-group">
|
|
<span class="input-group-text icon-preview-cell" style="min-width:38px">
|
|
${item.icon ? `<i class="${esc(item.icon)}"></i>` : ''}
|
|
</span>
|
|
<input type="text" class="form-control form-control-sm" data-field="icon"
|
|
value="${esc(item.icon)}" placeholder="Click to pick..." readonly
|
|
style="cursor:pointer;background:#fff"
|
|
onclick="IconPicker.open(this)">
|
|
</div>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label small">Href</label>
|
|
<input type="text" class="form-control form-control-sm" data-field="href"
|
|
value="${esc(item.href)}" placeholder="https://...">
|
|
</div>
|
|
<div class="col-md-2 d-flex align-items-end">
|
|
<button type="button" class="btn btn-link text-danger btn-sm p-0"
|
|
onclick="this.closest('.social-link-item').remove()">
|
|
<i class="fas fa-trash"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>`);
|
|
}
|
|
|
|
function populateSocialLinks(items) {
|
|
document.getElementById('socialLinksContainer').innerHTML = '';
|
|
items.forEach(item => addSocialLink(item));
|
|
}
|
|
|
|
// ── Explore Links ────────────────────────────────────────────────────────
|
|
function addExploreLink(item = {}) {
|
|
const c = document.getElementById('exploreLinksContainer');
|
|
c.insertAdjacentHTML('beforeend', `
|
|
<div class="card mb-2 explore-link-item">
|
|
<div class="card-body p-3">
|
|
<div class="row g-2 align-items-center">
|
|
<div class="col-md-5">
|
|
<label class="form-label small">Label</label>
|
|
<input type="text" class="form-control form-control-sm" data-field="label"
|
|
value="${esc(item.label)}">
|
|
</div>
|
|
<div class="col-md-5">
|
|
<label class="form-label small">Href</label>
|
|
<input type="text" class="form-control form-control-sm" data-field="href"
|
|
value="${esc(item.href)}">
|
|
</div>
|
|
<div class="col-md-2 d-flex align-items-end">
|
|
<button type="button" class="btn btn-link text-danger btn-sm p-0"
|
|
onclick="this.closest('.explore-link-item').remove()">
|
|
<i class="fas fa-trash"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>`);
|
|
}
|
|
|
|
function populateExploreLinks(items) {
|
|
document.getElementById('exploreLinksContainer').innerHTML = '';
|
|
items.forEach(item => addExploreLink(item));
|
|
}
|
|
|
|
// ── Bottom Links ─────────────────────────────────────────────────────────
|
|
function addBottomLink(item = {}) {
|
|
const c = document.getElementById('bottomLinksContainer');
|
|
c.insertAdjacentHTML('beforeend', `
|
|
<div class="card mb-2 bottom-link-item">
|
|
<div class="card-body p-3">
|
|
<div class="row g-2 align-items-center">
|
|
<div class="col-md-5">
|
|
<label class="form-label small">Label</label>
|
|
<input type="text" class="form-control form-control-sm" data-field="label"
|
|
value="${esc(item.label)}">
|
|
</div>
|
|
<div class="col-md-5">
|
|
<label class="form-label small">Href</label>
|
|
<input type="text" class="form-control form-control-sm" data-field="href"
|
|
value="${esc(item.href)}">
|
|
</div>
|
|
<div class="col-md-2 d-flex align-items-end">
|
|
<button type="button" class="btn btn-link text-danger btn-sm p-0"
|
|
onclick="this.closest('.bottom-link-item').remove()">
|
|
<i class="fas fa-trash"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>`);
|
|
}
|
|
|
|
function populateBottomLinks(items) {
|
|
document.getElementById('bottomLinksContainer').innerHTML = '';
|
|
items.forEach(item => addBottomLink(item));
|
|
}
|
|
|
|
// ── Utilities ────────────────────────────────────────────────────────────
|
|
function esc(val) {
|
|
if (!val) return '';
|
|
return String(val).replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>');
|
|
}
|
|
|
|
function resetForm() {
|
|
if (confirm('Reset all changes to last saved state?')) {
|
|
populateAll(originalFormData);
|
|
}
|
|
}
|
|
|
|
function openImageUploader(targetInput, imageType) {
|
|
let fileInput = document.getElementById('__globalFileInput');
|
|
if (!fileInput) {
|
|
fileInput = document.createElement('input');
|
|
fileInput.type = 'file';
|
|
fileInput.accept = 'image/*';
|
|
fileInput.id = '__globalFileInput';
|
|
fileInput.style.cssText = 'position:fixed;top:-9999px;left:-9999px;opacity:0;width:1px;height:1px;';
|
|
document.body.appendChild(fileInput);
|
|
}
|
|
|
|
fileInput.value = '';
|
|
|
|
fileInput.onchange = async function (e) {
|
|
const file = e.target.files[0];
|
|
if (!file) return;
|
|
|
|
const uploadBtn = document.querySelector(`[onclick*="${targetInput}"]`);
|
|
if (uploadBtn) uploadBtn.disabled = true;
|
|
|
|
try {
|
|
const formData = new FormData();
|
|
formData.append('image', file);
|
|
|
|
const response = await fetch(`/admin/upload/image?imageType=${imageType}`, {
|
|
method: 'POST',
|
|
body: formData,
|
|
});
|
|
if (!response.ok) throw new Error('Upload failed');
|
|
const result = await response.json();
|
|
if (!result.success) throw new Error(result.error || 'Upload failed');
|
|
|
|
const input = document.getElementById(targetInput);
|
|
if (input) {
|
|
input.value = result.path;
|
|
const previewUrl = result.path.startsWith('http')
|
|
? result.path
|
|
: window.location.origin + result.path;
|
|
let preview = input.closest('.col-md-6, .col-md-12')?.querySelector('.uploaded-preview');
|
|
if (preview) {
|
|
preview.src = previewUrl;
|
|
} else {
|
|
const img = document.createElement('img');
|
|
img.src = previewUrl;
|
|
img.className = 'img-thumbnail uploaded-preview mt-2';
|
|
img.style.maxHeight = '80px';
|
|
input.closest('.input-group')?.insertAdjacentElement('afterend', img);
|
|
}
|
|
}
|
|
} catch (err) {
|
|
console.error('Upload error:', err);
|
|
alert('Upload failed: ' + err.message);
|
|
} finally {
|
|
if (uploadBtn) uploadBtn.disabled = false;
|
|
fileInput.value = '';
|
|
}
|
|
};
|
|
|
|
fileInput.click();
|
|
}
|
|
</script> |