forked from UKSOURCE/cms.hailearning.edu.vn
350 lines
15 KiB
Plaintext
350 lines
15 KiB
Plaintext
<!-- Hero Tab -->
|
|
<div class="tab-pane fade show active" id="hero" role="tabpanel">
|
|
<div class="row g-4">
|
|
<div class="col-md-12">
|
|
<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-layer-group me-2"></i>Hero Carousel Setup
|
|
</h6>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="alert alert-light border mb-3">
|
|
<div class="fw-semibold mb-1">Current homepage hero behavior</div>
|
|
<div class="small text-muted">
|
|
Mỗi slide dùng ảnh riêng làm nền full-width. Title, description và 2 button chỉ là lớp overlay.
|
|
Trường video hiện không còn được hiển thị ở frontend.
|
|
</div>
|
|
</div>
|
|
<div class="row g-3 align-items-start">
|
|
<div class="col-lg-6">
|
|
<label class="form-label fw-medium">Fallback Background Image</label>
|
|
<small class="text-muted d-block mb-1">
|
|
Tùy chọn dự phòng. Khung hero desktop hiện hiển thị khoảng 1512x544px, nên nên upload ảnh ngang ít nhất 1920x700px.
|
|
</small>
|
|
<div class="input-group mb-2">
|
|
<input type="text" class="form-control" id="heroBackgroundImage"
|
|
value="<%= data.hero?.backgroundImage || '' %>" placeholder="/uploads/home/hero-fallback.jpg"
|
|
maxlength="255" data-maxlength="255" />
|
|
<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>
|
|
<% } %>
|
|
</div>
|
|
<div class="col-lg-6">
|
|
<div class="border rounded p-3 h-100 bg-light-subtle">
|
|
<div class="fw-semibold mb-2">Recommended content structure</div>
|
|
<ul class="small text-muted mb-0 ps-3">
|
|
<li>Dùng ảnh slide tỉ lệ ngang, ưu tiên ảnh lớn để fit container.</li>
|
|
<li>Title ngắn 2-4 dòng để không tràn trên mobile.</li>
|
|
<li>Description giữ ở mức 1-3 câu ngắn.</li>
|
|
<li>Hai nút nên dùng link nội bộ như <code>/contact</code>.</li>
|
|
</ul>
|
|
</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"
|
|
maxlength="72" data-maxlength="72" />
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label fw-medium">Subtitle</label>
|
|
<small class="text-muted d-block mb-1">Hiện không render ngoài frontend, chỉ giữ để tương thích dữ liệu cũ.</small>
|
|
<input type="text" class="form-control" id="heroSlide_<%= index %>_subtitle"
|
|
value="<%= slide.subtitle || '' %>" placeholder="e.g., Global Education Simplified"
|
|
maxlength="48" data-maxlength="48" />
|
|
</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" maxlength="220" data-maxlength="220"><%= slide.description || '' %></textarea>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label fw-medium">Slide Background Image</label>
|
|
<small class="text-muted d-block mb-1">Ảnh này phủ toàn bộ hero. Khung desktop hiện khoảng 1512x544px, khuyến nghị upload 1920x700px hoặc lớn hơn.</small>
|
|
<div class="input-group mb-2">
|
|
<input type="text" class="form-control" id="heroSlide_<%= index %>_heroImage"
|
|
value="<%= slide.heroImage || '' %>" placeholder="/uploads/home/hero-slide-01.jpg"
|
|
maxlength="255" data-maxlength="255" />
|
|
<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>
|
|
<small class="text-muted d-block mb-1">Frontend hiện không render video trong hero. Giữ lại chỉ để tránh mất dữ liệu cũ.</small>
|
|
<input type="text" class="form-control" id="heroSlide_<%= index %>_videoUrl"
|
|
value="<%= slide.videoUrl || '' %>" placeholder="Không bắt buộc"
|
|
maxlength="255" data-maxlength="255" />
|
|
</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"
|
|
maxlength="32" data-maxlength="32" />
|
|
</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"
|
|
maxlength="255" data-maxlength="255" />
|
|
</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"
|
|
maxlength="32" data-maxlength="32" />
|
|
</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"
|
|
maxlength="255" data-maxlength="255" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</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();
|
|
if (typeof initHomeCharacterCounters === "function") {
|
|
initHomeCharacterCounters(clone);
|
|
}
|
|
});
|
|
|
|
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();
|
|
if (typeof initHomeCharacterCounters === "function") {
|
|
initHomeCharacterCounters(container);
|
|
}
|
|
});
|
|
</script>
|