forked from UKSOURCE/cms.hailearning.edu.vn
1692 lines
107 KiB
Plaintext
1692 lines
107 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 on Contact Us page</p>
|
||
</div>
|
||
<div>
|
||
<a href="<%= frontendUrl %>/contact" 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>
|
||
<li class="nav-item">
|
||
<a class="nav-link" data-bs-toggle="tab" href="#submissions" role="tab">
|
||
<i class="fas fa-list me-2"></i>Submissions
|
||
</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 || '' %>"
|
||
maxlength="255" data-maxlength="255">
|
||
<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 d-block mt-1">Recommended minimum upload: 1920x700px.</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 || '' %>"
|
||
maxlength="40" data-maxlength="40">
|
||
<small class="text-muted d-block mt-1">Keep the hero title short so the centered breadcrumb stays balanced on tablet and mobile.</small>
|
||
</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 || '' %>"
|
||
maxlength="40" data-maxlength="40">
|
||
</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"
|
||
maxlength="255" data-maxlength="255">
|
||
<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 d-block mt-1">Custom icons render at about 28x28px inside a 64x64 circle. Use SVG or a square PNG/WebP at 64x64px or 128x128px.</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" maxlength="220" data-maxlength="220"><%= (card.content || []).join('\n') %></textarea>
|
||
<small class="text-muted d-block mt-1">Each line is shown inside a compact contact card. Keep it to 1-3 short lines.</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"
|
||
maxlength="48" data-maxlength="48">
|
||
</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"
|
||
maxlength="120" data-maxlength="120">
|
||
<small class="text-muted d-block mt-1">Enter a full address. This text is used for map lookup and should stay concise.</small>
|
||
</div>
|
||
<div class="col-md-12">
|
||
<label class="form-label">Google Map Embed URL</label>
|
||
<input type="text" class="form-control" id="mapEmbedUrl"
|
||
value="<%= data.map?.embedUrl || '' %>"
|
||
placeholder="https://www.google.com/maps/embed?..."
|
||
maxlength="1000" data-maxlength="1000">
|
||
<small class="text-muted d-block mt-1">Paste the Google Maps embed URL from Share -> Embed a map.</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?.embedUrl) { %>
|
||
<iframe width="100%" height="100%"
|
||
style="border:0; border-radius: 4px;"
|
||
src="<%= data.map.embedUrl %>" frameborder="0"
|
||
allowfullscreen="" loading="lazy">
|
||
</iframe>
|
||
<div class="mt-2 text-center">
|
||
<small class="text-muted">Location: <%=
|
||
data.map?.markerTitle || data.map?.location
|
||
|| 'Location' %></small>
|
||
</div>
|
||
<% } else if (data.map?.location && data.map?.coordinates?.lat
|
||
&& data.map?.coordinates?.lng) { %>
|
||
<% var lat=data.map.coordinates.lat; var
|
||
lng=data.map.coordinates.lng; var zoom=data.map.zoom ||
|
||
15; var markerTitle=data.map.markerTitle ||
|
||
data.map.location; var 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 }; var delta=zoomDelta[zoom]
|
||
|| 0.003; var latDelta=delta; var 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 || '' %>"
|
||
maxlength="32" data-maxlength="32">
|
||
<small class="text-muted d-block mt-1">Legacy label. Keep it short if you still use it in future templates.</small>
|
||
</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' %>"
|
||
maxlength="24" data-maxlength="24">
|
||
<small class="text-muted d-block mt-1">Recommended maximum: 24 characters.</small>
|
||
</div>
|
||
<div class="col-md-12">
|
||
<label class="form-label">Heading</label>
|
||
<input type="text" class="form-control" id="formHeading"
|
||
value="<%= data.form?.heading || '' %>"
|
||
maxlength="48" data-maxlength="48">
|
||
<small class="text-muted d-block mt-1">The form heading spans the full form width. Recommended maximum: 48 characters.</small>
|
||
</div>
|
||
<div class="col-md-12">
|
||
<label class="form-label">Description</label>
|
||
<textarea class="form-control" id="formDescription"
|
||
rows="2" maxlength="160" data-maxlength="160"><%= data.form?.description || '' %></textarea>
|
||
<small class="text-muted d-block mt-1">This line is centered under the form heading. Recommended maximum: 160 characters.</small>
|
||
</div>
|
||
<!-- Hidden fields for submitButton icon and buttonClass -->
|
||
<input type="hidden" id="formSubmitButtonIcon"
|
||
value="<%= data.form?.submitButton?.icon || 'fa-solid fa-arrow-right' %>">
|
||
<input type="hidden" id="formSubmitButtonClass"
|
||
value="<%= data.form?.submitButton?.buttonClass || 'theme-btn style-2' %>">
|
||
</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"
|
||
maxlength="32" data-maxlength="32">
|
||
<small class="text-muted d-block mt-1">Keep field labels short for the stacked mobile form.</small>
|
||
</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 || '' %>"
|
||
maxlength="72" data-maxlength="72">
|
||
</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"
|
||
maxlength="48" data-maxlength="48">
|
||
<small class="text-muted d-block mt-1">Internal programme reference only. Recommended maximum: 48 characters.</small>
|
||
</div>
|
||
<!-- Hidden fields for label and colClass -->
|
||
<input type="hidden" name="fieldLabel_<%= index %>"
|
||
value="<%= field.label || '' %>">
|
||
<input type="hidden" name="fieldColClass_<%= index %>"
|
||
value="<%= field.colClass || 'col-lg-12' %>">
|
||
</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>
|
||
|
||
<!-- Submissions Tab -->
|
||
<div class="tab-pane fade" id="submissions" 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">Recent Submissions</h6>
|
||
</div>
|
||
|
||
<!-- Date Filter -->
|
||
<div class="row g-2 mb-4 align-items-end" id="filterContainer">
|
||
<input type="hidden" id="filterTab" value="submissions">
|
||
<div class="col-md-3">
|
||
<label class="form-label small text-muted">Start Date</label>
|
||
<input type="date" class="form-control form-control-sm"
|
||
id="filterStartDate" value="<%= locals.startDate || '' %>">
|
||
</div>
|
||
<div class="col-md-3">
|
||
<label class="form-label small text-muted">End Date</label>
|
||
<input type="date" class="form-control form-control-sm"
|
||
id="filterEndDate" value="<%= locals.endDate || '' %>">
|
||
</div>
|
||
<div class="col-md-3">
|
||
<button type="button" class="btn btn-sm btn-primary w-100"
|
||
onclick="applyDateFilter()">
|
||
<i class="fas fa-filter me-1"></i> Filter
|
||
</button>
|
||
</div>
|
||
<div class="col-md-2">
|
||
<a href="/admin/contact?tab=submissions"
|
||
class="btn btn-sm btn-outline-secondary w-100">
|
||
<i class="fas fa-times me-1"></i> Clear
|
||
</a>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="table-responsive">
|
||
<table class="table table-hover align-middle">
|
||
<thead class="table-light">
|
||
<tr>
|
||
<th>Date</th>
|
||
<th>Name</th>
|
||
<th>Email</th>
|
||
<th>Phone</th>
|
||
<th>Message</th>
|
||
<th>Status</th>
|
||
<th>Action</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<% if (locals.submissions && submissions.length> 0) { %>
|
||
<% submissions.forEach(submission=> { %>
|
||
<tr>
|
||
<td>
|
||
<%= new
|
||
Date(submission.createdAt).toLocaleDateString()
|
||
%>
|
||
<%= new
|
||
Date(submission.createdAt).toLocaleTimeString([],
|
||
{hour: '2-digit' , minute:'2-digit'}) %>
|
||
</td>
|
||
<td>
|
||
<%= submission.name %>
|
||
</td>
|
||
<td><a href="mailto:<%= submission.email %>">
|
||
<%= submission.email %>
|
||
</a></td>
|
||
<td>
|
||
<%= submission.phone || '-' %>
|
||
</td>
|
||
<td>
|
||
<div title="<%= submission.message %>"
|
||
style="max-width: 300px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">
|
||
<%= submission.message %>
|
||
</div>
|
||
</td>
|
||
<td>
|
||
<% let statusClass='bg-secondary' ;
|
||
if(submission.status==='pending' )
|
||
statusClass='bg-warning text-dark' ;
|
||
if(submission.status==='read' )
|
||
statusClass='bg-info text-dark' ;
|
||
if(submission.status==='replied' )
|
||
statusClass='bg-success' ;
|
||
if(submission.status==='archived' )
|
||
statusClass='bg-secondary' ; %>
|
||
<span
|
||
class="badge <%= statusClass %> rounded-pill">
|
||
<%= submission.status %>
|
||
</span>
|
||
</td>
|
||
<td>
|
||
<button type="button"
|
||
class="btn btn-sm btn-outline-primary"
|
||
onclick="openStatusModal('<%= submission._id %>', '<%= submission.status %>')"
|
||
title="Update Status">
|
||
<i class="fas fa-edit"></i>
|
||
</button>
|
||
</td>
|
||
</tr>
|
||
<% }); %>
|
||
<% } else { %>
|
||
<tr>
|
||
<td colspan="7"
|
||
class="text-center py-4 text-muted">No
|
||
submissions found</td>
|
||
</tr>
|
||
<% } %>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<div class="mt-3 text-end">
|
||
<small class="text-muted">Showing last 50 submissions</small>
|
||
</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>
|
||
|
||
<!-- Status Update Modal -->
|
||
<div class="modal fade" id="statusModal" tabindex="-1" aria-hidden="true">
|
||
<div class="modal-dialog">
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<h5 class="modal-title">Update Status</h5>
|
||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<form id="statusForm">
|
||
<input type="hidden" id="statusSubmissionId">
|
||
<div class="mb-3">
|
||
<label for="statusSelect" class="form-label">Status</label>
|
||
<select class="form-select" id="statusSelect">
|
||
<option value="pending">Pending</option>
|
||
<option value="read">Read</option>
|
||
<option value="replied">Replied</option>
|
||
<option value="archived">Archived</option>
|
||
</select>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||
<button type="button" class="btn btn-primary" onclick="saveStatus()">Save changes</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script type="application/json" id="contactDataJson"><%- JSON.stringify(data) %></script>
|
||
|
||
<script>
|
||
let originalFormData = null;
|
||
let statusModal = null;
|
||
|
||
document.addEventListener('DOMContentLoaded', function () {
|
||
try {
|
||
var jsonScript = document.getElementById('contactDataJson');
|
||
originalFormData = JSON.parse(jsonScript.textContent);
|
||
} catch (e) {
|
||
console.error('Error parsing originalFormData:', e);
|
||
originalFormData = {};
|
||
}
|
||
|
||
// Check for tab parameter in URL
|
||
const urlParams = new URLSearchParams(window.location.search);
|
||
const tab = urlParams.get('tab');
|
||
if (tab) {
|
||
const triggerEl = document.querySelector(`a[href="#${tab}"]`);
|
||
if (triggerEl) {
|
||
const tabInstance = new bootstrap.Tab(triggerEl);
|
||
tabInstance.show();
|
||
}
|
||
}
|
||
|
||
// Move modal to body to prevent backdrop issues
|
||
const statusModalEl = document.getElementById('statusModal');
|
||
if (statusModalEl) {
|
||
document.body.appendChild(statusModalEl);
|
||
}
|
||
statusModal = new bootstrap.Modal(statusModalEl);
|
||
|
||
updateAllJsonInputs(originalFormData);
|
||
initializeFormHandlers();
|
||
initContactCharacterCounters(document);
|
||
});
|
||
|
||
function applyDateFilter() {
|
||
const startDate = document.getElementById('filterStartDate').value;
|
||
const endDate = document.getElementById('filterEndDate').value;
|
||
const url = new URL(window.location.href);
|
||
url.searchParams.set('tab', 'submissions');
|
||
|
||
if (startDate) {
|
||
url.searchParams.set('startDate', startDate);
|
||
} else {
|
||
url.searchParams.delete('startDate');
|
||
}
|
||
|
||
if (endDate) {
|
||
url.searchParams.set('endDate', endDate);
|
||
} else {
|
||
url.searchParams.delete('endDate');
|
||
}
|
||
|
||
window.location.href = url.toString();
|
||
}
|
||
|
||
function openStatusModal(id, currentStatus) {
|
||
document.getElementById('statusSubmissionId').value = id;
|
||
document.getElementById('statusSelect').value = currentStatus;
|
||
statusModal.show();
|
||
}
|
||
|
||
async function saveStatus() {
|
||
const id = document.getElementById('statusSubmissionId').value;
|
||
const status = document.getElementById('statusSelect').value;
|
||
|
||
try {
|
||
const response = await fetch(`/admin/contact/submissions/${id}`, {
|
||
method: 'PUT',
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify({ status })
|
||
});
|
||
|
||
const result = await response.json();
|
||
|
||
if (result.success) {
|
||
window.location.reload();
|
||
} else {
|
||
alert('Failed to update status: ' + (result.error || 'Unknown error'));
|
||
}
|
||
} catch (error) {
|
||
console.error('Error updating status:', error);
|
||
alert('Error updating status');
|
||
}
|
||
}
|
||
|
||
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 {
|
||
console.log('=== Before updateJsonData ===');
|
||
console.log('heroJson before:', document.getElementById('heroJson').value);
|
||
updateJsonData();
|
||
console.log('=== After updateJsonData ===');
|
||
console.log('heroJson after:', document.getElementById('heroJson').value);
|
||
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) {
|
||
renderMapPreview();
|
||
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
|
||
const savedLocation = sanitizeLocationInput(mapLocationInput.value);
|
||
showMapPreview(parseFloat(mapLat.value), parseFloat(mapLng.value), savedLocation || null);
|
||
} else if (mapLocationInput.value) {
|
||
// Only geocode if coordinates don't exist
|
||
setTimeout(() => geocodeLocation(mapLocationInput.value), 500);
|
||
}
|
||
|
||
mapLocationInput.addEventListener('blur', function () {
|
||
const cleaned = sanitizeLocationInput(this.value);
|
||
if (!cleaned && this.value.trim()) {
|
||
showToast('Warning', 'Invalid location. Please enter a readable address.', 'warning');
|
||
}
|
||
this.value = cleaned;
|
||
renderMapPreview({ address: this.value });
|
||
});
|
||
}
|
||
|
||
const mapEmbedUrlInput = document.getElementById('mapEmbedUrl');
|
||
if (mapEmbedUrlInput) {
|
||
const normalizeEmbedInput = () => {
|
||
const raw = mapEmbedUrlInput.value || '';
|
||
const normalized = extractEmbedUrl(raw);
|
||
if (normalized && normalized !== raw.trim()) {
|
||
mapEmbedUrlInput.value = normalized;
|
||
}
|
||
renderMapPreview();
|
||
};
|
||
|
||
mapEmbedUrlInput.addEventListener('paste', function () {
|
||
setTimeout(normalizeEmbedInput, 0);
|
||
});
|
||
mapEmbedUrlInput.addEventListener('blur', normalizeEmbedInput);
|
||
}
|
||
|
||
const mapMarkerTitleInput = document.getElementById('mapMarkerTitle');
|
||
if (mapMarkerTitleInput) {
|
||
mapMarkerTitleInput.addEventListener('input', function () {
|
||
renderMapPreview();
|
||
});
|
||
}
|
||
}
|
||
function showMapPreview(lat, lng, address = null) {
|
||
renderMapPreview({ lat, lng, address });
|
||
}
|
||
|
||
function renderMapPreview(options = {}) {
|
||
const mapPreview = document.getElementById('mapPreview');
|
||
if (!mapPreview) return;
|
||
|
||
const mapLat = parseFloat((document.getElementById('mapLat') || {}).value);
|
||
const mapLng = parseFloat((document.getElementById('mapLng') || {}).value);
|
||
const lat = typeof options.lat === 'number' ? options.lat : mapLat;
|
||
const lng = typeof options.lng === 'number' ? options.lng : mapLng;
|
||
const address = options.address || null;
|
||
|
||
const zoom = parseInt(document.getElementById('mapZoom')?.value) || 15;
|
||
const locationInputValue = sanitizeLocationInput(document.getElementById('mapLocation')?.value || '');
|
||
const markerTitle = document.getElementById('mapMarkerTitle')?.value || address || locationInputValue || 'Location';
|
||
const embedUrl = extractEmbedUrl(document.getElementById('mapEmbedUrl')?.value || '');
|
||
|
||
const caption = '<div class="mt-2 text-center">' +
|
||
'<small class="text-muted">Location: ' + escapeHtml(markerTitle) + '</small>' +
|
||
'</div>';
|
||
|
||
if (embedUrl) {
|
||
mapPreview.innerHTML = '<iframe ' +
|
||
'width="100%" ' +
|
||
'height="100%" ' +
|
||
'style="border:0; border-radius: 4px;" ' +
|
||
'src="' + embedUrl + '" ' +
|
||
'frameborder="0" allowfullscreen="" loading="lazy">' +
|
||
'</iframe>' +
|
||
caption;
|
||
return;
|
||
}
|
||
|
||
if (!lat || !lng) {
|
||
mapPreview.innerHTML = 'Enter location above to see map preview';
|
||
return;
|
||
}
|
||
|
||
// 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>' +
|
||
caption;
|
||
}
|
||
|
||
function escapeHtml(value) {
|
||
return String(value || '')
|
||
.replace(/&/g, '&')
|
||
.replace(/</g, '<')
|
||
.replace(/>/g, '>')
|
||
.replace(/"/g, '"')
|
||
.replace(/'/g, ''');
|
||
}
|
||
|
||
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 initContactCharacterCounters(scope = document) {
|
||
scope.querySelectorAll('.contact-limit-counter').forEach((node) => node.remove());
|
||
|
||
if (window.AdminFormHelpers) {
|
||
window.AdminFormHelpers.refresh(scope);
|
||
}
|
||
}
|
||
|
||
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 || {});
|
||
|
||
const mapEmbedUrlInput = document.getElementById('mapEmbedUrl');
|
||
if (mapEmbedUrlInput) {
|
||
mapEmbedUrlInput.value = data.map?.embedUrl || '';
|
||
}
|
||
|
||
populateContactCardsFromData(data.contactCards || []);
|
||
populateFormFieldsFromData(data.form?.fields || []);
|
||
}
|
||
|
||
function extractEmbedUrl(raw) {
|
||
if (!raw) return '';
|
||
const trimmed = raw.trim();
|
||
const srcMatch = trimmed.match(/src\s*=\s*["']([^"']+)["']/i);
|
||
if (srcMatch && srcMatch[1]) return srcMatch[1].trim();
|
||
return trimmed;
|
||
}
|
||
|
||
function sanitizeLocationInput(raw) {
|
||
if (!raw) return '';
|
||
const trimmed = String(raw).trim();
|
||
if (!trimmed) return '';
|
||
if (/leaflet-container|leaflet-touch|leaflet-fade-anim|leaflet-grab|leaflet-touch-drag|leaflet-touch-zoom/i.test(trimmed)) {
|
||
return '';
|
||
}
|
||
if (/<[^>]+>/.test(trimmed)) {
|
||
return trimmed.replace(/<[^>]+>/g, '').trim();
|
||
}
|
||
return trimmed;
|
||
}
|
||
|
||
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}" maxlength="40" data-maxlength="40">
|
||
</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" maxlength="255" data-maxlength="255">
|
||
<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 d-block mt-1">Custom icons render at about 28x28px inside a 64x64 circle. Use SVG or a square PNG/WebP at 64x64px or 128x128px.</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" maxlength="220" data-maxlength="220"></textarea>
|
||
<small class="text-muted d-block mt-1">Each line is shown inside a compact contact card. Keep it to 1-3 short lines.</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);
|
||
});
|
||
}
|
||
|
||
initContactCharacterCounters(newCard);
|
||
}
|
||
|
||
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" maxlength="32" data-maxlength="32">
|
||
<small class="text-muted d-block mt-1">Keep field labels short for the stacked mobile form.</small>
|
||
</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}" maxlength="72" data-maxlength="72">
|
||
</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" maxlength="48" data-maxlength="48">
|
||
<small class="text-muted d-block mt-1">Internal programme reference only. Recommended maximum: 48 characters.</small>
|
||
</div>
|
||
<!-- Hidden fields for label and colClass -->
|
||
<input type="hidden" name="fieldLabel_${index}" value="">
|
||
<input type="hidden" name="fieldColClass_${index}" value="col-lg-12">
|
||
</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);
|
||
initContactCharacterCounters(container.lastElementChild);
|
||
}
|
||
|
||
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 rawLocation = (document.getElementById('mapLocation') || {}).value?.trim() || '';
|
||
const cleanedLocation = sanitizeLocationInput(rawLocation) || originalFormData?.map?.location || '';
|
||
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: cleanedLocation,
|
||
markerTitle: (document.getElementById('mapMarkerTitle') || {}).value?.trim() || '',
|
||
embedUrl: (document.getElementById('mapEmbedUrl') || {}).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, idx) => {
|
||
const nameEl = item.querySelector('[name^="fieldName_"]');
|
||
const labelEl = item.querySelector('[name^="fieldLabel_"]');
|
||
const typeEl = item.querySelector('[name^="fieldType_"]');
|
||
const placeholderEl = item.querySelector('[name^="fieldPlaceholder_"]');
|
||
const requiredEl = item.querySelector('[name^="fieldRequired_"]');
|
||
const colClassEl = item.querySelector('[name^="fieldColClass_"]');
|
||
const programmeNameEl = item.querySelector('[name^="fieldProgrammeName_"]');
|
||
|
||
// Get original field data if exists
|
||
const originalField = originalFormData?.form?.fields?.[idx] || {};
|
||
|
||
const fieldData = {
|
||
name: (nameEl?.value || '').trim(),
|
||
label: (labelEl?.value || '').trim() || originalField.label || '',
|
||
type: (typeEl?.value || '').trim() || originalField.type || 'text',
|
||
placeholder: (placeholderEl?.value || '').trim(),
|
||
required: (requiredEl?.checked || false),
|
||
colClass: (colClassEl?.value || '').trim() || originalField.colClass || 'col-lg-12'
|
||
};
|
||
|
||
console.log(`Field ${idx} (${fieldData.name}): colClassInput='${colClassEl?.value}', original='${originalField.colClass}', FINAL='${fieldData.colClass}'`);
|
||
|
||
// Add programmeName if field type is programme
|
||
if (fieldData.type === 'programme' && programmeNameEl) {
|
||
fieldData.programmeName = (programmeNameEl?.value || '').trim();
|
||
}
|
||
|
||
return fieldData;
|
||
})
|
||
.filter(field => field.name !== '');
|
||
|
||
// Get original form data
|
||
const originalForm = originalFormData?.form || {};
|
||
|
||
const formData = {
|
||
sectionLabel: (document.getElementById('formSectionLabel') || {}).value?.trim() || '',
|
||
heading: (document.getElementById('formHeading') || {}).value?.trim() || '',
|
||
description: (document.getElementById('formDescription') || {}).value?.trim() || originalForm.description || '',
|
||
fields: formFieldsData,
|
||
submitButton: {
|
||
text: (document.getElementById('formSubmitButtonText') || {}).value?.trim() || 'Send Message',
|
||
icon: (document.getElementById('formSubmitButtonIcon') || {}).value?.trim() || originalForm.submitButton?.icon || 'fa-solid fa-arrow-right',
|
||
buttonClass: (document.getElementById('formSubmitButtonClass') || {}).value?.trim() || originalForm.submitButton?.buttonClass || 'theme-btn style-2'
|
||
}
|
||
};
|
||
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;
|
||
const labelEl = el.querySelector(`[name="fieldLabel_${i}"]`);
|
||
if (labelEl) labelEl.value = field.label || '';
|
||
const colClassEl = el.querySelector(`[name="fieldColClass_${i}"]`);
|
||
if (colClassEl) colClassEl.value = field.colClass || 'col-lg-12';
|
||
|
||
// 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>
|