forked from UKSOURCE/cms.hailearning.edu.vn
refactor: enhance home page structure and content management
This commit is contained in:
@@ -1,157 +1,312 @@
|
||||
<!-- Hero Tab -->
|
||||
<div class="tab-pane fade show active" id="hero" role="tabpanel">
|
||||
<div class="row g-4">
|
||||
<!-- Basic Info -->
|
||||
<!-- Background Image (section-level) -->
|
||||
<div class="col-md-12">
|
||||
<div class="card border shadow-sm">
|
||||
<div class="card-header bg-white">
|
||||
<div class="card border shadow-sm mb-3">
|
||||
<div class="card-header bg-white d-flex justify-content-between align-items-center">
|
||||
<h6 class="mb-0">
|
||||
<i class="fas fa-info-circle me-2"></i>Basic Information
|
||||
<i class="fas fa-image me-2"></i>Hero Background
|
||||
</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Title</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="heroTitle"
|
||||
value="<%= data.hero?.title || '' %>"
|
||||
placeholder="e.g., From Application to Visa"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Subtitle</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="heroSubtitle"
|
||||
value="<%= data.hero?.subtitle || '' %>"
|
||||
placeholder="e.g., Global Education Simplified"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-medium">Description</label>
|
||||
<textarea
|
||||
class="form-control"
|
||||
id="heroDescription"
|
||||
rows="3"
|
||||
placeholder="Enter hero description"
|
||||
><%= data.hero?.description || '' %></textarea>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Background Image</label>
|
||||
<div class="input-group mb-2">
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="heroBackgroundImage"
|
||||
value="<%= data.hero?.backgroundImage || '' %>"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-outline-primary btn-upload-image"
|
||||
data-target-input="heroBackgroundImage"
|
||||
data-image-type="home"
|
||||
>
|
||||
<input type="text" class="form-control" id="heroBackgroundImage"
|
||||
value="<%= data.hero?.backgroundImage || '' %>" placeholder="/assets/img/home-1/hero/bg.jpg" />
|
||||
<button type="button" class="btn btn-outline-primary btn-upload-image"
|
||||
data-target-input="heroBackgroundImage" data-image-type="home">
|
||||
<i class="fas fa-upload me-1"></i>Upload
|
||||
</button>
|
||||
</div>
|
||||
<% if (data.hero?.backgroundImage) { %>
|
||||
<div class="mt-2">
|
||||
<img
|
||||
src="<%= data.hero.backgroundImage %>"
|
||||
class="img-thumbnail"
|
||||
style="height: 200px; width: 100%; object-fit: cover;"
|
||||
alt="Background preview"
|
||||
/>
|
||||
<div class="mt-2">
|
||||
<img src="<%= data.hero.backgroundImage %>" class="img-thumbnail"
|
||||
style="height: 200px; width: 100%; object-fit: cover;" alt="Background preview" />
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Slides -->
|
||||
<div class="col-md-12">
|
||||
<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-sliders-h me-2"></i>Hero Slides
|
||||
</h6>
|
||||
<button type="button" class="btn btn-sm btn-primary" id="addHeroSlideBtn">
|
||||
<i class="fas fa-plus me-1"></i>Add Slide
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-body" id="heroSlidesContainer">
|
||||
<% const existingSlides=(data.hero && Array.isArray(data.hero.slides) && data.hero.slides.length> 0)
|
||||
? data.hero.slides
|
||||
: [{
|
||||
title: data.hero?.title || '',
|
||||
subtitle: data.hero?.subtitle || '',
|
||||
description: data.hero?.description || '',
|
||||
heroImage: data.hero?.heroImage || '',
|
||||
videoUrl: data.hero?.videoUrl || '',
|
||||
primaryButton: data.hero?.primaryButton || {},
|
||||
secondaryButton: data.hero?.secondaryButton || {},
|
||||
}];
|
||||
%>
|
||||
|
||||
<% existingSlides.forEach(function(slide, index) { %>
|
||||
<div class="card mb-3 border hero-slide-item" data-index="<%= index %>">
|
||||
<div class="card-header bg-light d-flex justify-content-between align-items-center">
|
||||
<span class="fw-semibold">Slide
|
||||
<span class="hero-slide-label">
|
||||
<%= index + 1 %>
|
||||
</span>
|
||||
</span>
|
||||
<button type="button" class="btn btn-sm btn-outline-danger btn-remove-hero-slide">
|
||||
<i class="fas fa-trash-alt"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Title</label>
|
||||
<input type="text" class="form-control" id="heroSlide_<%= index %>_title"
|
||||
value="<%= slide.title || '' %>" placeholder="e.g., From Application to Visa" />
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Subtitle</label>
|
||||
<input type="text" class="form-control" id="heroSlide_<%= index %>_subtitle"
|
||||
value="<%= slide.subtitle || '' %>" placeholder="e.g., Global Education Simplified" />
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-medium">Description</label>
|
||||
<textarea class="form-control" id="heroSlide_<%= index %>_description" rows="3"
|
||||
placeholder="Enter hero description"><%= slide.description || '' %></textarea>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Hero Image</label>
|
||||
<small class="text-muted d-block mb-1">Recommended size: 893x848px</small>
|
||||
<div class="input-group mb-2">
|
||||
<input type="text" class="form-control" id="heroSlide_<%= index %>_heroImage"
|
||||
value="<%= slide.heroImage || '' %>" placeholder="/assets/img/home-1/hero/man.png" />
|
||||
<button type="button" class="btn btn-outline-primary btn-upload-image"
|
||||
data-target-input="heroSlide_<%= index %>_heroImage" data-image-type="home">
|
||||
<i class="fas fa-upload me-1"></i>Upload
|
||||
</button>
|
||||
</div>
|
||||
<% if (slide.heroImage) { %>
|
||||
<div class="mt-2">
|
||||
<img src="<%= slide.heroImage %>" class="img-thumbnail"
|
||||
style="height: 200px; width: 100%; object-fit: cover;" alt="Hero image preview" />
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Video URL</label>
|
||||
<input type="text" class="form-control" id="heroSlide_<%= index %>_videoUrl"
|
||||
value="<%= slide.videoUrl || '' %>" placeholder="https://www.youtube.com/watch?v=..." />
|
||||
</div>
|
||||
|
||||
<!-- Primary Button -->
|
||||
<div class="col-md-6">
|
||||
<div class="border rounded p-3 h-100">
|
||||
<h6 class="fw-semibold mb-3">
|
||||
<i class="fas fa-mouse-pointer me-2"></i>Primary Button
|
||||
</h6>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-medium">Label</label>
|
||||
<input type="text" class="form-control" id="heroSlide_<%= index %>_primaryLabel"
|
||||
value="<%= slide.primaryButton?.label || '' %>" placeholder="e.g., Apply now" />
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-medium">Link</label>
|
||||
<input type="text" class="form-control" id="heroSlide_<%= index %>_primaryHref"
|
||||
value="<%= slide.primaryButton?.href || '' %>" placeholder="/contact" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Secondary Button -->
|
||||
<div class="col-md-6">
|
||||
<div class="border rounded p-3 h-100">
|
||||
<h6 class="fw-semibold mb-3">
|
||||
<i class="fas fa-mouse-pointer me-2"></i>Secondary Button
|
||||
</h6>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-medium">Label</label>
|
||||
<input type="text" class="form-control" id="heroSlide_<%= index %>_secondaryLabel"
|
||||
value="<%= slide.secondaryButton?.label || '' %>"
|
||||
placeholder="e.g., Book Free Consultation" />
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-medium">Link</label>
|
||||
<input type="text" class="form-control" id="heroSlide_<%= index %>_secondaryHref"
|
||||
value="<%= slide.secondaryButton?.href || '' %>" placeholder="/contact" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Video URL</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="heroVideoUrl"
|
||||
value="<%= data.hero?.videoUrl || '' %>"
|
||||
placeholder="https://www.youtube.com/watch?v=..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Primary Button -->
|
||||
<div class="col-md-6">
|
||||
<div class="card border shadow-sm">
|
||||
<div class="card-header bg-white">
|
||||
<h6 class="mb-0">
|
||||
<i class="fas fa-mouse-pointer me-2"></i>Primary Button
|
||||
</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-medium">Label</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="heroPrimaryButtonLabel"
|
||||
value="<%= data.hero?.primaryButton?.label || '' %>"
|
||||
placeholder="e.g., Apply now"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-medium">Link</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="heroPrimaryButtonHref"
|
||||
value="<%= data.hero?.primaryButton?.href || '' %>"
|
||||
placeholder="/contact"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Secondary Button -->
|
||||
<div class="col-md-6">
|
||||
<div class="card border shadow-sm">
|
||||
<div class="card-header bg-white">
|
||||
<h6 class="mb-0">
|
||||
<i class="fas fa-mouse-pointer me-2"></i>Secondary Button
|
||||
</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-medium">Label</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="heroSecondaryButtonLabel"
|
||||
value="<%= data.hero?.secondaryButton?.label || '' %>"
|
||||
placeholder="e.g., Book Free Consultation"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-medium">Link</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="heroSecondaryButtonHref"
|
||||
value="<%= data.hero?.secondaryButton?.href || '' %>"
|
||||
placeholder="/contact"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<% }); %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Register scraper to collect Hero data before form submit
|
||||
window.homeScrapers = window.homeScrapers || {};
|
||||
window.homeScrapers.hero = () => {
|
||||
const getVal = (id) => (document.getElementById(id)?.value || "").trim();
|
||||
|
||||
const backgroundImage = getVal("heroBackgroundImage");
|
||||
|
||||
const slides = [];
|
||||
const slideEls = document.querySelectorAll(".hero-slide-item");
|
||||
slideEls.forEach((el, idx) => {
|
||||
const index = el.getAttribute("data-index") || idx;
|
||||
const prefix = `heroSlide_${index}_`;
|
||||
|
||||
const slide = {
|
||||
title: getVal(prefix + "title"),
|
||||
subtitle: getVal(prefix + "subtitle"),
|
||||
description: getVal(prefix + "description"),
|
||||
heroImage: getVal(prefix + "heroImage"),
|
||||
videoUrl: getVal(prefix + "videoUrl"),
|
||||
primaryButton: {
|
||||
label: getVal(prefix + "primaryLabel"),
|
||||
href: getVal(prefix + "primaryHref")
|
||||
},
|
||||
secondaryButton: {
|
||||
label: getVal(prefix + "secondaryLabel"),
|
||||
href: getVal(prefix + "secondaryHref")
|
||||
}
|
||||
};
|
||||
|
||||
// Bỏ qua slide trống hoàn toàn
|
||||
const hasContent = slide.title || slide.subtitle || slide.description || slide.heroImage || slide.videoUrl || slide.primaryButton.label || slide.secondaryButton.label;
|
||||
|
||||
if (hasContent) {
|
||||
slides.push(slide);
|
||||
}
|
||||
});
|
||||
|
||||
// Legacy single-slide fields: map từ slide đầu tiên (nếu có) để không vỡ FE cũ
|
||||
const first = slides[0] || {};
|
||||
|
||||
return {
|
||||
backgroundImage,
|
||||
slides,
|
||||
title: first.title || "",
|
||||
subtitle: first.subtitle || "",
|
||||
description: first.description || "",
|
||||
heroImage: first.heroImage || "",
|
||||
videoUrl: first.videoUrl || "",
|
||||
primaryButton: first.primaryButton || {
|
||||
label: "",
|
||||
href: ""
|
||||
},
|
||||
secondaryButton: first.secondaryButton || {
|
||||
label: "",
|
||||
href: ""
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
// Add / remove slides on the client
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
const container = document.getElementById("heroSlidesContainer");
|
||||
const addBtn = document.getElementById("addHeroSlideBtn");
|
||||
|
||||
if (!container || !addBtn)
|
||||
return;
|
||||
|
||||
|
||||
// Re-index slides, update labels, IDs and upload target attributes
|
||||
const updateLabels = () => {
|
||||
container.querySelectorAll(".hero-slide-item").forEach((el, idx) => {
|
||||
el.setAttribute("data-index", String(idx));
|
||||
|
||||
const label = el.querySelector(".hero-slide-label");
|
||||
if (label)
|
||||
label.textContent = String(idx + 1);
|
||||
|
||||
|
||||
// Update input/textarea IDs to match new index
|
||||
el.querySelectorAll("input, textarea").forEach((input) => {
|
||||
if (!input.id)
|
||||
return;
|
||||
|
||||
const newId = input.id.replace(/heroSlide_\d+_/, `heroSlide_${idx}_`);
|
||||
input.id = newId;
|
||||
});
|
||||
|
||||
// Update upload button target-input so upload goes to correct slide
|
||||
el.querySelectorAll(".btn-upload-image").forEach((btn) => {
|
||||
const target = btn.getAttribute("data-target-input") || "";
|
||||
if (!target)
|
||||
return;
|
||||
|
||||
const newTarget = target.replace(/heroSlide_\d+_/, `heroSlide_${idx}_`);
|
||||
btn.setAttribute("data-target-input", newTarget);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
addBtn.addEventListener("click", () => {
|
||||
const template = container.querySelector(".hero-slide-item");
|
||||
if (!template)
|
||||
return;
|
||||
|
||||
|
||||
const clone = template.cloneNode(true);
|
||||
|
||||
// Clear values for the cloned slide (IDs will be fixed by updateLabels)
|
||||
clone.querySelectorAll("input, textarea").forEach((input) => {
|
||||
input.value = "";
|
||||
});
|
||||
|
||||
// Clear image previews
|
||||
clone.querySelectorAll("img").forEach((img) => {
|
||||
img.classList.add("d-none");
|
||||
img.removeAttribute("src");
|
||||
});
|
||||
|
||||
container.appendChild(clone);
|
||||
updateLabels();
|
||||
});
|
||||
|
||||
container.addEventListener("click", (e) => {
|
||||
const btn = e.target.closest(".btn-remove-hero-slide");
|
||||
if (!btn)
|
||||
return;
|
||||
|
||||
|
||||
const card = btn.closest(".hero-slide-item");
|
||||
if (!card)
|
||||
return;
|
||||
|
||||
|
||||
const all = container.querySelectorAll(".hero-slide-item");
|
||||
if (all.length <= 1) { // Không cho xóa slide cuối cùng
|
||||
return;
|
||||
}
|
||||
|
||||
card.remove();
|
||||
updateLabels();
|
||||
});
|
||||
|
||||
// Initial normalization (in case indices rendered from server are not 0..n)
|
||||
updateLabels();
|
||||
});
|
||||
</script>
|
||||
Reference in New Issue
Block a user