Files
cms.uldp.edu.vn/views/admin/home/sections/hero.ejs
2026-04-10 15:55:15 +07:00

343 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="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">
Optional fallback. The hero desktop frame currently displays approximately 1512x544px, so upload a landscape image of at least 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>Use landscape slide images; prefer large images to fit the container.</li>
<li>Keep titles to 2-4 lines to avoid overflow on mobile.</li>
<li>Limit descriptions to 1-3 short sentences.</li>
<li>Both buttons should use internal links like <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">Currently not rendered on the frontend; kept for backward compatibility with existing data.</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">Recommended upload size is 1920x700px or larger.</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">The frontend currently does not render video in the hero. Kept only to preserve existing data.</small>
<input type="text" class="form-control" id="heroSlide_<%= index %>_videoUrl"
value="<%= slide.videoUrl || '' %>" placeholder="Optional"
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>