forked from UKSOURCE/cms.lams
Refactor Homepage, About, Footer (Controller,Model, View, Data), Update: Dashboard, Delete old file
This commit is contained in:
+624
-180
@@ -1,231 +1,675 @@
|
||||
<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)">
|
||||
Homepage Management
|
||||
<h1 class="h3 mb-0" style="color: var(--primary-dark);">
|
||||
<%= title %>
|
||||
</h1>
|
||||
<p class="text-muted mb-0">Edit content displayed on homepage</p>
|
||||
</div>
|
||||
<div>
|
||||
<a href="<%= frontendUrl %>" class="btn btn-outline-primary" target="_blank">
|
||||
<i class="fas fa-external-link-alt me-2"></i>View Homepage
|
||||
</a>
|
||||
<p class="text-muted mb-0">Edit content displayed on the Home page</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<form action="/admin/home/update" method="POST" class="content-with-fixed-buttons">
|
||||
<!-- Hidden inputs for JSON data -->
|
||||
<input type="hidden" name="hero" id="heroJson" />
|
||||
<input type="hidden" name="whyChooseUs" id="whyChooseUsJson" />
|
||||
<input type="hidden" name="visaSolutions" id="visaSolutionsJson" />
|
||||
<input type="hidden" name="visaCountries" id="visaCountriesJson" />
|
||||
<input type="hidden" name="testimonials" id="testimonialsJson" />
|
||||
<input type="hidden" name="videoGallery" id="videoGalleryJson" />
|
||||
<input type="hidden" name="faq" id="faqJson" />
|
||||
<input type="hidden" name="achievements" id="achievementsJson" />
|
||||
<input type="hidden" name="partners" id="partnersJson" />
|
||||
<input type="hidden" name="blogPreview" id="blogPreviewJson" />
|
||||
<form method="POST" id="homeForm" action="/admin/home/update">
|
||||
<!-- Hidden JSON inputs -->
|
||||
<input type="hidden" name="hero" id="heroJson">
|
||||
<input type="hidden" name="quickLinks" id="quickLinksJson">
|
||||
<input type="hidden" name="valueProp" id="valuePropJson">
|
||||
<input type="hidden" name="programs" id="programsJson">
|
||||
<input type="hidden" name="requestInfo" id="requestInfoJson">
|
||||
|
||||
<!-- Navigation Tabs -->
|
||||
<!-- 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>
|
||||
<ul class="nav nav-tabs card-header-tabs" id="homeTabs" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<a class="nav-link active" id="tab-hero" data-bs-toggle="tab" href="#tab-hero-pane" role="tab"><i
|
||||
class="fas fa-image me-2"></i>Hero</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-bs-toggle="tab" href="#whychooseus" role="tab">
|
||||
<i class="fas fa-star me-2"></i>Why Choose Us
|
||||
</a>
|
||||
<li class="nav-item" role="presentation">
|
||||
<a class="nav-link" id="tab-quickLinks" data-bs-toggle="tab" href="#tab-quickLinks-pane" role="tab"><i
|
||||
class="fas fa-link me-2"></i>Quick Links</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-bs-toggle="tab" href="#visasolutions" role="tab">
|
||||
<i class="fas fa-concierge-bell me-2"></i>Visa Solutions
|
||||
</a>
|
||||
<li class="nav-item" role="presentation">
|
||||
<a class="nav-link" id="tab-valueProp" data-bs-toggle="tab" href="#tab-valueProp-pane" role="tab"><i
|
||||
class="fas fa-star me-2"></i>Value Prop</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-bs-toggle="tab" href="#visacountries" role="tab">
|
||||
<i class="fas fa-globe-americas me-2"></i>Visa Countries
|
||||
</a>
|
||||
<li class="nav-item" role="presentation">
|
||||
<a class="nav-link" id="tab-programs" data-bs-toggle="tab" href="#tab-programs-pane" role="tab"><i
|
||||
class="fas fa-graduation-cap me-2"></i>Programs</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-bs-toggle="tab" href="#testimonials" role="tab">
|
||||
<i class="fas fa-comments me-2"></i>Testimonials
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-bs-toggle="tab" href="#videogallery" role="tab">
|
||||
<i class="fas fa-video me-2"></i>Video Gallery
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-bs-toggle="tab" href="#faq" role="tab">
|
||||
<i class="fas fa-question-circle me-2"></i>FAQ
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-bs-toggle="tab" href="#achievements" role="tab">
|
||||
<i class="fas fa-chart-pie me-2"></i>Achievements
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-bs-toggle="tab" href="#partners" role="tab">
|
||||
<i class="fas fa-handshake me-2"></i>Partners
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-bs-toggle="tab" href="#blogpreview" role="tab">
|
||||
<i class="fas fa-blog me-2"></i>Blog Preview
|
||||
</a>
|
||||
<li class="nav-item" role="presentation">
|
||||
<a class="nav-link" id="tab-requestInfo" data-bs-toggle="tab" href="#tab-requestInfo-pane" role="tab"><i
|
||||
class="fas fa-envelope me-2"></i>Request Info</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<div class="tab-content">
|
||||
<%- include('sections/hero') %>
|
||||
<%- include('sections/whyChooseUs') %>
|
||||
<%- include('sections/visaSolutions') %>
|
||||
<%- include('sections/visaCountries') %>
|
||||
<%- include('sections/testimonials') %>
|
||||
<%- include('sections/videoGallery') %>
|
||||
<%- include('sections/faq') %>
|
||||
<%- include('sections/achievements') %>
|
||||
<%- include('sections/partners') %>
|
||||
<%- include('sections/blogPreview') %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Move buttons to fixed bottom -->
|
||||
<div class="fixed-bottom-buttons">
|
||||
<button type="reset" class="btn btn-secondary">
|
||||
<i class="fas fa-undo"></i>
|
||||
<span>Reset</span>
|
||||
</button>
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fas fa-save"></i>
|
||||
<span>Save Changes</span>
|
||||
</button>
|
||||
<!-- ===== HERO TAB ===== -->
|
||||
<div class="tab-pane fade show active" id="tab-hero-pane" role="tabpanel">
|
||||
<div class="card border shadow-sm">
|
||||
<div class="card-header bg-white">
|
||||
<h6 class="mb-0"><i class="fas fa-image me-2"></i>Hero Section</h6>
|
||||
</div>
|
||||
<div class="card-body p-4">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Badge</label>
|
||||
<input type="text" class="form-control" id="heroBadge" value="<%= data.hero?.badge || '' %>">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Button Label</label>
|
||||
<input type="text" class="form-control" id="heroButtonLabel"
|
||||
value="<%= data.hero?.buttonLabel || '' %>">
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label">Title</label>
|
||||
<input type="text" class="form-control" id="heroTitle" value="<%= data.hero?.title || '' %>">
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label">Description</label>
|
||||
<textarea class="form-control" id="heroDescription"
|
||||
rows="3"><%= data.hero?.description || '' %></textarea>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Search Placeholder</label>
|
||||
<input type="text" class="form-control" id="heroSearchPlaceholder"
|
||||
value="<%= data.hero?.searchPlaceholder || '' %>">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Image Alt Text</label>
|
||||
<input type="text" class="form-control" id="heroImageAlt"
|
||||
value="<%= data.hero?.imageAlt || '' %>">
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label">Image URL</label>
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" id="heroImage" value="<%= data.hero?.image || '' %>">
|
||||
<button class="btn btn-outline-primary btn-upload-image" type="button"
|
||||
data-target-input="heroImage" data-image-type="home"><i
|
||||
class="fas fa-upload me-1"></i>Upload</button>
|
||||
</div>
|
||||
<% if (data.hero?.image) { %>
|
||||
<img src="<%= data.hero.image %>" class="img-thumbnail uploaded-preview mt-2"
|
||||
style="max-height:180px;">
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h6 class="mt-4 mb-3">Floating Badge</h6>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Icon (FA class)</label>
|
||||
<input type="text" class="form-control" id="floatingBadgeIcon"
|
||||
value="<%= data.hero?.floatingBadge?.icon || '' %>" placeholder="e.g. fa-users">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Value</label>
|
||||
<input type="text" class="form-control" id="floatingBadgeValue"
|
||||
value="<%= data.hero?.floatingBadge?.value || '' %>">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Label</label>
|
||||
<input type="text" class="form-control" id="floatingBadgeLabel"
|
||||
value="<%= data.hero?.floatingBadge?.label || '' %>">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ===== QUICK LINKS TAB ===== -->
|
||||
<div class="tab-pane fade" id="tab-quickLinks-pane" role="tabpanel">
|
||||
<div class="card border shadow-sm">
|
||||
<div class="card-header bg-white d-flex justify-content-between align-items-center">
|
||||
<h6 class="mb-0"><i class="fas fa-link me-2"></i>Quick Links</h6>
|
||||
<button type="button" class="btn btn-outline-primary btn-sm" onclick="addQuickLink()"><i
|
||||
class="fas fa-plus me-1"></i>Add Link</button>
|
||||
</div>
|
||||
<div class="card-body p-4">
|
||||
<div id="quickLinksContainer"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ===== VALUE PROP TAB ===== -->
|
||||
<div class="tab-pane fade" id="tab-valueProp-pane" role="tabpanel">
|
||||
<div class="card border shadow-sm">
|
||||
<div class="card-header bg-white">
|
||||
<h6 class="mb-0"><i class="fas fa-star me-2"></i>Value Proposition</h6>
|
||||
</div>
|
||||
<div class="card-body p-4">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Badge</label>
|
||||
<input type="text" class="form-control" id="valuePropBadge"
|
||||
value="<%= data.valueProp?.badge || '' %>">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Title</label>
|
||||
<input type="text" class="form-control" id="valuePropTitle"
|
||||
value="<%= data.valueProp?.title || '' %>">
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label">Description</label>
|
||||
<textarea class="form-control" id="valuePropDescription"
|
||||
rows="3"><%= data.valueProp?.description || '' %></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<h6 class="mb-0">Features</h6>
|
||||
<button type="button" class="btn btn-outline-primary btn-sm" onclick="addValuePropFeature()"><i
|
||||
class="fas fa-plus me-1"></i>Add Feature</button>
|
||||
</div>
|
||||
<div id="valuePropFeaturesContainer"></div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<h6 class="mb-0">Stats</h6>
|
||||
<button type="button" class="btn btn-outline-primary btn-sm" onclick="addValuePropStat()"><i
|
||||
class="fas fa-plus me-1"></i>Add Stat</button>
|
||||
</div>
|
||||
<div id="valuePropStatsContainer"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ===== PROGRAMS TAB ===== -->
|
||||
<div class="tab-pane fade" id="tab-programs-pane" role="tabpanel">
|
||||
<div class="card border shadow-sm">
|
||||
<div class="card-header bg-white">
|
||||
<h6 class="mb-0"><i class="fas fa-graduation-cap me-2"></i>Programs Section</h6>
|
||||
</div>
|
||||
<div class="card-body p-4">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Heading</label>
|
||||
<input type="text" class="form-control" id="programsHeading"
|
||||
value="<%= data.programs?.heading || '' %>">
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label">Description</label>
|
||||
<textarea class="form-control" id="programsDescription"
|
||||
rows="2"><%= data.programs?.description || '' %></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<h6 class="mb-0">Program Items</h6>
|
||||
<button type="button" class="btn btn-outline-primary btn-sm" onclick="addProgramItem()"><i
|
||||
class="fas fa-plus me-1"></i>Add Program</button>
|
||||
</div>
|
||||
<div id="programItemsContainer"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ===== REQUEST INFO TAB ===== -->
|
||||
<div class="tab-pane fade" id="tab-requestInfo-pane" role="tabpanel">
|
||||
<div class="card border shadow-sm">
|
||||
<div class="card-header bg-white">
|
||||
<h6 class="mb-0"><i class="fas fa-envelope me-2"></i>Request Info Section</h6>
|
||||
</div>
|
||||
<div class="card-body p-4">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-12">
|
||||
<label class="form-label">Heading</label>
|
||||
<input type="text" class="form-control" id="requestInfoHeading"
|
||||
value="<%= data.requestInfo?.heading || '' %>">
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label">Description</label>
|
||||
<textarea class="form-control" id="requestInfoDescription"
|
||||
rows="2"><%= data.requestInfo?.description || '' %></textarea>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Phone</label>
|
||||
<input type="text" class="form-control" id="requestInfoPhone"
|
||||
value="<%= data.requestInfo?.phone || '' %>">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Email</label>
|
||||
<input type="text" class="form-control" id="requestInfoEmail"
|
||||
value="<%= data.requestInfo?.email || '' %>">
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<h6 class="mb-0">Programs List</h6>
|
||||
<button type="button" class="btn btn-outline-primary btn-sm"
|
||||
onclick="addRequestInfoProgram()"><i class="fas fa-plus me-1"></i>Add Program</button>
|
||||
</div>
|
||||
<div id="requestInfoProgramsContainer"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div><!-- /.tab-content -->
|
||||
</div><!-- /.card-body -->
|
||||
|
||||
<div class="card-footer bg-light d-flex justify-content-end py-3 gap-2">
|
||||
<button type="button" class="btn btn-outline-secondary px-4" onclick="resetForm()"><i
|
||||
class="fas fa-undo me-2"></i>Reset</button>
|
||||
<button type="submit" class="btn btn-outline-primary px-4" id="submitBtn"><i
|
||||
class="fas fa-save me-2"></i>Save Changes</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Image upload input -->
|
||||
<input type="file" id="directImageUpload" style="display: none" />
|
||||
<input type="hidden" id="currentImageType" name="imageType" />
|
||||
<input type="hidden" id="currentTargetInput" name="targetInput" />
|
||||
|
||||
<script>
|
||||
/**
|
||||
* BRIDGE SCRIPT: Cho phép các section lẻ tự đăng ký logic lấy dữ liệu.
|
||||
* Cách dùng trong file lẻ (vị dụ hero.ejs):
|
||||
* <script>
|
||||
* window.homeScrapers = window.homeScrapers || {};
|
||||
* window.homeScrapers.hero = () => ({ title: document.getElementById('heroTitle').value, ... });
|
||||
* <\/script>
|
||||
*/
|
||||
window.homeScrapers = window.homeScrapers || {};
|
||||
let originalFormData = null;
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
const form = document.querySelector("form");
|
||||
if (form) {
|
||||
form.addEventListener("submit", function (e) {
|
||||
console.log("Form submitting, collecting data from scrapers...");
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
originalFormData = <%- JSON.stringify(data) %>;
|
||||
populateAll(originalFormData);
|
||||
|
||||
// Tự động thu gom dữ liệu từ các section đã đăng ký
|
||||
Object.keys(window.homeScrapers).forEach(section => {
|
||||
const input = document.getElementById(section + 'Json');
|
||||
if (input) {
|
||||
try {
|
||||
const data = window.homeScrapers[section]();
|
||||
console.log(`- Collected data for [${section}]:`, data);
|
||||
input.value = JSON.stringify(data);
|
||||
} catch (err) {
|
||||
console.error(`Error scraping section [${section}]:`, err);
|
||||
}
|
||||
}
|
||||
});
|
||||
document.getElementById('homeForm').addEventListener('submit', function (e) {
|
||||
e.preventDefault();
|
||||
serializeAll();
|
||||
this.submit();
|
||||
});
|
||||
|
||||
// Để form tự submit tự nhiên sau khi đã điền xong các hidden inputs
|
||||
});
|
||||
}
|
||||
|
||||
// Khởi tạo các nút upload ảnh (dùng chung cho toàn bộ các section)
|
||||
initImageUploads();
|
||||
document.body.addEventListener('click', function (e) {
|
||||
const btn = e.target.closest('.btn-upload-image');
|
||||
if (btn) openImageUploader(btn.dataset.targetInput, btn.dataset.imageType);
|
||||
});
|
||||
});
|
||||
|
||||
// --- UTILITIES (Dùng chung) ---
|
||||
// ── Populate all sections from data object ──────────────────────────────
|
||||
function populateAll(data) {
|
||||
if (!data) return;
|
||||
|
||||
function initImageUploads() {
|
||||
document.addEventListener("click", function (e) {
|
||||
const btn = e.target.closest(".btn-upload-image");
|
||||
if (btn) {
|
||||
document.getElementById("currentImageType").value = btn.dataset.imageType;
|
||||
document.getElementById("currentTargetInput").value = btn.dataset.targetInput;
|
||||
document.getElementById("directImageUpload").click();
|
||||
// Hero
|
||||
const h = data.hero || {};
|
||||
setVal('heroBadge', h.badge);
|
||||
setVal('heroTitle', h.title);
|
||||
setVal('heroDescription', h.description);
|
||||
setVal('heroSearchPlaceholder', h.searchPlaceholder);
|
||||
setVal('heroButtonLabel', h.buttonLabel);
|
||||
setVal('heroImage', h.image);
|
||||
setVal('heroImageAlt', h.imageAlt);
|
||||
setVal('floatingBadgeIcon', h.floatingBadge?.icon);
|
||||
setVal('floatingBadgeValue', h.floatingBadge?.value);
|
||||
setVal('floatingBadgeLabel', h.floatingBadge?.label);
|
||||
updateImagePreview('heroImage', h.image);
|
||||
|
||||
// Quick Links
|
||||
populateQuickLinks(data.quickLinks || []);
|
||||
|
||||
// Value Prop
|
||||
const vp = data.valueProp || {};
|
||||
setVal('valuePropBadge', vp.badge);
|
||||
setVal('valuePropTitle', vp.title);
|
||||
setVal('valuePropDescription', vp.description);
|
||||
populateValuePropFeatures(vp.features || []);
|
||||
populateValuePropStats(vp.stats || []);
|
||||
|
||||
// Programs
|
||||
const pr = data.programs || {};
|
||||
setVal('programsHeading', pr.heading);
|
||||
setVal('programsDescription', pr.description);
|
||||
populateProgramItems(pr.items || []);
|
||||
|
||||
// Request Info
|
||||
const ri = data.requestInfo || {};
|
||||
setVal('requestInfoHeading', ri.heading);
|
||||
setVal('requestInfoDescription', ri.description);
|
||||
setVal('requestInfoPhone', ri.phone);
|
||||
setVal('requestInfoEmail', ri.email);
|
||||
populateRequestInfoPrograms(ri.programs || []);
|
||||
}
|
||||
|
||||
function setVal(id, val) {
|
||||
const el = document.getElementById(id);
|
||||
if (!el) return;
|
||||
el.tagName === 'TEXTAREA' ? (el.value = val || '') : (el.value = val || '');
|
||||
}
|
||||
|
||||
// ── Serialize all sections into hidden JSON inputs ───────────────────────
|
||||
function serializeAll() {
|
||||
// Hero
|
||||
document.getElementById('heroJson').value = JSON.stringify({
|
||||
badge: v('heroBadge'),
|
||||
title: v('heroTitle'),
|
||||
description: v('heroDescription'),
|
||||
searchPlaceholder: v('heroSearchPlaceholder'),
|
||||
buttonLabel: v('heroButtonLabel'),
|
||||
image: v('heroImage'),
|
||||
imageAlt: v('heroImageAlt'),
|
||||
floatingBadge: {
|
||||
icon: v('floatingBadgeIcon'),
|
||||
value: v('floatingBadgeValue'),
|
||||
label: v('floatingBadgeLabel')
|
||||
}
|
||||
});
|
||||
|
||||
const fileInput = document.getElementById("directImageUpload");
|
||||
if (fileInput) {
|
||||
fileInput.addEventListener("change", handleDirectImageUpload);
|
||||
// Quick Links
|
||||
document.getElementById('quickLinksJson').value = JSON.stringify(
|
||||
Array.from(document.querySelectorAll('.quicklink-item')).map(row => ({
|
||||
icon: row.querySelector('[data-field="icon"]').value.trim(),
|
||||
title: row.querySelector('[data-field="title"]').value.trim(),
|
||||
description: row.querySelector('[data-field="description"]').value.trim(),
|
||||
linkText: row.querySelector('[data-field="linkText"]').value.trim(),
|
||||
href: row.querySelector('[data-field="href"]').value.trim()
|
||||
}))
|
||||
);
|
||||
|
||||
// Value Prop
|
||||
document.getElementById('valuePropJson').value = JSON.stringify({
|
||||
badge: v('valuePropBadge'),
|
||||
title: v('valuePropTitle'),
|
||||
description: v('valuePropDescription'),
|
||||
features: Array.from(document.querySelectorAll('.vp-feature-item')).map(row => ({
|
||||
icon: row.querySelector('[data-field="icon"]').value.trim(),
|
||||
title: row.querySelector('[data-field="title"]').value.trim(),
|
||||
description: row.querySelector('[data-field="description"]').value.trim()
|
||||
})),
|
||||
stats: Array.from(document.querySelectorAll('.vp-stat-item')).map(row => ({
|
||||
value: row.querySelector('[data-field="value"]').value.trim(),
|
||||
label: row.querySelector('[data-field="label"]').value.trim(),
|
||||
image: row.querySelector('[data-field="image"]').value.trim(),
|
||||
imageAlt: row.querySelector('[data-field="imageAlt"]').value.trim()
|
||||
}))
|
||||
});
|
||||
|
||||
// Programs
|
||||
document.getElementById('programsJson').value = JSON.stringify({
|
||||
heading: v('programsHeading'),
|
||||
description: v('programsDescription'),
|
||||
items: Array.from(document.querySelectorAll('.program-item')).map(row => ({
|
||||
category: row.querySelector('[data-field="category"]').value.trim(),
|
||||
image: row.querySelector('[data-field="image"]').value.trim(),
|
||||
duration: row.querySelector('[data-field="duration"]').value.trim(),
|
||||
rating: row.querySelector('[data-field="rating"]').value.trim(),
|
||||
title: row.querySelector('[data-field="title"]').value.trim(),
|
||||
description: row.querySelector('[data-field="description"]').value.trim(),
|
||||
studentCount: row.querySelector('[data-field="studentCount"]').value.trim(),
|
||||
href: row.querySelector('[data-field="href"]').value.trim()
|
||||
}))
|
||||
});
|
||||
|
||||
// Request Info
|
||||
document.getElementById('requestInfoJson').value = JSON.stringify({
|
||||
heading: v('requestInfoHeading'),
|
||||
description: v('requestInfoDescription'),
|
||||
phone: v('requestInfoPhone'),
|
||||
email: v('requestInfoEmail'),
|
||||
programs: Array.from(document.querySelectorAll('.ri-program-input')).map(i => i.value.trim()).filter(Boolean)
|
||||
});
|
||||
}
|
||||
|
||||
function v(id) {
|
||||
const el = document.getElementById(id);
|
||||
return el ? el.value.trim() : '';
|
||||
}
|
||||
|
||||
// ── Quick Links ──────────────────────────────────────────────────────────
|
||||
function addQuickLink(item = {}) {
|
||||
const c = document.getElementById('quickLinksContainer');
|
||||
const html = `
|
||||
<div class="card mb-3 quicklink-item">
|
||||
<div class="card-body p-3">
|
||||
<div class="row g-2">
|
||||
<div class="col-md-2">
|
||||
<label class="form-label small">Icon (FA class)</label>
|
||||
<input type="text" class="form-control form-control-sm" data-field="icon" value="${esc(item.icon)}" placeholder="fa-trophy">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label small">Title</label>
|
||||
<input type="text" class="form-control form-control-sm" data-field="title" value="${esc(item.title)}">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label small">Description</label>
|
||||
<input type="text" class="form-control form-control-sm" data-field="description" value="${esc(item.description)}">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label small">Link Text</label>
|
||||
<input type="text" class="form-control form-control-sm" data-field="linkText" value="${esc(item.linkText)}">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label small">Href</label>
|
||||
<input type="text" class="form-control form-control-sm" data-field="href" value="${esc(item.href)}">
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="btn btn-link text-danger btn-sm p-0 mt-2" onclick="this.closest('.quicklink-item').remove()"><i class="fas fa-trash me-1"></i>Remove</button>
|
||||
</div>
|
||||
</div>`;
|
||||
c.insertAdjacentHTML('beforeend', html);
|
||||
}
|
||||
|
||||
function populateQuickLinks(items) {
|
||||
document.getElementById('quickLinksContainer').innerHTML = '';
|
||||
items.forEach(item => addQuickLink(item));
|
||||
}
|
||||
|
||||
// ── Value Prop Features ──────────────────────────────────────────────────
|
||||
function addValuePropFeature(item = {}) {
|
||||
const c = document.getElementById('valuePropFeaturesContainer');
|
||||
const html = `
|
||||
<div class="card mb-2 vp-feature-item">
|
||||
<div class="card-body p-3">
|
||||
<div class="row g-2">
|
||||
<div class="col-md-3">
|
||||
<label class="form-label small">Icon (FA class)</label>
|
||||
<input type="text" class="form-control form-control-sm" data-field="icon" value="${esc(item.icon)}" placeholder="fa-laptop-code">
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label small">Title</label>
|
||||
<input type="text" class="form-control form-control-sm" data-field="title" value="${esc(item.title)}">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label small">Description</label>
|
||||
<input type="text" class="form-control form-control-sm" data-field="description" value="${esc(item.description)}">
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="btn btn-link text-danger btn-sm p-0 mt-2" onclick="this.closest('.vp-feature-item').remove()"><i class="fas fa-trash me-1"></i>Remove</button>
|
||||
</div>
|
||||
</div>`;
|
||||
c.insertAdjacentHTML('beforeend', html);
|
||||
}
|
||||
|
||||
function populateValuePropFeatures(items) {
|
||||
document.getElementById('valuePropFeaturesContainer').innerHTML = '';
|
||||
items.forEach(item => addValuePropFeature(item));
|
||||
}
|
||||
|
||||
// ── Value Prop Stats ─────────────────────────────────────────────────────
|
||||
function addValuePropStat(item = {}) {
|
||||
const idx = Date.now();
|
||||
const c = document.getElementById('valuePropStatsContainer');
|
||||
const html = `
|
||||
<div class="card mb-2 vp-stat-item">
|
||||
<div class="card-body p-3">
|
||||
<div class="row g-2">
|
||||
<div class="col-md-2">
|
||||
<label class="form-label small">Value</label>
|
||||
<input type="text" class="form-control form-control-sm" data-field="value" value="${esc(item.value)}">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label small">Label</label>
|
||||
<input type="text" class="form-control form-control-sm" data-field="label" value="${esc(item.label)}">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label small">Image URL</label>
|
||||
<div class="input-group input-group-sm">
|
||||
<input type="text" class="form-control" data-field="image" id="vpStatImg_${idx}" value="${esc(item.image)}">
|
||||
<button class="btn btn-outline-primary btn-upload-image" type="button" data-target-input="vpStatImg_${idx}" data-image-type="home"><i class="fas fa-upload"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label small">Image Alt</label>
|
||||
<input type="text" class="form-control form-control-sm" data-field="imageAlt" value="${esc(item.imageAlt)}">
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="btn btn-link text-danger btn-sm p-0 mt-2" onclick="this.closest('.vp-stat-item').remove()"><i class="fas fa-trash me-1"></i>Remove</button>
|
||||
</div>
|
||||
</div>`;
|
||||
c.insertAdjacentHTML('beforeend', html);
|
||||
}
|
||||
|
||||
function populateValuePropStats(items) {
|
||||
document.getElementById('valuePropStatsContainer').innerHTML = '';
|
||||
items.forEach(item => addValuePropStat(item));
|
||||
}
|
||||
|
||||
// ── Program Items ────────────────────────────────────────────────────────
|
||||
function addProgramItem(item = {}) {
|
||||
const idx = Date.now();
|
||||
const c = document.getElementById('programItemsContainer');
|
||||
const html = `
|
||||
<div class="card mb-3 program-item">
|
||||
<div class="card-body p-3">
|
||||
<div class="row g-2">
|
||||
<div class="col-md-3">
|
||||
<label class="form-label small">Title</label>
|
||||
<input type="text" class="form-control form-control-sm" data-field="title" value="${esc(item.title)}">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label small">Category</label>
|
||||
<input type="text" class="form-control form-control-sm" data-field="category" value="${esc(item.category)}">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label small">Duration</label>
|
||||
<input type="text" class="form-control form-control-sm" data-field="duration" value="${esc(item.duration)}">
|
||||
</div>
|
||||
<div class="col-md-1">
|
||||
<label class="form-label small">Rating</label>
|
||||
<input type="text" class="form-control form-control-sm" data-field="rating" value="${esc(item.rating)}">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label small">Student Count</label>
|
||||
<input type="text" class="form-control form-control-sm" data-field="studentCount" value="${esc(item.studentCount)}">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label small">Href</label>
|
||||
<input type="text" class="form-control form-control-sm" data-field="href" value="${esc(item.href)}">
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<label class="form-label small">Description</label>
|
||||
<input type="text" class="form-control form-control-sm" data-field="description" value="${esc(item.description)}">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label small">Image URL</label>
|
||||
<div class="input-group input-group-sm">
|
||||
<input type="text" class="form-control" data-field="image" id="progImg_${idx}" value="${esc(item.image)}">
|
||||
<button class="btn btn-outline-primary btn-upload-image" type="button" data-target-input="progImg_${idx}" data-image-type="home"><i class="fas fa-upload"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="btn btn-link text-danger btn-sm p-0 mt-2" onclick="this.closest('.program-item').remove()"><i class="fas fa-trash me-1"></i>Remove</button>
|
||||
</div>
|
||||
</div>`;
|
||||
c.insertAdjacentHTML('beforeend', html);
|
||||
}
|
||||
|
||||
function populateProgramItems(items) {
|
||||
document.getElementById('programItemsContainer').innerHTML = '';
|
||||
items.forEach(item => addProgramItem(item));
|
||||
}
|
||||
|
||||
// ── Request Info Programs ────────────────────────────────────────────────
|
||||
function addRequestInfoProgram(val = '') {
|
||||
const c = document.getElementById('requestInfoProgramsContainer');
|
||||
const html = `
|
||||
<div class="input-group input-group-sm mb-2">
|
||||
<input type="text" class="form-control ri-program-input" value="${esc(val)}" placeholder="Program name">
|
||||
<button class="btn btn-outline-danger" type="button" onclick="this.parentElement.remove()"><i class="fas fa-times"></i></button>
|
||||
</div>`;
|
||||
c.insertAdjacentHTML('beforeend', html);
|
||||
}
|
||||
|
||||
function populateRequestInfoPrograms(programs) {
|
||||
document.getElementById('requestInfoProgramsContainer').innerHTML = '';
|
||||
programs.forEach(p => addRequestInfoProgram(p));
|
||||
}
|
||||
|
||||
// ── Utilities ────────────────────────────────────────────────────────────
|
||||
function esc(val) {
|
||||
if (!val) return '';
|
||||
return String(val).replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>');
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
if (confirm('Reset all changes to last saved state?')) {
|
||||
populateAll(originalFormData);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDirectImageUpload(e) {
|
||||
if (!this.files || !this.files[0]) return;
|
||||
const file = this.files[0];
|
||||
const imageType = document.getElementById("currentImageType").value;
|
||||
const targetInput = document.getElementById("currentTargetInput").value;
|
||||
function updateImagePreview(inputId, imagePath) {
|
||||
if (!imagePath) return;
|
||||
const input = document.getElementById(inputId);
|
||||
if (!input) return;
|
||||
const card = input.closest('.card');
|
||||
const preview = card ? card.querySelector('.uploaded-preview') : null;
|
||||
if (preview) preview.src = imagePath;
|
||||
}
|
||||
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append("image", file);
|
||||
const response = await fetch(`/admin/upload/image?imageType=${imageType}`, { method: "POST", body: formData });
|
||||
const result = await response.json();
|
||||
function openImageUploader(targetInput, imageType) {
|
||||
// Dùng persistent hidden file input để tránh browser block programmatic click
|
||||
let fileInput = document.getElementById('__globalFileInput');
|
||||
if (!fileInput) {
|
||||
fileInput = document.createElement('input');
|
||||
fileInput.type = 'file';
|
||||
fileInput.accept = 'image/*';
|
||||
fileInput.id = '__globalFileInput';
|
||||
fileInput.style.cssText = 'position:fixed;top:-9999px;left:-9999px;opacity:0;width:1px;height:1px;';
|
||||
document.body.appendChild(fileInput);
|
||||
}
|
||||
|
||||
if (result.success && result.path) {
|
||||
const input = document.getElementById(targetInput);
|
||||
fileInput.value = '';
|
||||
|
||||
fileInput.onchange = async function (e) {
|
||||
const file = e.target.files[0];
|
||||
if (!file) return;
|
||||
|
||||
const uploadBtn = document.querySelector(`[data-target-input="${targetInput}"]`) ||
|
||||
document.querySelector(`[onclick*="${targetInput}"]`);
|
||||
if (uploadBtn) uploadBtn.disabled = true;
|
||||
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('image', file);
|
||||
|
||||
const response = await fetch(`/admin/upload/image?imageType=${imageType}`, { method: 'POST', body: formData });
|
||||
if (!response.ok) throw new Error('Upload failed');
|
||||
const result = await response.json();
|
||||
if (!result.success) throw new Error(result.error || 'Upload failed');
|
||||
|
||||
const input = document.getElementById(targetInput) || document.querySelector(`[data-target-input-id="${targetInput}"]`);
|
||||
if (input) {
|
||||
input.value = result.path;
|
||||
// Cập nhật preview nếu có img ngay sau input group
|
||||
const previewImg = input.closest('.input-group')?.nextElementSibling?.querySelector('img');
|
||||
if (previewImg) {
|
||||
previewImg.src = result.path;
|
||||
previewImg.classList.remove('d-none');
|
||||
const previewUrl = result.path.startsWith('http') ? result.path : window.location.origin + result.path;
|
||||
let preview = input.closest('.card')?.querySelector('.uploaded-preview');
|
||||
if (preview) {
|
||||
preview.src = previewUrl;
|
||||
} else {
|
||||
const img = document.createElement('img');
|
||||
img.src = previewUrl;
|
||||
img.className = 'img-thumbnail uploaded-preview mt-2';
|
||||
img.style.maxHeight = '150px';
|
||||
input.closest('.input-group')?.insertAdjacentElement('afterend', img);
|
||||
}
|
||||
}
|
||||
showToast("Success", "Image uploaded successfully", "success");
|
||||
} else {
|
||||
throw new Error(result.error || "Upload failed");
|
||||
} catch (err) {
|
||||
console.error('Upload error:', err);
|
||||
alert('Upload failed: ' + err.message);
|
||||
} finally {
|
||||
if (uploadBtn) uploadBtn.disabled = false;
|
||||
fileInput.value = '';
|
||||
}
|
||||
} catch (error) {
|
||||
showToast("Error", "Upload failed: " + error.message, "error");
|
||||
}
|
||||
this.value = "";
|
||||
}
|
||||
|
||||
function showToast(title, message, type = "info") {
|
||||
let container = document.querySelector(".toast-container") || (() => {
|
||||
const c = document.createElement("div");
|
||||
c.className = "toast-container position-fixed top-0 end-0 p-3";
|
||||
document.body.appendChild(c);
|
||||
return c;
|
||||
})();
|
||||
|
||||
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.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"></button></div>`;
|
||||
container.appendChild(toast);
|
||||
new bootstrap.Toast(toast, { autohide: true, delay: 3000 }).show();
|
||||
toast.addEventListener("hidden.bs.toast", () => toast.remove());
|
||||
};
|
||||
fileInput.click();
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user