Files
cms.uldp.edu.vn/views/admin/contact/index.ejs
r2xrzh9q2z-lab d1b931d547 first commit
2026-02-02 11:07:09 +07:00

1124 lines
70 KiB
Plaintext
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.

<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 on Contact Us page</p>
</div>
<div>
<a href="<%= frontendUrl %>/contact-us/" class="btn btn-outline-primary" target="_blank">
<i class="fas fa-external-link-alt me-2"></i>View Contact Us Page
</a>
</div>
</div>
<div class="row">
<div class="col-12">
<form method="POST" class="content-with-fixed-buttons" id="contactForm"
action="/admin/contact/update">
<!-- Hidden inputs for JSON data -->
<input type="hidden" name="hero" id="heroJson">
<input type="hidden" name="contactCards" id="contactCardsJson">
<input type="hidden" name="map" id="mapJson">
<input type="hidden" name="form" id="formJson">
<!-- Navigation 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" role="tablist">
<li class="nav-item">
<a class="nav-link active" data-bs-toggle="tab" href="#hero" role="tab">
<i class="fas fa-home me-2"></i>Hero
</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" href="#contactCards" role="tab">
<i class="fas fa-address-card me-2"></i>Contact Cards
</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" href="#map" role="tab">
<i class="fas fa-map-marker-alt me-2"></i>Map
</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" href="#form" role="tab">
<i class="fas fa-envelope me-2"></i>Form
</a>
</li>
</ul>
</div>
<div class="card-body">
<div class="tab-content">
<!-- Hero Tab -->
<div class="tab-pane fade show active" id="hero" role="tabpanel">
<div class="card border shadow-sm">
<div class="card-body">
<div class="row">
<div class="col-md-5">
<label class="form-label fw-medium">Background Image</label>
<div class="input-group mb-2">
<input type="text" class="form-control" id="heroBackgroundImage"
name="heroBackgroundImage" value="<%= data.hero?.backgroundImage || '' %>">
<button type="button"
class="btn btn-outline-primary btn-upload-image"
data-target-input="heroBackgroundImage" data-image-type="contact">
<i class="fas fa-upload me-1"></i>Upload
</button>
</div>
<small class="text-muted">Recommended size: 1920x1080px</small>
</div>
<div class="col-md-7">
<div id="heroImagePreview" style="height: 300px;">
<% if (data.hero?.backgroundImage) { %>
<%
let heroImgSrc = data.hero.backgroundImage;
if (heroImgSrc && !heroImgSrc.startsWith('http://') && !heroImgSrc.startsWith('https://')) {
heroImgSrc = heroImgSrc.startsWith('/') ? heroImgSrc : '/' + heroImgSrc;
}
%>
<img src="<%= heroImgSrc %>" class="img-thumbnail" id="heroPreviewImg"
style="height: 300px; width: 100%; object-fit: cover;"
alt="Background image preview" onerror="this.style.display='none'; this.nextElementSibling.style.display='flex';">
<div class="border rounded p-5 text-center text-muted" style="height: 300px; display: none; align-items: center; justify-content: center;">
Image preview
</div>
<% } else { %>
<div class="border rounded p-5 text-center text-muted" style="height: 300px; display: flex; align-items: center; justify-content: center;">
Image preview
</div>
<% } %>
</div>
</div>
</div>
<div class="row mt-3">
<div class="col-md-12">
<label class="form-label fw-medium">Title</label>
<input type="text" class="form-control" id="heroTitle" name="heroTitle"
value="<%= data.hero?.title || '' %>">
</div>
</div>
<!-- Hidden field for overlayColor - keep default value -->
<input type="hidden" id="heroOverlayColor" value="<%= data.hero?.overlayColor || 'rgba(0, 0, 0, 0)' %>">
<!-- Hidden fields for class values - keep in data but not editable -->
<input type="hidden" id="heroSectionClass" value="<%= data.hero?.sectionClass || '' %>">
<input type="hidden" id="heroTitleClass" value="<%= data.hero?.titleClass || '' %>">
<input type="hidden" id="heroBackgroundPosition" value="<%= data.hero?.backgroundPosition || 'center' %>">
<input type="hidden" id="heroEnableScrollspy" value="<%= data.hero?.enableScrollspy ? 'true' : 'false' %>">
</div>
</div>
</div>
<!-- Contact Cards Tab -->
<div class="tab-pane fade" id="contactCards" role="tabpanel">
<div class="card border shadow-sm">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-3">
<h6 class="fw-medium mb-0">Contact Cards</h6>
<button type="button" class="btn btn-primary btn-sm" onclick="addContactCard()">
<i class="fas fa-plus"></i> Add Card
</button>
</div>
<div id="contactCardsContainer">
<% if (data.contactCards && data.contactCards.length > 0) { %>
<% data.contactCards.forEach((card, index) => { %>
<%
const iconSource = card.iconSource || (card.iconType && card.iconType.startsWith('/uploads/') ? 'image' : 'fontawesome');
const isImageIcon = iconSource === 'image';
const faIconValue = !isImageIcon ? (card.iconType || '') : '';
const imageIconValue = isImageIcon ? (card.iconType || '') : '';
%>
<div class="card mb-3 contact-card-item">
<div class="card-body">
<div class="row g-3">
<div class="col-md-3">
<label class="form-label">Type</label>
<select class="form-select card-type-select" name="cardType_<%= index %>" data-index="<%= index %>">
<option value="phone" <%= card.type === 'phone' ? 'selected' : '' %>>Phone</option>
<option value="email" <%= card.type === 'email' ? 'selected' : '' %>>Email</option>
<option value="location" <%= card.type === 'location' ? 'selected' : '' %>>Location</option>
<option value="hours" <%= card.type === 'hours' ? 'selected' : '' %>>Hours</option>
<option value="website" <%= card.type === 'website' ? 'selected' : '' %>>Website</option>
<option value="social" <%= card.type === 'social' ? 'selected' : '' %>>Social Media</option>
<option value="custom" <%= card.type === 'custom' ? 'selected' : '' %>>Custom</option>
</select>
</div>
<div class="col-md-6">
<label class="form-label">Title</label>
<input type="text" class="form-control" name="cardTitle_<%= index %>"
value="<%= card.title || '' %>">
</div>
<div class="col-md-12">
<label class="form-label">Icon Type</label>
<div class="btn-group w-100 mb-3" role="group">
<input type="radio" class="btn-check icon-source-radio" name="cardIconSource_<%= index %>"
id="iconSource_fa_<%= index %>" value="fontawesome"
<%= iconSource === 'fontawesome' ? 'checked' : '' %>
data-index="<%= index %>" onchange="handleIconSourceChange(this)">
<label class="btn btn-outline-primary" for="iconSource_fa_<%= index %>">
<i class="fas fa-icons me-2"></i>Font Awesome
</label>
<input type="radio" class="btn-check icon-source-radio" name="cardIconSource_<%= index %>"
id="iconSource_img_<%= index %>" value="image"
<%= iconSource === 'image' ? 'checked' : '' %>
data-index="<%= index %>" onchange="handleIconSourceChange(this)">
<label class="btn btn-outline-primary" for="iconSource_img_<%= index %>">
<i class="fas fa-image me-2"></i>Upload Image
</label>
</div>
<!-- Font Awesome Icon Selector -->
<div class="icon-fa-container" data-index="<%= index %>" style="display: <%= iconSource === 'fontawesome' ? 'block' : 'none' %>;">
<label class="form-label">Select Font Awesome Icon</label>
<select class="form-select fa-icon-select" name="cardFaIcon_<%= index %>" data-index="<%= index %>">
<option value="">-- Select Icon --</option>
<option value="fas fa-phone" <%= faIconValue === 'fas fa-phone' ? 'selected' : '' %>>📞 Phone</option>
<option value="fas fa-envelope" <%= faIconValue === 'fas fa-envelope' ? 'selected' : '' %>>✉️ Email</option>
<option value="fas fa-map-marker-alt" <%= faIconValue === 'fas fa-map-marker-alt' ? 'selected' : '' %>>📍 Location</option>
<option value="fas fa-clock" <%= faIconValue === 'fas fa-clock' ? 'selected' : '' %>>🕐 Clock</option>
<option value="fas fa-globe" <%= faIconValue === 'fas fa-globe' ? 'selected' : '' %>>🌐 Website</option>
<option value="fas fa-calendar" <%= faIconValue === 'fas fa-calendar' ? 'selected' : '' %>>📅 Calendar</option>
<option value="fas fa-user" <%= faIconValue === 'fas fa-user' ? 'selected' : '' %>>👤 User</option>
<option value="fas fa-users" <%= faIconValue === 'fas fa-users' ? 'selected' : '' %>>👥 Users</option>
<option value="fas fa-building" <%= faIconValue === 'fas fa-building' ? 'selected' : '' %>>🏢 Building</option>
<option value="fas fa-home" <%= faIconValue === 'fas fa-home' ? 'selected' : '' %>>🏠 Home</option>
<option value="fab fa-facebook" <%= faIconValue === 'fab fa-facebook' ? 'selected' : '' %>>📘 Facebook</option>
<option value="fab fa-twitter" <%= faIconValue === 'fab fa-twitter' ? 'selected' : '' %>>🐦 Twitter</option>
<option value="fab fa-instagram" <%= faIconValue === 'fab fa-instagram' ? 'selected' : '' %>>📷 Instagram</option>
<option value="fab fa-linkedin" <%= faIconValue === 'fab fa-linkedin' ? 'selected' : '' %>>💼 LinkedIn</option>
<option value="fab fa-youtube" <%= faIconValue === 'fab fa-youtube' ? 'selected' : '' %>>📺 YouTube</option>
<option value="fas fa-mobile-alt" <%= faIconValue === 'fas fa-mobile-alt' ? 'selected' : '' %>>📱 Mobile</option>
<option value="fas fa-fax" <%= faIconValue === 'fas fa-fax' ? 'selected' : '' %>>📠 Fax</option>
<option value="fas fa-headset" <%= faIconValue === 'fas fa-headset' ? 'selected' : '' %>>🎧 Support</option>
<option value="fas fa-info-circle" <%= faIconValue === 'fas fa-info-circle' ? 'selected' : '' %>> Info</option>
</select>
<small class="text-muted">Choose a Font Awesome icon from the list</small>
<div class="mt-2 fa-icon-preview" data-index="<%= index %>">
<% if (faIconValue) { %>
<i class="<%= faIconValue %> fa-2x text-primary"></i>
<% } %>
</div>
</div>
<!-- Upload Image -->
<div class="icon-image-container" data-index="<%= index %>" style="display: <%= iconSource === 'image' ? 'block' : 'none' %>;">
<label class="form-label">Upload Icon Image</label>
<div class="input-group">
<input type="text" class="form-control card-icon-image-input" name="cardIconImage_<%= index %>"
value="<%= imageIconValue %>" placeholder="/uploads/icon.png">
<button type="button" class="btn btn-outline-primary btn-upload-image"
data-target-input="cardIconImage_<%= index %>" data-image-type="contact">
<i class="fas fa-upload me-1"></i>Upload Icon
</button>
</div>
<% if (imageIconValue) { %>
<img src="<%= imageIconValue %>" class="img-thumbnail mt-2 icon-image-preview" data-index="<%= index %>" style="max-height: 100px; width: auto;" alt="Icon preview">
<% } else { %>
<img src="" class="img-thumbnail mt-2 icon-image-preview" data-index="<%= index %>" style="max-height: 100px; width: auto; display: none;" alt="Icon preview">
<% } %>
<small class="text-muted">Upload a custom icon image for this contact card</small>
</div>
</div>
<div class="col-md-12">
<label class="form-label">Content (one per line)</label>
<textarea class="form-control" name="cardContent_<%= index %>" rows="3"><%= (card.content || []).join('\n') %></textarea>
<small class="text-muted">Enter each content item on a new line</small>
</div>
</div>
<button type="button"
class="btn btn-outline-danger btn-sm mt-3"
onclick="removeContactCard(this)">
<i class="fas fa-trash me-2"></i>Remove Card
</button>
</div>
</div>
<% }); %>
<% } %>
</div>
</div>
</div>
</div>
<!-- Map Tab -->
<div class="tab-pane fade" id="map" role="tabpanel">
<div class="card border shadow-sm">
<div class="card-body">
<h6 class="fw-medium mb-3">Map Settings</h6>
<div class="row g-3">
<div class="col-md-6">
<label class="form-label">Marker Title</label>
<input type="text" class="form-control" id="mapMarkerTitle"
value="<%= data.map?.markerTitle || '' %>" placeholder="e.g., Our Office">
</div>
<div class="col-md-6">
<label class="form-label">Location</label>
<input type="text" class="form-control" id="mapLocation"
value="<%= data.map?.location || '' %>" placeholder="e.g., 123 Main St, City, Country">
<small class="text-muted">Enter address - map will be automatically shown</small>
</div>
<div class="col-md-12">
<div id="mapPreview" style="height: 400px; border: 1px solid #ddd; border-radius: 4px; margin-top: 15px; background: #f5f5f5; display: flex; align-items: center; justify-content: center; color: #999;">
<% if (data.map?.location && data.map?.coordinates?.lat && data.map?.coordinates?.lng) { %>
<%
const lat = data.map.coordinates.lat;
const lng = data.map.coordinates.lng;
const zoom = data.map.zoom || 15;
const markerTitle = data.map.markerTitle || data.map.location;
// Calculate bbox for proper zoom - smaller bbox = more zoomed in
const zoomDelta = {
10: 0.1, 11: 0.05, 12: 0.025, 13: 0.0125, 14: 0.006,
15: 0.003, 16: 0.0015, 17: 0.00075, 18: 0.000375
};
const delta = zoomDelta[zoom] || 0.003;
const latDelta = delta;
const lngDelta = delta * 1.5;
%>
<iframe
width="100%"
height="100%"
style="border:0; border-radius: 4px;"
src="https://www.openstreetmap.org/export/embed.html?bbox=<%= lng-lngDelta %>,<%= lat-latDelta %>,<%= lng+lngDelta %>,<%= lat+latDelta %>&layer=mapnik&marker=<%= lat %>,<%= lng %>"
frameborder="0">
</iframe>
<div class="mt-2 text-center">
<small class="text-muted">📍 <%= markerTitle %></small>
</div>
<% } else { %>
Enter location above to see map preview
<% } %>
</div>
</div>
</div>
<!-- Hidden fields for map settings - keep defaults -->
<input type="hidden" id="mapLat" value="<%= data.map?.coordinates?.lat || 0 %>">
<input type="hidden" id="mapLng" value="<%= data.map?.coordinates?.lng || 0 %>">
<input type="hidden" id="mapZoom" value="<%= data.map?.zoom || 15 %>">
<input type="hidden" id="tileLayerUrl" value="<%= data.map?.tileLayer?.url || 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png' %>">
<input type="hidden" id="tileLayerAttribution" value="<%= data.map?.tileLayer?.attribution || '' %>">
<input type="hidden" id="tileLayerMaxZoom" value="<%= data.map?.tileLayer?.maxZoom || 18 %>">
<input type="hidden" id="tileLayerMinZoom" value="<%= data.map?.tileLayer?.minZoom || 0 %>">
</div>
</div>
</div>
<!-- Form Tab -->
<div class="tab-pane fade" id="form" role="tabpanel">
<div class="card border shadow-sm">
<div class="card-body">
<h6 class="fw-medium mb-3">Form Settings</h6>
<div class="row g-3">
<div class="col-md-6">
<label class="form-label">Section Label</label>
<input type="text" class="form-control" id="formSectionLabel"
value="<%= data.form?.sectionLabel || '' %>">
</div>
<div class="col-md-6">
<label class="form-label">Submit Button Text</label>
<input type="text" class="form-control" id="formSubmitButtonText"
value="<%= data.form?.submitButton?.text || 'Send Message' %>">
</div>
<div class="col-md-12">
<label class="form-label">Heading</label>
<input type="text" class="form-control" id="formHeading"
value="<%= data.form?.heading || '' %>">
</div>
</div>
<hr class="my-4">
<div class="d-flex justify-content-between align-items-center mb-3">
<h6 class="fw-medium mb-0">Form Fields</h6>
<button type="button" class="btn btn-primary btn-sm" onclick="addFormField()">
<i class="fas fa-plus"></i> Add Field
</button>
</div>
<div id="formFieldsContainer">
<% if (data.form?.fields && data.form.fields.length > 0) { %>
<% data.form.fields.forEach((field, index) => { %>
<div class="card mb-3 form-field-item">
<div class="card-body">
<div class="row g-3">
<div class="col-md-3">
<label class="form-label">Label</label>
<input type="text" class="form-control" name="fieldName_<%= index %>"
value="<%= field.name || '' %>" placeholder="e.g., Your Name">
</div>
<div class="col-md-3">
<label class="form-label">Field Type</label>
<select class="form-select field-type-select" name="fieldType_<%= index %>" data-index="<%= index %>" onchange="handleFieldTypeChange(this)">
<option value="text" <%= field.type === 'text' ? 'selected' : '' %>>Text</option>
<option value="email" <%= field.type === 'email' ? 'selected' : '' %>>Email</option>
<option value="tel" <%= field.type === 'tel' ? 'selected' : '' %>>Tel</option>
<option value="textarea" <%= field.type === 'textarea' ? 'selected' : '' %>>Textarea</option>
<option value="programme" <%= field.type === 'programme' ? 'selected' : '' %>>Programme</option>
</select>
</div>
<div class="col-md-4">
<label class="form-label">Placeholder</label>
<input type="text" class="form-control" name="fieldPlaceholder_<%= index %>"
value="<%= field.placeholder || '' %>">
</div>
<div class="col-md-2">
<label class="form-label">Required</label>
<div class="form-check mt-2">
<input class="form-check-input" type="checkbox" name="fieldRequired_<%= index %>"
<%= field.required ? 'checked' : '' %>>
</div>
</div>
<div class="col-md-3 programme-name-field" style="display: <%= field.type === 'programme' ? 'block' : 'none' %>;">
<label class="form-label">Programme Name</label>
<input type="text" class="form-control" name="fieldProgrammeName_<%= index %>"
value="<%= field.programmeName || '' %>" placeholder="e.g., Summer Camp 2024">
<small class="text-muted">Internal name for the programme</small>
</div>
</div>
<button type="button"
class="btn btn-outline-danger btn-sm mt-3"
onclick="removeFormField(this)">
<i class="fas fa-trash me-2"></i>Remove Field
</button>
</div>
</div>
<% }); %>
<% } %>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Fixed Bottom Buttons -->
<div class="fixed-bottom-buttons">
<button type="reset" class="btn btn-secondary" onclick="resetForm()">
<i class="fas fa-undo me-2"></i>Reset
</button>
<button type="submit" class="btn btn-primary" id="submitBtn">
<i class="fas fa-save me-2"></i>Save Changes
</button>
</div>
</form>
</div>
</div>
</div>
<script>
let originalFormData = null;
document.addEventListener('DOMContentLoaded', function () {
originalFormData = <%- JSON.stringify(data) %>;
updateAllJsonInputs(originalFormData);
initializeFormHandlers();
});
function initializeFormHandlers() {
const form = document.getElementById('contactForm');
form.addEventListener('submit', async function (e) {
e.preventDefault();
const submitBtn = document.getElementById('submitBtn');
submitBtn.disabled = true;
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i>Saving...';
try {
updateJsonData();
this.submit();
} catch (error) {
console.error('Error updating data:', error);
alert('Failed to process form data. Please try again.');
submitBtn.disabled = false;
submitBtn.innerHTML = '<i class="fas fa-save me-2"></i>Save Changes';
}
});
document.querySelectorAll('.btn-upload-image').forEach(button => {
button.addEventListener('click', function () {
const targetInput = this.dataset.targetInput;
const imageType = this.dataset.imageType;
openImageUploader(targetInput, imageType);
});
});
// Initialize map location geocoding
const mapLocationInput = document.getElementById('mapLocation');
if (mapLocationInput) {
let geocodeTimeout;
mapLocationInput.addEventListener('input', function() {
clearTimeout(geocodeTimeout);
const location = this.value.trim();
if (!location) {
document.getElementById('mapPreview').innerHTML = 'Enter location above to see map preview';
return;
}
geocodeTimeout = setTimeout(() => {
geocodeLocation(location);
}, 800); // Wait 800ms after user stops typing
});
// Show map immediately if coordinates already exist
const mapLat = document.getElementById('mapLat');
const mapLng = document.getElementById('mapLng');
if (mapLat && mapLng && mapLat.value && mapLng.value && parseFloat(mapLat.value) !== 0 && parseFloat(mapLng.value) !== 0) {
// Coordinates already exist, show map immediately
showMapPreview(parseFloat(mapLat.value), parseFloat(mapLng.value));
} else if (mapLocationInput.value) {
// Only geocode if coordinates don't exist
setTimeout(() => geocodeLocation(mapLocationInput.value), 500);
}
}
}
function showMapPreview(lat, lng, address = null) {
const mapPreview = document.getElementById('mapPreview');
if (!mapPreview) return;
const zoom = parseInt(document.getElementById('mapZoom')?.value) || 15;
const markerTitle = document.getElementById('mapMarkerTitle')?.value || address || 'Location';
// Calculate bbox for proper zoom - smaller bbox = more zoomed in
// For zoom level 15, use approximately 0.01 degree = ~1km
const zoomDelta = {
10: 0.1,
11: 0.05,
12: 0.025,
13: 0.0125,
14: 0.006,
15: 0.003,
16: 0.0015,
17: 0.00075,
18: 0.000375
};
const delta = zoomDelta[zoom] || 0.003;
const latDelta = delta;
const lngDelta = delta * 1.5; // Adjust for latitude
mapPreview.innerHTML = `
<iframe
width="100%"
height="100%"
style="border:0; border-radius: 4px;"
src="https://www.openstreetmap.org/export/embed.html?bbox=${lng-lngDelta},${lat-latDelta},${lng+lngDelta},${lat+latDelta}&layer=mapnik&marker=${lat},${lng}"
frameborder="0">
</iframe>
<div class="mt-2 text-center">
<small class="text-muted">📍 ${markerTitle}</small>
</div>
`;
}
async function geocodeLocation(address) {
const mapPreview = document.getElementById('mapPreview');
if (!mapPreview) return;
try {
mapPreview.innerHTML = '<div class="text-center p-4"><i class="fas fa-spinner fa-spin me-2"></i>Finding location...</div>';
// Use Nominatim API (free, no key required)
const response = await fetch(`https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent(address)}&limit=1`);
const data = await response.json();
if (data && data.length > 0) {
const result = data[0];
const lat = parseFloat(result.lat);
const lng = parseFloat(result.lon);
// Update hidden fields
document.getElementById('mapLat').value = lat;
document.getElementById('mapLng').value = lng;
// Show map preview with proper zoom
showMapPreview(lat, lng, address);
} else {
mapPreview.innerHTML = '<div class="text-center p-4 text-danger"><i class="fas fa-exclamation-triangle me-2"></i>Location not found. Please try a more specific address.</div>';
}
} catch (error) {
console.error('Geocoding error:', error);
mapPreview.innerHTML = '<div class="text-center p-4 text-danger"><i class="fas fa-exclamation-triangle me-2"></i>Error finding location. Please try again.</div>';
}
}
function openImageUploader(targetInput, imageType) {
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.accept = 'image/*';
fileInput.style.display = 'none';
document.body.appendChild(fileInput);
fileInput.onchange = async function (e) {
const file = e.target.files[0];
if (!file) return;
try {
const formData = new FormData();
formData.append('image', file);
const uploadBtn = document.querySelector(`[data-target-input="${targetInput}"]`);
const originalBtnHtml = uploadBtn.innerHTML;
uploadBtn.disabled = true;
uploadBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Uploading...';
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) || document.querySelector(`[name="${targetInput}"]`);
if (!input) {
throw new Error('Target input not found');
}
const previewUrl = (result.path && (result.path.startsWith('http://') || result.path.startsWith('https://'))) ? result.path : (window.location.origin + result.path);
input.value = result.path;
// Handle hero image preview
if (targetInput === 'heroBackgroundImage') {
const heroPreview = document.getElementById('heroImagePreview');
if (heroPreview) {
let previewImg = document.getElementById('heroPreviewImg');
if (previewImg) {
previewImg.src = previewUrl;
previewImg.style.display = 'block';
previewImg.nextElementSibling.style.display = 'none';
} else {
heroPreview.innerHTML = `
<img src="${previewUrl}" class="img-thumbnail" id="heroPreviewImg"
style="height: 300px; width: 100%; object-fit: cover;"
alt="Background image preview">
`;
}
}
} else {
// Handle contact card icon image previews
if (targetInput.startsWith('cardIconImage_')) {
const index = targetInput.replace('cardIconImage_', '');
const previewImg = document.querySelector(`.icon-image-preview[data-index="${index}"]`);
if (previewImg) {
previewImg.src = previewUrl;
previewImg.style.display = 'block';
}
} else {
// Handle other image previews
let card = input.closest('.card');
let previewImg = card ? card.querySelector('img') : null;
if (!previewImg) {
const parent = input.parentElement || input.closest('.input-group') || input.closest('div');
previewImg = parent ? parent.querySelector('.uploaded-preview') : null;
}
if (previewImg) {
previewImg.src = previewUrl;
previewImg.style.display = 'block';
} else {
const img = document.createElement('img');
img.src = previewUrl;
img.className = 'img-thumbnail uploaded-preview mt-2';
img.style.height = '150px';
img.style.width = '100%';
img.style.objectFit = 'cover';
const parent = input.parentElement || input.closest('.input-group') || input.closest('div');
if (parent) parent.appendChild(img);
}
}
}
showToast('Success', 'Image uploaded successfully', 'success');
uploadBtn.disabled = false;
uploadBtn.innerHTML = originalBtnHtml;
} catch (error) {
console.error('Upload error:', error);
showToast('Error', 'Failed to upload image: ' + error.message, 'error');
const uploadBtn = document.querySelector(`[data-target-input="${targetInput}"]`);
if (uploadBtn) {
uploadBtn.disabled = false;
uploadBtn.innerHTML = uploadBtn.innerHTML.replace('Uploading...', 'Upload');
}
} finally {
document.body.removeChild(fileInput);
}
};
fileInput.click();
}
function resetForm() {
if (confirm('Are you sure you want to reset all changes?')) {
updateAllJsonInputs(originalFormData);
location.reload();
}
}
function showToast(title, message, type = 'info') {
const toast = document.createElement('div');
toast.className = `toast align-items-center text-white bg-${type === 'error' ? 'danger' : type} border-0`;
toast.setAttribute('role', 'alert');
toast.setAttribute('aria-live', 'assertive');
toast.setAttribute('aria-atomic', 'true');
toast.innerHTML = `
<div class="d-flex">
<div class="toast-body">
<strong>${title}:</strong> ${message}
</div>
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
`;
let container = document.querySelector('.toast-container');
if (!container) {
container = document.createElement('div');
container.className = 'toast-container position-fixed top-0 end-0 p-3';
document.body.appendChild(container);
}
container.appendChild(toast);
const bsToast = new bootstrap.Toast(toast, {
animation: true,
autohide: true,
delay: 3000
});
bsToast.show();
toast.addEventListener('hidden.bs.toast', () => {
toast.remove();
});
}
function updateAllJsonInputs(data) {
document.getElementById('heroJson').value = JSON.stringify(data.hero || {});
document.getElementById('contactCardsJson').value = JSON.stringify(data.contactCards || []);
document.getElementById('mapJson').value = JSON.stringify(data.map || {});
document.getElementById('formJson').value = JSON.stringify(data.form || {});
populateContactCardsFromData(data.contactCards || []);
populateFormFieldsFromData(data.form?.fields || []);
}
function addContactCard() {
const container = document.getElementById('contactCardsContainer');
const index = container.children.length;
const html = `
<div class="card mb-3 contact-card-item">
<div class="card-body">
<div class="row g-3">
<div class="col-md-3">
<label class="form-label">Type</label>
<select class="form-select card-type-select" name="cardType_${index}" data-index="${index}">
<option value="phone">Phone</option>
<option value="email">Email</option>
<option value="location">Location</option>
<option value="hours">Hours</option>
<option value="website">Website</option>
<option value="social">Social Media</option>
<option value="custom">Custom</option>
</select>
</div>
<div class="col-md-6">
<label class="form-label">Title</label>
<input type="text" class="form-control" name="cardTitle_${index}">
</div>
<div class="col-md-12">
<label class="form-label">Icon Type</label>
<div class="btn-group w-100 mb-3" role="group">
<input type="radio" class="btn-check icon-source-radio" name="cardIconSource_${index}"
id="iconSource_fa_${index}" value="fontawesome" checked
data-index="${index}" onchange="handleIconSourceChange(this)">
<label class="btn btn-outline-primary" for="iconSource_fa_${index}">
<i class="fas fa-icons me-2"></i>Font Awesome
</label>
<input type="radio" class="btn-check icon-source-radio" name="cardIconSource_${index}"
id="iconSource_img_${index}" value="image"
data-index="${index}" onchange="handleIconSourceChange(this)">
<label class="btn btn-outline-primary" for="iconSource_img_${index}">
<i class="fas fa-image me-2"></i>Upload Image
</label>
</div>
<!-- Font Awesome Icon Selector -->
<div class="icon-fa-container" data-index="${index}">
<label class="form-label">Select Font Awesome Icon</label>
<select class="form-select fa-icon-select" name="cardFaIcon_${index}" data-index="${index}" onchange="updateFaIconPreview(this)">
<option value="">-- Select Icon --</option>
<option value="fas fa-phone">📞 Phone</option>
<option value="fas fa-envelope">✉️ Email</option>
<option value="fas fa-map-marker-alt">📍 Location</option>
<option value="fas fa-clock">🕐 Clock</option>
<option value="fas fa-globe">🌐 Website</option>
<option value="fas fa-calendar">📅 Calendar</option>
<option value="fas fa-user">👤 User</option>
<option value="fas fa-users">👥 Users</option>
<option value="fas fa-building">🏢 Building</option>
<option value="fas fa-home">🏠 Home</option>
<option value="fab fa-facebook">📘 Facebook</option>
<option value="fab fa-twitter">🐦 Twitter</option>
<option value="fab fa-instagram">📷 Instagram</option>
<option value="fab fa-linkedin">💼 LinkedIn</option>
<option value="fab fa-youtube">📺 YouTube</option>
<option value="fas fa-mobile-alt">📱 Mobile</option>
<option value="fas fa-fax">📠 Fax</option>
<option value="fas fa-headset">🎧 Support</option>
<option value="fas fa-info-circle"> Info</option>
</select>
<small class="text-muted">Choose a Font Awesome icon from the list</small>
<div class="mt-2 fa-icon-preview" data-index="${index}"></div>
</div>
<!-- Upload Image -->
<div class="icon-image-container" data-index="${index}" style="display: none;">
<label class="form-label">Upload Icon Image</label>
<div class="input-group">
<input type="text" class="form-control card-icon-image-input" name="cardIconImage_${index}"
placeholder="/uploads/icon.png">
<button type="button" class="btn btn-outline-primary btn-upload-image"
data-target-input="cardIconImage_${index}" data-image-type="contact">
<i class="fas fa-upload me-1"></i>Upload Icon
</button>
</div>
<img src="" class="img-thumbnail mt-2 icon-image-preview" data-index="${index}" style="max-height: 100px; width: auto; display: none;" alt="Icon preview">
<small class="text-muted">Upload a custom icon image for this contact card</small>
</div>
</div>
<div class="col-md-12">
<label class="form-label">Content (one per line)</label>
<textarea class="form-control" name="cardContent_${index}" rows="3"></textarea>
<small class="text-muted">Enter each content item on a new line</small>
</div>
</div>
<button type="button" class="btn btn-outline-danger btn-sm mt-3" onclick="removeContactCard(this)">
<i class="fas fa-trash me-2"></i>Remove Card
</button>
</div>
</div>
`;
container.insertAdjacentHTML('beforeend', html);
// Initialize upload button for new card
const newCard = container.lastElementChild;
const uploadBtn = newCard.querySelector('.btn-upload-image');
if (uploadBtn) {
uploadBtn.addEventListener('click', function() {
const targetInput = this.dataset.targetInput;
const imageType = this.dataset.imageType;
openImageUploader(targetInput, imageType);
});
}
}
function handleIconSourceChange(radio) {
const index = radio.dataset.index;
const source = radio.value;
const faContainer = document.querySelector(`.icon-fa-container[data-index="${index}"]`);
const imageContainer = document.querySelector(`.icon-image-container[data-index="${index}"]`);
if (source === 'fontawesome') {
if (faContainer) faContainer.style.display = 'block';
if (imageContainer) imageContainer.style.display = 'none';
} else {
if (faContainer) faContainer.style.display = 'none';
if (imageContainer) imageContainer.style.display = 'block';
}
}
function updateFaIconPreview(select) {
const index = select.dataset.index;
const iconClass = select.value;
const preview = document.querySelector(`.fa-icon-preview[data-index="${index}"]`);
if (preview) {
if (iconClass) {
preview.innerHTML = `<i class="${iconClass} fa-2x text-primary"></i>`;
} else {
preview.innerHTML = '';
}
}
}
function removeContactCard(button) {
button.closest('.contact-card-item').remove();
}
function addFormField() {
const container = document.getElementById('formFieldsContainer');
const index = container.children.length;
const html = `
<div class="card mb-3 form-field-item">
<div class="card-body">
<div class="row g-3">
<div class="col-md-3">
<label class="form-label">Label</label>
<input type="text" class="form-control" name="fieldName_${index}" placeholder="e.g., Your Name">
</div>
<div class="col-md-3">
<label class="form-label">Field Type</label>
<select class="form-select field-type-select" name="fieldType_${index}" data-index="${index}" onchange="handleFieldTypeChange(this)">
<option value="text">Text</option>
<option value="email">Email</option>
<option value="tel">Tel</option>
<option value="textarea">Textarea</option>
<option value="programme">Programme</option>
</select>
</div>
<div class="col-md-4">
<label class="form-label">Placeholder</label>
<input type="text" class="form-control" name="fieldPlaceholder_${index}">
</div>
<div class="col-md-2">
<label class="form-label">Required</label>
<div class="form-check mt-2">
<input class="form-check-input" type="checkbox" name="fieldRequired_${index}">
</div>
</div>
<div class="col-md-3 programme-name-field" style="display: none;">
<label class="form-label">Programme Name</label>
<input type="text" class="form-control" name="fieldProgrammeName_${index}" placeholder="e.g., Summer Camp 2024">
<small class="text-muted">Internal name for the programme</small>
</div>
</div>
<button type="button" class="btn btn-outline-danger btn-sm mt-3" onclick="removeFormField(this)">
<i class="fas fa-trash me-2"></i>Remove Field
</button>
</div>
</div>
`;
container.insertAdjacentHTML('beforeend', html);
}
function removeFormField(button) {
button.closest('.form-field-item').remove();
}
function handleFieldTypeChange(selectElement) {
const index = selectElement.dataset.index;
const fieldType = selectElement.value;
const fieldItem = selectElement.closest('.form-field-item');
const labelInput = fieldItem.querySelector(`[name="fieldName_${index}"]`);
const programmeNameField = fieldItem.querySelector('.programme-name-field');
if (fieldType === 'programme') {
// Auto-generate label for programme field
if (labelInput && (!labelInput.value || labelInput.value.trim() === '')) {
labelInput.value = 'Programme that you are looking for';
}
// Show programme name field
if (programmeNameField) {
programmeNameField.style.display = 'block';
}
} else {
// Hide programme name field
if (programmeNameField) {
programmeNameField.style.display = 'none';
}
}
}
function updateJsonData() {
try {
// Hero
const heroSectionClassEl = document.getElementById('heroSectionClass');
const heroTitleClassEl = document.getElementById('heroTitleClass');
const heroEnableScrollspyEl = document.getElementById('heroEnableScrollspy');
const heroBackgroundPositionEl = document.getElementById('heroBackgroundPosition');
const heroData = {
title: (document.getElementById('heroTitle') || {}).value?.trim() || '',
backgroundImage: (document.getElementById('heroBackgroundImage') || {}).value?.trim() || '',
overlayColor: (document.getElementById('heroOverlayColor') || {}).value?.trim() || 'rgba(0, 0, 0, 0)',
sectionClass: heroSectionClassEl?.value?.trim() || (originalFormData?.hero?.sectionClass || ''),
titleClass: heroTitleClassEl?.value?.trim() || (originalFormData?.hero?.titleClass || ''),
enableScrollspy: heroEnableScrollspyEl?.value === 'true' || (originalFormData?.hero?.enableScrollspy || false),
backgroundPosition: heroBackgroundPositionEl?.value?.trim() || (originalFormData?.hero?.backgroundPosition || 'center')
};
document.getElementById('heroJson').value = JSON.stringify(heroData);
// Contact Cards
const contactCardsData = Array.from(document.querySelectorAll('.contact-card-item'))
.map((item) => {
const typeEl = item.querySelector('[name^="cardType_"]');
const iconSourceRadio = item.querySelector('.icon-source-radio:checked');
const faIconEl = item.querySelector('[name^="cardFaIcon_"]');
const imageIconEl = item.querySelector('[name^="cardIconImage_"]');
const titleEl = item.querySelector('[name^="cardTitle_"]');
const contentEl = item.querySelector('[name^="cardContent_"]');
const content = (contentEl?.value || '').split('\n').filter(line => line.trim() !== '');
const iconSource = (iconSourceRadio?.value || 'fontawesome');
let iconType = '';
if (iconSource === 'fontawesome') {
iconType = (faIconEl?.value || '').trim();
} else {
iconType = (imageIconEl?.value || '').trim();
}
return {
type: (typeEl?.value || '').trim(),
iconType: iconType,
iconSource: iconSource,
title: (titleEl?.value || '').trim(),
content: content
};
})
.filter(card => card.type !== '' || card.title !== '');
document.getElementById('contactCardsJson').value = JSON.stringify(contactCardsData);
// Map
const mapData = {
coordinates: {
lat: parseFloat((document.getElementById('mapLat') || {}).value) || 0,
lng: parseFloat((document.getElementById('mapLng') || {}).value) || 0
},
zoom: parseInt((document.getElementById('mapZoom') || {}).value) || 15,
location: (document.getElementById('mapLocation') || {}).value?.trim() || '',
markerTitle: (document.getElementById('mapMarkerTitle') || {}).value?.trim() || '',
tileLayer: {
url: (document.getElementById('tileLayerUrl') || {}).value?.trim() || '',
attribution: (document.getElementById('tileLayerAttribution') || {}).value?.trim() || '',
maxZoom: parseInt((document.getElementById('tileLayerMaxZoom') || {}).value) || 18,
minZoom: parseInt((document.getElementById('tileLayerMinZoom') || {}).value) || 0
}
};
document.getElementById('mapJson').value = JSON.stringify(mapData);
// Form
const formFieldsData = Array.from(document.querySelectorAll('.form-field-item'))
.map((item) => {
const nameEl = item.querySelector('[name^="fieldName_"]');
const typeEl = item.querySelector('[name^="fieldType_"]');
const placeholderEl = item.querySelector('[name^="fieldPlaceholder_"]');
const requiredEl = item.querySelector('[name^="fieldRequired_"]');
const programmeNameEl = item.querySelector('[name^="fieldProgrammeName_"]');
const fieldData = {
name: (nameEl?.value || '').trim(),
type: (typeEl?.value || '').trim(),
placeholder: (placeholderEl?.value || '').trim(),
required: (requiredEl?.checked || false)
};
// Add programmeName if field type is programme
if (fieldData.type === 'programme' && programmeNameEl) {
fieldData.programmeName = (programmeNameEl?.value || '').trim();
}
return fieldData;
})
.filter(field => field.name !== '');
const formData = {
sectionLabel: (document.getElementById('formSectionLabel') || {}).value?.trim() || '',
heading: (document.getElementById('formHeading') || {}).value?.trim() || '',
fields: formFieldsData,
submitButton: {
text: (document.getElementById('formSubmitButtonText') || {}).value?.trim() || 'Send Message'
}
};
document.getElementById('formJson').value = JSON.stringify(formData);
} catch (error) {
console.error('Error updating JSON data:', error);
throw new Error('Failed to process form data');
}
}
function populateContactCardsFromData(cards) {
const container = document.getElementById('contactCardsContainer');
container.innerHTML = '';
(cards || []).forEach((card, i) => {
addContactCard();
const el = container.lastElementChild;
// Set basic fields
el.querySelector(`[name="cardType_${i}"]`).value = card.type || '';
el.querySelector(`[name="cardTitle_${i}"]`).value = card.title || '';
el.querySelector(`[name="cardContent_${i}"]`).value = (card.content || []).join('\n');
// Determine icon source
const iconType = card.iconType || '';
const iconSource = card.iconSource || (iconType && iconType.startsWith('/uploads/') ? 'image' : 'fontawesome');
// Set icon source radio button
const faRadio = el.querySelector(`#iconSource_fa_${i}`);
const imgRadio = el.querySelector(`#iconSource_img_${i}`);
if (iconSource === 'image') {
if (imgRadio) imgRadio.checked = true;
handleIconSourceChange(imgRadio);
} else {
if (faRadio) faRadio.checked = true;
handleIconSourceChange(faRadio);
}
// Set icon value based on source
if (iconSource === 'fontawesome') {
const faSelect = el.querySelector(`[name="cardFaIcon_${i}"]`);
if (faSelect && iconType) {
faSelect.value = iconType;
updateFaIconPreview(faSelect);
}
} else {
const imageInput = el.querySelector(`[name="cardIconImage_${i}"]`);
if (imageInput && iconType) {
imageInput.value = iconType;
// Show preview image
const previewImg = el.querySelector(`.icon-image-preview[data-index="${i}"]`);
if (previewImg) {
const previewUrl = (iconType.startsWith('http://') || iconType.startsWith('https://'))
? iconType
: (window.location.origin + iconType);
previewImg.src = previewUrl;
previewImg.style.display = 'block';
}
}
}
});
}
function populateFormFieldsFromData(fields) {
const container = document.getElementById('formFieldsContainer');
container.innerHTML = '';
(fields || []).forEach((field, i) => {
addFormField();
const el = container.lastElementChild;
el.querySelector(`[name="fieldName_${i}"]`).value = field.name || '';
el.querySelector(`[name="fieldType_${i}"]`).value = field.type || '';
el.querySelector(`[name="fieldPlaceholder_${i}"]`).value = field.placeholder || '';
el.querySelector(`[name="fieldRequired_${i}"]`).checked = field.required || false;
// Handle programme type - show/hide programme name field and set value
if (field.type === 'programme') {
const typeSelect = el.querySelector(`[name="fieldType_${i}"]`);
if (typeSelect) {
handleFieldTypeChange(typeSelect);
}
const programmeNameInput = el.querySelector(`[name="fieldProgrammeName_${i}"]`);
if (programmeNameInput && field.programmeName) {
programmeNameInput.value = field.programmeName || '';
}
}
});
}
</script>