forked from UKSOURCE/cms.lams
Fix: migrate footer, delete more file view
This commit is contained in:
@@ -29,7 +29,7 @@ async function migrateFooterData() {
|
|||||||
// Đọc file JSON
|
// Đọc file JSON
|
||||||
const footerData = JSON.parse(
|
const footerData = JSON.parse(
|
||||||
await fs.readFile(
|
await fs.readFile(
|
||||||
path.join(__dirname, '../data/new/footer.json'),
|
path.join(__dirname, '../data/footer.json'),
|
||||||
'utf8'
|
'utf8'
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -212,13 +212,13 @@
|
|||||||
<span>Menu Header API</span>
|
<span>Menu Header API</span>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td><code>/api/header</code></td>
|
<td><code>/api/header-menu</code></td>
|
||||||
<td>
|
<td>
|
||||||
<span class="badge" style="background-color: var(--primary-color)">GET</span>
|
<span class="badge" style="background-color: var(--primary-color)">GET</span>
|
||||||
</td>
|
</td>
|
||||||
<td>API to get menu header data</td>
|
<td>API to get menu header data</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="/api/header" class="btn btn-sm btn-outline-primary" target="_blank">
|
<a href="/api/header-menu" class="btn btn-sm btn-outline-primary" target="_blank">
|
||||||
<i class="fas fa-external-link-alt me-1"></i>View
|
<i class="fas fa-external-link-alt me-1"></i>View
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
@@ -385,30 +385,6 @@
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
|
|
||||||
<!--Blog Details API-->
|
|
||||||
<!-- <tr>
|
|
||||||
<td>
|
|
||||||
<div class="d-flex align-items-center">
|
|
||||||
<div class="rounded-circle d-flex align-items-center justify-content-center me-2"
|
|
||||||
style="width: 32px; height: 32px; background-color: rgba(184, 183, 106, 0.1);">
|
|
||||||
<i class="fas fa-blog" style="color: var(--primary-color);"></i>
|
|
||||||
</div>
|
|
||||||
<span>Blog API</span>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td><code>/api/blog</code></td>
|
|
||||||
<td>
|
|
||||||
<span class="badge" style="background-color: var(--primary-color)">GET</span>
|
|
||||||
</td>
|
|
||||||
<td>List published blog posts (supports ?page, ?category, ?search)</td>
|
|
||||||
<td>
|
|
||||||
<a href="/api/blog" class="btn btn-sm btn-outline-primary" target="_blank">
|
|
||||||
<i class="fas fa-external-link-alt me-1"></i>View
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
</tr> -->
|
|
||||||
|
|
||||||
<!--Programmes API-->
|
<!--Programmes API-->
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
@@ -431,29 +407,6 @@
|
|||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
<!--Programmes Details API-->
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<div class="d-flex align-items-center">
|
|
||||||
<div class="rounded-circle d-flex align-items-center justify-content-center me-2"
|
|
||||||
style="width: 32px; height: 32px; background-color: rgba(184, 183, 106, 0.1);">
|
|
||||||
<i class="fas fa-graduation-cap" style="color: var(--primary-color);"></i>
|
|
||||||
</div>
|
|
||||||
<span>Programme Detail API</span>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td><code>/api/programmes/:id</code></td>
|
|
||||||
<td>
|
|
||||||
<span class="badge" style="background-color: var(--primary-color)">GET</span>
|
|
||||||
</td>
|
|
||||||
<td>Single programme by slug ID</td>
|
|
||||||
<td>
|
|
||||||
<a href="/api/programmes" class="btn btn-sm btn-outline-primary" target="_blank">
|
|
||||||
<i class="fas fa-external-link-alt me-1"></i>View
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,742 +0,0 @@
|
|||||||
<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 Pricing page</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<a href="<%= frontendUrl %>/pricing/" class="btn btn-outline-primary" target="_blank">
|
|
||||||
<i class="fas fa-external-link-alt me-2"></i>View Pricing Page
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-12">
|
|
||||||
<form method="POST" class="content-with-fixed-buttons" id="pricingForm" action="/admin/pricing/update">
|
|
||||||
<!-- Hidden inputs for JSON data -->
|
|
||||||
<input type="hidden" name="hero" id="heroJson">
|
|
||||||
<input type="hidden" name="pricingSection" id="pricingSectionJson">
|
|
||||||
<input type="hidden" name="plans" id="plansJson">
|
|
||||||
<input type="hidden" name="testimonials" id="testimonialsJson">
|
|
||||||
|
|
||||||
<!-- 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="#pricingSection" role="tab">
|
|
||||||
<i class="fas fa-tags me-2"></i>Pricing Section
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link" data-bs-toggle="tab" href="#plans" role="tab">
|
|
||||||
<i class="fas fa-dollar-sign me-2"></i>Plans
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link" data-bs-toggle="tab" href="#testimonials" role="tab">
|
|
||||||
<i class="fas fa-quote-right me-2"></i>Testimonials
|
|
||||||
</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">
|
|
||||||
<h6 class="fw-medium mb-3">Hero Section</h6>
|
|
||||||
<div class="row g-3">
|
|
||||||
<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 || '' %>">
|
|
||||||
<button type="button"
|
|
||||||
class="btn btn-outline-primary btn-upload-image"
|
|
||||||
data-target-input="heroBackgroundImage"
|
|
||||||
data-image-type="pricing">
|
|
||||||
<i class="fas fa-upload me-1"></i>Upload
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<small class="text-muted">Recommended size: 1920x1080px</small>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-7">
|
|
||||||
<div id="heroImagePreview" style="height: 200px;">
|
|
||||||
<% 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: 200px; 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: 200px; display: none; align-items: center; justify-content: center;">
|
|
||||||
Image preview
|
|
||||||
</div>
|
|
||||||
<% } else { %>
|
|
||||||
<div class="border rounded p-5 text-center text-muted"
|
|
||||||
style="height: 200px; display: flex; align-items: center; justify-content: center;">
|
|
||||||
Image preview
|
|
||||||
</div>
|
|
||||||
<% } %>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row g-3 mt-2">
|
|
||||||
<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 || '' %>">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Pricing Section Tab -->
|
|
||||||
<div class="tab-pane fade" id="pricingSection" role="tabpanel">
|
|
||||||
<div class="card border shadow-sm">
|
|
||||||
<div class="card-body">
|
|
||||||
<h6 class="fw-medium mb-3">Pricing Section Header</h6>
|
|
||||||
<div class="row g-3">
|
|
||||||
<div class="col-md-6">
|
|
||||||
<label class="form-label fw-medium">Subtitle</label>
|
|
||||||
<input type="text" class="form-control" id="pricingSectionSubtitle"
|
|
||||||
value="<%= data.pricingSection?.subtitle || '' %>">
|
|
||||||
</div>
|
|
||||||
<div class="col-md-6">
|
|
||||||
<label class="form-label fw-medium">Heading</label>
|
|
||||||
<input type="text" class="form-control" id="pricingSectionHeading"
|
|
||||||
value="<%= data.pricingSection?.heading || '' %>">
|
|
||||||
</div>
|
|
||||||
<div class="col-md-12">
|
|
||||||
<label class="form-label fw-medium">Description</label>
|
|
||||||
<textarea class="form-control" id="pricingSectionDescription"
|
|
||||||
rows="3"><%= data.pricingSection?.description || '' %></textarea>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Plans Tab -->
|
|
||||||
<div class="tab-pane fade" id="plans" role="tabpanel">
|
|
||||||
<!-- Monthly Plans -->
|
|
||||||
<div class="card border shadow-sm mb-4">
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
|
||||||
<h6 class="fw-medium mb-0">Monthly Plans</h6>
|
|
||||||
<button type="button" class="btn btn-primary btn-sm"
|
|
||||||
onclick="addPlan('monthly')">
|
|
||||||
<i class="fas fa-plus"></i> Add Plan
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div id="monthlyPlansContainer">
|
|
||||||
<% if (data.plans?.monthly && data.plans.monthly.length> 0) { %>
|
|
||||||
<% data.plans.monthly.forEach((plan, index)=> { %>
|
|
||||||
<div class="card mb-3 plan-item" data-type="monthly">
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="row g-3">
|
|
||||||
<div class="col-md-3">
|
|
||||||
<label class="form-label">Plan Name</label>
|
|
||||||
<input type="text" class="form-control plan-name"
|
|
||||||
value="<%= plan.name || '' %>">
|
|
||||||
</div>
|
|
||||||
<div class="col-md-2">
|
|
||||||
<label class="form-label">Price</label>
|
|
||||||
<input type="text" class="form-control plan-price"
|
|
||||||
value="<%= plan.price || '' %>">
|
|
||||||
</div>
|
|
||||||
<div class="col-md-2">
|
|
||||||
<label class="form-label">Currency</label>
|
|
||||||
<input type="text"
|
|
||||||
class="form-control plan-currency"
|
|
||||||
value="<%= plan.currency || '$' %>">
|
|
||||||
</div>
|
|
||||||
<div class="col-md-2">
|
|
||||||
<label class="form-label">Period</label>
|
|
||||||
<input type="text" class="form-control plan-period"
|
|
||||||
value="<%= plan.period || 'mo' %>">
|
|
||||||
</div>
|
|
||||||
<div class="col-md-3">
|
|
||||||
<label class="form-label">Style</label>
|
|
||||||
<select class="form-select plan-style">
|
|
||||||
<option value="default"
|
|
||||||
<%=plan.style==='default' ? 'selected' : ''
|
|
||||||
%>>Default</option>
|
|
||||||
<option value="style-2"
|
|
||||||
<%=plan.style==='style-2' ? 'selected' : ''
|
|
||||||
%>>Style 2 (Featured)</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-4">
|
|
||||||
<label class="form-label">Button Text</label>
|
|
||||||
<input type="text"
|
|
||||||
class="form-control plan-button-text"
|
|
||||||
value="<%= plan.buttonText || 'Get Started Today' %>">
|
|
||||||
</div>
|
|
||||||
<div class="col-md-4">
|
|
||||||
<label class="form-label">Button Link</label>
|
|
||||||
<input type="text"
|
|
||||||
class="form-control plan-button-link"
|
|
||||||
value="<%= plan.buttonLink || '/pricing' %>">
|
|
||||||
</div>
|
|
||||||
<div class="col-md-4">
|
|
||||||
<label class="form-label">Button Icon</label>
|
|
||||||
<input type="text"
|
|
||||||
class="form-control plan-button-icon"
|
|
||||||
value="<%= plan.buttonIcon || 'fa-solid fa-arrow-right' %>">
|
|
||||||
</div>
|
|
||||||
<div class="col-md-12">
|
|
||||||
<label class="form-label">Features (one per
|
|
||||||
line)</label>
|
|
||||||
<textarea class="form-control plan-features"
|
|
||||||
rows="4"><%= (plan.features || []).join('\n') %></textarea>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button type="button"
|
|
||||||
class="btn btn-outline-danger btn-sm mt-3"
|
|
||||||
onclick="removePlan(this)">
|
|
||||||
<i class="fas fa-trash me-2"></i>Remove Plan
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<% }); %>
|
|
||||||
<% } %>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Yearly Plans -->
|
|
||||||
<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">Yearly Plans</h6>
|
|
||||||
<button type="button" class="btn btn-primary btn-sm"
|
|
||||||
onclick="addPlan('yearly')">
|
|
||||||
<i class="fas fa-plus"></i> Add Plan
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div id="yearlyPlansContainer">
|
|
||||||
<% if (data.plans?.yearly && data.plans.yearly.length> 0) { %>
|
|
||||||
<% data.plans.yearly.forEach((plan, index)=> { %>
|
|
||||||
<div class="card mb-3 plan-item" data-type="yearly">
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="row g-3">
|
|
||||||
<div class="col-md-3">
|
|
||||||
<label class="form-label">Plan Name</label>
|
|
||||||
<input type="text" class="form-control plan-name"
|
|
||||||
value="<%= plan.name || '' %>">
|
|
||||||
</div>
|
|
||||||
<div class="col-md-2">
|
|
||||||
<label class="form-label">Price</label>
|
|
||||||
<input type="text" class="form-control plan-price"
|
|
||||||
value="<%= plan.price || '' %>">
|
|
||||||
</div>
|
|
||||||
<div class="col-md-2">
|
|
||||||
<label class="form-label">Currency</label>
|
|
||||||
<input type="text"
|
|
||||||
class="form-control plan-currency"
|
|
||||||
value="<%= plan.currency || '$' %>">
|
|
||||||
</div>
|
|
||||||
<div class="col-md-2">
|
|
||||||
<label class="form-label">Period</label>
|
|
||||||
<input type="text" class="form-control plan-period"
|
|
||||||
value="<%= plan.period || 'mo' %>">
|
|
||||||
</div>
|
|
||||||
<div class="col-md-3">
|
|
||||||
<label class="form-label">Style</label>
|
|
||||||
<select class="form-select plan-style">
|
|
||||||
<option value="default"
|
|
||||||
<%=plan.style==='default' ? 'selected' : ''
|
|
||||||
%>>Default</option>
|
|
||||||
<option value="style-2"
|
|
||||||
<%=plan.style==='style-2' ? 'selected' : ''
|
|
||||||
%>>Style 2 (Featured)</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-4">
|
|
||||||
<label class="form-label">Button Text</label>
|
|
||||||
<input type="text"
|
|
||||||
class="form-control plan-button-text"
|
|
||||||
value="<%= plan.buttonText || 'Get Started Today' %>">
|
|
||||||
</div>
|
|
||||||
<div class="col-md-4">
|
|
||||||
<label class="form-label">Button Link</label>
|
|
||||||
<input type="text"
|
|
||||||
class="form-control plan-button-link"
|
|
||||||
value="<%= plan.buttonLink || '/pricing' %>">
|
|
||||||
</div>
|
|
||||||
<div class="col-md-4">
|
|
||||||
<label class="form-label">Button Icon</label>
|
|
||||||
<input type="text"
|
|
||||||
class="form-control plan-button-icon"
|
|
||||||
value="<%= plan.buttonIcon || 'fa-solid fa-arrow-right' %>">
|
|
||||||
</div>
|
|
||||||
<div class="col-md-12">
|
|
||||||
<label class="form-label">Features (one per
|
|
||||||
line)</label>
|
|
||||||
<textarea class="form-control plan-features"
|
|
||||||
rows="4"><%= (plan.features || []).join('\n') %></textarea>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button type="button"
|
|
||||||
class="btn btn-outline-danger btn-sm mt-3"
|
|
||||||
onclick="removePlan(this)">
|
|
||||||
<i class="fas fa-trash me-2"></i>Remove Plan
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<% }); %>
|
|
||||||
<% } %>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Testimonials Tab -->
|
|
||||||
<div class="tab-pane fade" id="testimonials" role="tabpanel">
|
|
||||||
<div class="card border shadow-sm mb-4">
|
|
||||||
<div class="card-body">
|
|
||||||
<h6 class="fw-medium mb-3">Testimonials Section Header</h6>
|
|
||||||
<div class="row g-3">
|
|
||||||
<div class="col-md-6">
|
|
||||||
<label class="form-label fw-medium">Subtitle</label>
|
|
||||||
<input type="text" class="form-control" id="testimonialsSubtitle"
|
|
||||||
value="<%= data.testimonials?.subtitle || '' %>">
|
|
||||||
</div>
|
|
||||||
<div class="col-md-6">
|
|
||||||
<label class="form-label fw-medium">Heading</label>
|
|
||||||
<input type="text" class="form-control" id="testimonialsHeading"
|
|
||||||
value="<%= data.testimonials?.heading || '' %>">
|
|
||||||
</div>
|
|
||||||
<div class="col-md-4">
|
|
||||||
<label class="form-label fw-medium">Button Text</label>
|
|
||||||
<input type="text" class="form-control" id="testimonialsButtonText"
|
|
||||||
value="<%= data.testimonials?.buttonText || '' %>">
|
|
||||||
</div>
|
|
||||||
<div class="col-md-4">
|
|
||||||
<label class="form-label fw-medium">Button Link</label>
|
|
||||||
<input type="text" class="form-control" id="testimonialsButtonLink"
|
|
||||||
value="<%= data.testimonials?.buttonLink || '' %>">
|
|
||||||
</div>
|
|
||||||
<div class="col-md-4">
|
|
||||||
<label class="form-label fw-medium">Section Image</label>
|
|
||||||
<div class="input-group">
|
|
||||||
<input type="text" class="form-control" id="testimonialsImage"
|
|
||||||
value="<%= data.testimonials?.image || '' %>">
|
|
||||||
<button type="button"
|
|
||||||
class="btn btn-outline-primary btn-upload-image"
|
|
||||||
data-target-input="testimonialsImage" data-image-type="pricing">
|
|
||||||
<i class="fas fa-upload"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<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">Testimonial Items</h6>
|
|
||||||
<button type="button" class="btn btn-primary btn-sm"
|
|
||||||
onclick="addTestimonial()">
|
|
||||||
<i class="fas fa-plus"></i> Add Testimonial
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div id="testimonialsContainer">
|
|
||||||
<% if (data.testimonials?.items && data.testimonials.items.length> 0) { %>
|
|
||||||
<% data.testimonials.items.forEach((item, index)=> { %>
|
|
||||||
<div class="card mb-3 testimonial-item">
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="row g-3">
|
|
||||||
<div class="col-md-4">
|
|
||||||
<label class="form-label">Name</label>
|
|
||||||
<input type="text"
|
|
||||||
class="form-control testimonial-name"
|
|
||||||
value="<%= item.name || '' %>">
|
|
||||||
</div>
|
|
||||||
<div class="col-md-4">
|
|
||||||
<label class="form-label">Role/Type</label>
|
|
||||||
<input type="text"
|
|
||||||
class="form-control testimonial-role"
|
|
||||||
value="<%= item.role || '' %>">
|
|
||||||
</div>
|
|
||||||
<div class="col-md-4">
|
|
||||||
<label class="form-label">Rating</label>
|
|
||||||
<select class="form-select testimonial-rating">
|
|
||||||
<option value="1" <%=item.rating===1
|
|
||||||
? 'selected' : '' %>>1 Star</option>
|
|
||||||
<option value="2" <%=item.rating===2
|
|
||||||
? 'selected' : '' %>>2 Stars</option>
|
|
||||||
<option value="3" <%=item.rating===3
|
|
||||||
? 'selected' : '' %>>3 Stars</option>
|
|
||||||
<option value="4" <%=item.rating===4
|
|
||||||
? 'selected' : '' %>>4 Stars</option>
|
|
||||||
<option value="5" <%=item.rating===5
|
|
||||||
? 'selected' : '' %>>5 Stars</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-12">
|
|
||||||
<label class="form-label">Content</label>
|
|
||||||
<textarea class="form-control testimonial-content"
|
|
||||||
rows="3"><%= item.content || '' %></textarea>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button type="button"
|
|
||||||
class="btn btn-outline-danger btn-sm mt-3"
|
|
||||||
onclick="removeTestimonial(this)">
|
|
||||||
<i class="fas fa-trash me-2"></i>Remove Testimonial
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<% }); %>
|
|
||||||
<% } %>
|
|
||||||
</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>
|
|
||||||
|
|
||||||
<script type="application/json" id="pricingDataJson"><%- JSON.stringify(data) %></script>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
let originalFormData = null;
|
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
|
||||||
try {
|
|
||||||
var jsonScript = document.getElementById('pricingDataJson');
|
|
||||||
originalFormData = JSON.parse(jsonScript.textContent);
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Error parsing originalFormData:', e);
|
|
||||||
originalFormData = {};
|
|
||||||
}
|
|
||||||
updateAllJsonInputs();
|
|
||||||
initializeFormHandlers();
|
|
||||||
});
|
|
||||||
|
|
||||||
function initializeFormHandlers() {
|
|
||||||
const form = document.getElementById('pricingForm');
|
|
||||||
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 {
|
|
||||||
updateJsonData();
|
|
||||||
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';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Image upload buttons
|
|
||||||
document.querySelectorAll('.btn-upload-image').forEach(button => {
|
|
||||||
button.addEventListener('click', function () {
|
|
||||||
const targetInput = this.dataset.targetInput;
|
|
||||||
const imageType = this.dataset.imageType;
|
|
||||||
openImageUploader(targetInput, imageType);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Update preview when background image changes
|
|
||||||
document.getElementById('heroBackgroundImage').addEventListener('input', function () {
|
|
||||||
updateHeroImagePreview(this.value);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateHeroImagePreview(imagePath) {
|
|
||||||
const previewContainer = document.getElementById('heroImagePreview');
|
|
||||||
if (imagePath) {
|
|
||||||
let imgSrc = imagePath;
|
|
||||||
if (!imgSrc.startsWith('http://') && !imgSrc.startsWith('https://')) {
|
|
||||||
imgSrc = imgSrc.startsWith('/') ? imgSrc : '/' + imgSrc;
|
|
||||||
}
|
|
||||||
previewContainer.innerHTML = `
|
|
||||||
<img src="${imgSrc}" class="img-thumbnail" id="heroPreviewImg"
|
|
||||||
style="height: 200px; 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: 200px; display: none; align-items: center; justify-content: center;">
|
|
||||||
Image preview
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
} else {
|
|
||||||
previewContainer.innerHTML = `
|
|
||||||
<div class="border rounded p-5 text-center text-muted"
|
|
||||||
style="height: 200px; display: flex; align-items: center; justify-content: center;">
|
|
||||||
Image preview
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateAllJsonInputs() {
|
|
||||||
updateJsonData();
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateJsonData() {
|
|
||||||
// Hero data
|
|
||||||
const heroData = {
|
|
||||||
title: document.getElementById('heroTitle').value || '',
|
|
||||||
backgroundImage: document.getElementById('heroBackgroundImage').value || '',
|
|
||||||
shapeImage: originalFormData?.hero?.shapeImage || '/assets/img/inner-page/shape.png',
|
|
||||||
breadcrumb: originalFormData?.hero?.breadcrumb || [],
|
|
||||||
};
|
|
||||||
document.getElementById('heroJson').value = JSON.stringify(heroData);
|
|
||||||
|
|
||||||
// Pricing Section data
|
|
||||||
const pricingSectionData = {
|
|
||||||
subtitle: document.getElementById('pricingSectionSubtitle').value || '',
|
|
||||||
heading: document.getElementById('pricingSectionHeading').value || '',
|
|
||||||
description: document.getElementById('pricingSectionDescription').value || '',
|
|
||||||
};
|
|
||||||
document.getElementById('pricingSectionJson').value = JSON.stringify(pricingSectionData);
|
|
||||||
|
|
||||||
// Plans data
|
|
||||||
const monthlyPlans = [];
|
|
||||||
document.querySelectorAll('#monthlyPlansContainer .plan-item').forEach(item => {
|
|
||||||
const featuresText = item.querySelector('.plan-features').value || '';
|
|
||||||
monthlyPlans.push({
|
|
||||||
name: item.querySelector('.plan-name').value || '',
|
|
||||||
price: item.querySelector('.plan-price').value || '0',
|
|
||||||
currency: item.querySelector('.plan-currency').value || '$',
|
|
||||||
period: item.querySelector('.plan-period').value || 'mo',
|
|
||||||
style: item.querySelector('.plan-style').value || 'default',
|
|
||||||
buttonText: item.querySelector('.plan-button-text').value || 'Get Started Today',
|
|
||||||
buttonLink: item.querySelector('.plan-button-link').value || '/pricing',
|
|
||||||
buttonIcon: item.querySelector('.plan-button-icon').value || 'fa-solid fa-arrow-right',
|
|
||||||
features: featuresText.split('\n').filter(f => f.trim()),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const yearlyPlans = [];
|
|
||||||
document.querySelectorAll('#yearlyPlansContainer .plan-item').forEach(item => {
|
|
||||||
const featuresText = item.querySelector('.plan-features').value || '';
|
|
||||||
yearlyPlans.push({
|
|
||||||
name: item.querySelector('.plan-name').value || '',
|
|
||||||
price: item.querySelector('.plan-price').value || '0',
|
|
||||||
currency: item.querySelector('.plan-currency').value || '$',
|
|
||||||
period: item.querySelector('.plan-period').value || 'mo',
|
|
||||||
style: item.querySelector('.plan-style').value || 'default',
|
|
||||||
buttonText: item.querySelector('.plan-button-text').value || 'Get Started Today',
|
|
||||||
buttonLink: item.querySelector('.plan-button-link').value || '/pricing',
|
|
||||||
buttonIcon: item.querySelector('.plan-button-icon').value || 'fa-solid fa-arrow-right',
|
|
||||||
features: featuresText.split('\n').filter(f => f.trim()),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById('plansJson').value = JSON.stringify({
|
|
||||||
monthly: monthlyPlans,
|
|
||||||
yearly: yearlyPlans,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Testimonials data
|
|
||||||
const testimonialItems = [];
|
|
||||||
document.querySelectorAll('#testimonialsContainer .testimonial-item').forEach(item => {
|
|
||||||
testimonialItems.push({
|
|
||||||
name: item.querySelector('.testimonial-name').value || '',
|
|
||||||
role: item.querySelector('.testimonial-role').value || '',
|
|
||||||
rating: parseInt(item.querySelector('.testimonial-rating').value) || 5,
|
|
||||||
content: item.querySelector('.testimonial-content').value || '',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const testimonialsData = {
|
|
||||||
subtitle: document.getElementById('testimonialsSubtitle').value || '',
|
|
||||||
heading: document.getElementById('testimonialsHeading').value || '',
|
|
||||||
buttonText: document.getElementById('testimonialsButtonText').value || '',
|
|
||||||
buttonLink: document.getElementById('testimonialsButtonLink').value || '',
|
|
||||||
buttonIcon: originalFormData?.testimonials?.buttonIcon || 'fa-solid fa-arrow-right',
|
|
||||||
image: document.getElementById('testimonialsImage').value || '',
|
|
||||||
items: testimonialItems,
|
|
||||||
};
|
|
||||||
document.getElementById('testimonialsJson').value = JSON.stringify(testimonialsData);
|
|
||||||
}
|
|
||||||
|
|
||||||
function addPlan(type) {
|
|
||||||
const container = document.getElementById(type + 'PlansContainer');
|
|
||||||
const html = `
|
|
||||||
<div class="card mb-3 plan-item" data-type="${type}">
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="row g-3">
|
|
||||||
<div class="col-md-3">
|
|
||||||
<label class="form-label">Plan Name</label>
|
|
||||||
<input type="text" class="form-control plan-name" value="">
|
|
||||||
</div>
|
|
||||||
<div class="col-md-2">
|
|
||||||
<label class="form-label">Price</label>
|
|
||||||
<input type="text" class="form-control plan-price" value="">
|
|
||||||
</div>
|
|
||||||
<div class="col-md-2">
|
|
||||||
<label class="form-label">Currency</label>
|
|
||||||
<input type="text" class="form-control plan-currency" value="$">
|
|
||||||
</div>
|
|
||||||
<div class="col-md-2">
|
|
||||||
<label class="form-label">Period</label>
|
|
||||||
<input type="text" class="form-control plan-period" value="mo">
|
|
||||||
</div>
|
|
||||||
<div class="col-md-3">
|
|
||||||
<label class="form-label">Style</label>
|
|
||||||
<select class="form-select plan-style">
|
|
||||||
<option value="default" selected>Default</option>
|
|
||||||
<option value="style-2">Style 2 (Featured)</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-4">
|
|
||||||
<label class="form-label">Button Text</label>
|
|
||||||
<input type="text" class="form-control plan-button-text" value="Get Started Today">
|
|
||||||
</div>
|
|
||||||
<div class="col-md-4">
|
|
||||||
<label class="form-label">Button Link</label>
|
|
||||||
<input type="text" class="form-control plan-button-link" value="/pricing">
|
|
||||||
</div>
|
|
||||||
<div class="col-md-4">
|
|
||||||
<label class="form-label">Button Icon</label>
|
|
||||||
<input type="text" class="form-control plan-button-icon" value="fa-solid fa-arrow-right">
|
|
||||||
</div>
|
|
||||||
<div class="col-md-12">
|
|
||||||
<label class="form-label">Features (one per line)</label>
|
|
||||||
<textarea class="form-control plan-features" rows="4"></textarea>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button type="button" class="btn btn-outline-danger btn-sm mt-3" onclick="removePlan(this)">
|
|
||||||
<i class="fas fa-trash me-2"></i>Remove Plan
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
container.insertAdjacentHTML('beforeend', html);
|
|
||||||
}
|
|
||||||
|
|
||||||
function removePlan(button) {
|
|
||||||
if (confirm('Are you sure you want to remove this plan?')) {
|
|
||||||
button.closest('.plan-item').remove();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function addTestimonial() {
|
|
||||||
const container = document.getElementById('testimonialsContainer');
|
|
||||||
const html = `
|
|
||||||
<div class="card mb-3 testimonial-item">
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="row g-3">
|
|
||||||
<div class="col-md-4">
|
|
||||||
<label class="form-label">Name</label>
|
|
||||||
<input type="text" class="form-control testimonial-name" value="">
|
|
||||||
</div>
|
|
||||||
<div class="col-md-4">
|
|
||||||
<label class="form-label">Role/Type</label>
|
|
||||||
<input type="text" class="form-control testimonial-role" value="">
|
|
||||||
</div>
|
|
||||||
<div class="col-md-4">
|
|
||||||
<label class="form-label">Rating</label>
|
|
||||||
<select class="form-select testimonial-rating">
|
|
||||||
<option value="1">1 Star</option>
|
|
||||||
<option value="2">2 Stars</option>
|
|
||||||
<option value="3">3 Stars</option>
|
|
||||||
<option value="4">4 Stars</option>
|
|
||||||
<option value="5" selected>5 Stars</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-12">
|
|
||||||
<label class="form-label">Content</label>
|
|
||||||
<textarea class="form-control testimonial-content" rows="3"></textarea>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button type="button" class="btn btn-outline-danger btn-sm mt-3" onclick="removeTestimonial(this)">
|
|
||||||
<i class="fas fa-trash me-2"></i>Remove Testimonial
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
container.insertAdjacentHTML('beforeend', html);
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeTestimonial(button) {
|
|
||||||
if (confirm('Are you sure you want to remove this testimonial?')) {
|
|
||||||
button.closest('.testimonial-item').remove();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function resetForm() {
|
|
||||||
if (confirm('Are you sure you want to reset all changes?')) {
|
|
||||||
location.reload();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Image uploader function
|
|
||||||
function openImageUploader(targetInput, imageType) {
|
|
||||||
const input = document.createElement('input');
|
|
||||||
input.type = 'file';
|
|
||||||
input.accept = 'image/*';
|
|
||||||
input.onchange = async function (e) {
|
|
||||||
const file = e.target.files[0];
|
|
||||||
if (!file) return;
|
|
||||||
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append('image', file);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Send imageType via query string as controller expects req.query.imageType
|
|
||||||
const uploadUrl = '/admin/upload/image?imageType=' + encodeURIComponent(imageType || 'general');
|
|
||||||
const response = await fetch(uploadUrl, {
|
|
||||||
method: 'POST',
|
|
||||||
body: formData
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await response.json();
|
|
||||||
if (result.success && result.path) {
|
|
||||||
document.getElementById(targetInput).value = result.path;
|
|
||||||
if (targetInput === 'heroBackgroundImage') {
|
|
||||||
updateHeroImagePreview(result.path);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
alert('Upload failed: ' + (result.error || 'Unknown error'));
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Upload error:', error);
|
|
||||||
alert('Upload failed. Please try again.');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
input.click();
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@@ -1,755 +0,0 @@
|
|||||||
<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);">Service Details: <%= service.name %></h1>
|
|
||||||
<p class="text-muted mb-0">Edit detailed content for <%= service.name %> service</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<a href="/admin/service" class="btn btn-outline-primary">
|
|
||||||
<i class="fas fa-arrow-left me-2"></i>Back to Services
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-12">
|
|
||||||
<form action="/admin/service/<%= service.slug %>/details/update" method="POST" class="content-with-fixed-buttons" id="serviceDetailsForm">
|
|
||||||
<!-- Hidden inputs for JSON data -->
|
|
||||||
<input type="hidden" name="details" id="detailsJson">
|
|
||||||
<input type="hidden" name="features" id="featuresJson">
|
|
||||||
<input type="hidden" name="faq" id="faqJson">
|
|
||||||
|
|
||||||
<!-- 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="#basic-info" role="tab">
|
|
||||||
<i class="fas fa-info-circle me-2"></i>Basic Information
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link" data-bs-toggle="tab" href="#key-features" role="tab">
|
|
||||||
<i class="fas fa-star me-2"></i>Key Features
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link" data-bs-toggle="tab" href="#faq-section" role="tab">
|
|
||||||
<i class="fas fa-question-circle me-2"></i>FAQ Section
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="tab-content">
|
|
||||||
<!-- Basic Information Tab -->
|
|
||||||
<div class="tab-pane fade show active" id="basic-info" 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">Main Image</label>
|
|
||||||
<div class="input-group mb-2">
|
|
||||||
<input type="text" class="form-control" id="mainImage" name="mainImage"
|
|
||||||
value="<%= service.details?.mainImage || '' %>">
|
|
||||||
<button type="button" class="btn btn-outline-primary btn-upload-image"
|
|
||||||
data-target-input="mainImage" data-image-type="service">
|
|
||||||
<i class="fas fa-upload me-1"></i>Upload
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<small class="form-text text-muted">Recommended size: 800x600px</small>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-7">
|
|
||||||
<div id="mainImagePreview">
|
|
||||||
<% if (service.details?.mainImage) { %>
|
|
||||||
<img src="<%= getFullImageUrl(service.details.mainImage) %>" class="img-thumbnail" style="max-height: 250px; width: auto; max-width: 100%; object-fit: contain;" alt="Main image preview">
|
|
||||||
<% } %>
|
|
||||||
</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="detailsTitle" name="title"
|
|
||||||
value="<%= service.details?.title || service.name %>">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row mt-3">
|
|
||||||
<div class="col-md-12">
|
|
||||||
<label class="form-label fw-medium">Description</label>
|
|
||||||
<textarea class="form-control" id="detailsDescription" name="description" rows="3"><%= service.details?.description || service.description %></textarea>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Overview Section -->
|
|
||||||
<div class="row mt-4">
|
|
||||||
<div class="col-12">
|
|
||||||
<div class="card border">
|
|
||||||
<div class="card-header bg-light">
|
|
||||||
<h6 class="mb-0">Overview Section</h6>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="row g-3">
|
|
||||||
<div class="col-md-12">
|
|
||||||
<label class="form-label">Overview Title</label>
|
|
||||||
<input type="text" class="form-control" id="overviewTitle" name="overviewTitle"
|
|
||||||
value="<%= service.details?.overviewTitle || 'Service Overview' %>">
|
|
||||||
</div>
|
|
||||||
<div class="col-md-12">
|
|
||||||
<label class="form-label">Overview Description</label>
|
|
||||||
<textarea class="form-control" id="overviewDescription" name="overviewDescription" rows="4"><%= service.details?.overviewDescription || '' %></textarea>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-12">
|
|
||||||
<label class="form-label">Additional Description</label>
|
|
||||||
<textarea class="form-control" id="additionalDescription" name="additionalDescription" rows="3"><%= service.details?.additionalDescription || '' %></textarea>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Key Features Tab -->
|
|
||||||
<div class="tab-pane fade" id="key-features" role="tabpanel">
|
|
||||||
<div class="card border shadow-sm">
|
|
||||||
<div class="card-body">
|
|
||||||
<!-- Features Header -->
|
|
||||||
<div class="row mb-4">
|
|
||||||
<div class="col-md-6">
|
|
||||||
<label class="form-label fw-medium">Features Title</label>
|
|
||||||
<input type="text" class="form-control" id="keyFeaturesTitle" name="keyFeaturesTitle"
|
|
||||||
value="<%= service.details?.keyFeaturesTitle || 'Key Features' %>">
|
|
||||||
</div>
|
|
||||||
<div class="col-md-6">
|
|
||||||
<label class="form-label fw-medium">Features Image</label>
|
|
||||||
<div class="input-group">
|
|
||||||
<input type="text" class="form-control" id="keyFeaturesImage" name="keyFeaturesImage"
|
|
||||||
value="<%= service.details?.keyFeaturesImage || '' %>">
|
|
||||||
<button type="button" class="btn btn-outline-primary btn-upload-image"
|
|
||||||
data-target-input="keyFeaturesImage" data-image-type="service">
|
|
||||||
<i class="fas fa-upload me-1"></i>Upload
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Key Features Image Preview -->
|
|
||||||
<div class="row mt-3">
|
|
||||||
<div class="col-12">
|
|
||||||
<div id="keyFeaturesImagePreview">
|
|
||||||
<% if (service.details?.keyFeaturesImage) { %>
|
|
||||||
<img src="<%= getFullImageUrl(service.details.keyFeaturesImage) %>" class="img-thumbnail" style="max-height: 180px; width: auto; max-width: 100%; object-fit: contain;" alt="Key Features image preview">
|
|
||||||
<% } %>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Features List -->
|
|
||||||
<div class="row mt-4">
|
|
||||||
<div class="col-12">
|
|
||||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
|
||||||
<h6 class="fw-medium mb-0">Features</h6>
|
|
||||||
<button type="button" class="btn btn-primary btn-sm" onclick="addFeature()">
|
|
||||||
<i class="fas fa-plus"></i> Add Feature
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div id="featuresContainer">
|
|
||||||
<% if (service.details?.features && service.details.features.length > 0) { %>
|
|
||||||
<% service.details.features.forEach((feature, index) => { %>
|
|
||||||
<div class="card mb-3 feature-item">
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
|
||||||
<h6 class="mb-0 text-decoration-underline">Feature <%= index + 1 %></h6>
|
|
||||||
<button type="button" class="btn btn-danger btn-sm" onclick="removeFeature(this)">
|
|
||||||
<i class="fas fa-trash"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="row g-3">
|
|
||||||
<div class="col-md-8">
|
|
||||||
<label class="form-label">Title</label>
|
|
||||||
<input type="text" class="form-control feature-title"
|
|
||||||
value="<%= feature.title || '' %>" required>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row g-3 mt-2">
|
|
||||||
<div class="col-md-12">
|
|
||||||
<label class="form-label">Description</label>
|
|
||||||
<textarea class="form-control feature-description" rows="2" required><%= feature.description || '' %></textarea>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<% }) %>
|
|
||||||
<% } %>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- FAQ Section Tab -->
|
|
||||||
<div class="tab-pane fade" id="faq-section" role="tabpanel">
|
|
||||||
<div class="card border shadow-sm">
|
|
||||||
<div class="card-body">
|
|
||||||
<!-- FAQ Header -->
|
|
||||||
<div class="row mb-4">
|
|
||||||
<div class="col-md-6">
|
|
||||||
<label class="form-label fw-medium">FAQ Title</label>
|
|
||||||
<input type="text" class="form-control" id="faqTitle" name="faqTitle"
|
|
||||||
value="<%= service.details?.faqTitle || 'Frequently Asked Questions' %>">
|
|
||||||
</div>
|
|
||||||
<div class="col-md-6">
|
|
||||||
<label class="form-label fw-medium">FAQ Image</label>
|
|
||||||
<div class="input-group">
|
|
||||||
<input type="text" class="form-control" id="faqImage" name="faqImage"
|
|
||||||
value="<%= service.details?.faqImage || '' %>">
|
|
||||||
<button type="button" class="btn btn-outline-primary btn-upload-image"
|
|
||||||
data-target-input="faqImage" data-image-type="service">
|
|
||||||
<i class="fas fa-upload me-1"></i>Upload
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- FAQ Image Preview -->
|
|
||||||
<div class="row mt-3">
|
|
||||||
<div class="col-12">
|
|
||||||
<div id="faqImagePreview">
|
|
||||||
<% if (service.details?.faqImage) { %>
|
|
||||||
<img src="<%= getFullImageUrl(service.details.faqImage) %>" class="img-thumbnail" style="max-height: 180px; width: auto; max-width: 100%; object-fit: contain;" alt="FAQ image preview">
|
|
||||||
<% } %>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- FAQ List -->
|
|
||||||
<div class="row mt-4">
|
|
||||||
<div class="col-12">
|
|
||||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
|
||||||
<h6 class="fw-medium mb-0">FAQ Items</h6>
|
|
||||||
<button type="button" class="btn btn-primary btn-sm" onclick="addFAQ()">
|
|
||||||
<i class="fas fa-plus"></i> Add FAQ
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div id="faqContainer">
|
|
||||||
<% if (service.details?.faq && service.details.faq.length > 0) { %>
|
|
||||||
<% service.details.faq.forEach((faq, index) => { %>
|
|
||||||
<div class="card mb-3 faq-item">
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
|
||||||
<h6 class="mb-0 text-decoration-underline">FAQ <%= index + 1 %></h6>
|
|
||||||
<button type="button" class="btn btn-danger btn-sm" onclick="removeFAQ(this)">
|
|
||||||
<i class="fas fa-trash"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="row g-3">
|
|
||||||
<div class="col-md-6">
|
|
||||||
<label class="form-label">ID (Auto-generated)</label>
|
|
||||||
<input type="text" class="form-control faq-id"
|
|
||||||
value="<%= faq.id || 'faq-' + (index + 1) %>" readonly>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-6">
|
|
||||||
<label class="form-label">Expanded by Default</label>
|
|
||||||
<select class="form-control faq-expanded">
|
|
||||||
<option value="false" <%= !faq.isExpanded ? 'selected' : '' %>>No</option>
|
|
||||||
<option value="true" <%= faq.isExpanded ? 'selected' : '' %>>Yes</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row g-3 mt-2">
|
|
||||||
<div class="col-md-12">
|
|
||||||
<label class="form-label">Question</label>
|
|
||||||
<input type="text" class="form-control faq-question"
|
|
||||||
value="<%= faq.question || '' %>" required>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row g-3 mt-2">
|
|
||||||
<div class="col-md-12">
|
|
||||||
<label class="form-label">Answer</label>
|
|
||||||
<textarea class="form-control faq-answer" rows="3" required><%= faq.answer || '' %></textarea>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<% }) %>
|
|
||||||
<% } %>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Bottom Buttons -->
|
|
||||||
<div class="bottom-buttons">
|
|
||||||
<button type="reset" class="btn btn-secondary">
|
|
||||||
<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>
|
|
||||||
<!-- Scripts -->
|
|
||||||
<script>
|
|
||||||
let originalFormData = null;
|
|
||||||
let featureIndex = <%= service.details?.features?.length || 0 %>;
|
|
||||||
let faqIndex = <%= service.details?.faq?.length || 0 %>;
|
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
// Initialize form data
|
|
||||||
originalFormData = <%- JSON.stringify(service) %>;
|
|
||||||
|
|
||||||
// Set initial JSON values
|
|
||||||
updateAllJsonInputs(originalFormData);
|
|
||||||
|
|
||||||
// Initialize form handlers
|
|
||||||
initializeFormHandlers();
|
|
||||||
});
|
|
||||||
|
|
||||||
function initializeFormHandlers() {
|
|
||||||
// Form submission
|
|
||||||
const form = document.getElementById('serviceDetailsForm');
|
|
||||||
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 {
|
|
||||||
updateJsonData();
|
|
||||||
this.submit();
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error updating data:', error);
|
|
||||||
showError('Failed to process form data. Please try again.');
|
|
||||||
submitBtn.disabled = false;
|
|
||||||
submitBtn.innerHTML = '<i class="fas fa-save me-2"></i>Save Changes';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Initialize image upload buttons
|
|
||||||
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 image input change listeners for manual URL input
|
|
||||||
const imageInputs = ['mainImage', 'keyFeaturesImage', 'faqImage'];
|
|
||||||
imageInputs.forEach(inputId => {
|
|
||||||
const input = document.getElementById(inputId);
|
|
||||||
if (input) {
|
|
||||||
input.addEventListener('input', function() {
|
|
||||||
updateImagePreviewAfterUpload(inputId, this.value);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function addFeature() {
|
|
||||||
const container = document.getElementById('featuresContainer');
|
|
||||||
const featureHtml = `
|
|
||||||
<div class="card mb-3 feature-item">
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
|
||||||
<h6 class="mb-0 text-decoration-underline">Feature ${featureIndex + 1}</h6>
|
|
||||||
<button type="button" class="btn btn-danger btn-sm" onclick="removeFeature(this)">
|
|
||||||
<i class="fas fa-trash"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="row g-3">
|
|
||||||
<div class="col-md-8">
|
|
||||||
<label class="form-label">Title</label>
|
|
||||||
<input type="text" class="form-control feature-title" required>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row g-3 mt-2">
|
|
||||||
<div class="col-md-12">
|
|
||||||
<label class="form-label">Description</label>
|
|
||||||
<textarea class="form-control feature-description" rows="2" required></textarea>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
container.insertAdjacentHTML('beforeend', featureHtml);
|
|
||||||
featureIndex++;
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeFeature(button) {
|
|
||||||
const featureItem = button.closest('.feature-item');
|
|
||||||
if (featureItem) {
|
|
||||||
featureItem.remove();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function addFAQ() {
|
|
||||||
const container = document.getElementById('faqContainer');
|
|
||||||
const newFaqId = generateFAQId();
|
|
||||||
const faqNumber = document.querySelectorAll('.faq-item').length + 1;
|
|
||||||
const faqHtml = `
|
|
||||||
<div class="card mb-3 faq-item">
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
|
||||||
<h6 class="mb-0 text-decoration-underline">FAQ ${faqNumber}</h6>
|
|
||||||
<button type="button" class="btn btn-danger btn-sm" onclick="removeFAQ(this)">
|
|
||||||
<i class="fas fa-trash"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="row g-3">
|
|
||||||
<div class="col-md-6">
|
|
||||||
<label class="form-label">ID (Auto-generated)</label>
|
|
||||||
<input type="text" class="form-control faq-id"
|
|
||||||
value="${newFaqId}" readonly>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-6">
|
|
||||||
<label class="form-label">Expanded by Default</label>
|
|
||||||
<select class="form-control faq-expanded">
|
|
||||||
<option value="false">No</option>
|
|
||||||
<option value="true">Yes</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row g-3 mt-2">
|
|
||||||
<div class="col-md-12">
|
|
||||||
<label class="form-label">Question</label>
|
|
||||||
<input type="text" class="form-control faq-question" required>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row g-3 mt-2">
|
|
||||||
<div class="col-md-12">
|
|
||||||
<label class="form-label">Answer</label>
|
|
||||||
<textarea class="form-control faq-answer" rows="3" required></textarea>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
container.insertAdjacentHTML('beforeend', faqHtml);
|
|
||||||
faqIndex++;
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateFAQId(questionInput) {
|
|
||||||
// Không cần update ID nữa vì đã tự động theo số thứ tự
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeFAQ(button) {
|
|
||||||
const faqItem = button.closest('.faq-item');
|
|
||||||
if (faqItem) {
|
|
||||||
faqItem.remove();
|
|
||||||
// Cập nhật lại số thứ tự và ID của tất cả FAQ
|
|
||||||
updateFAQNumbers();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function generateFAQId() {
|
|
||||||
// Đếm số lượng FAQ hiện tại và tạo ID tiếp theo
|
|
||||||
const existingFAQs = document.querySelectorAll('.faq-item');
|
|
||||||
const nextNumber = existingFAQs.length + 1;
|
|
||||||
return `faq-${nextNumber}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateFAQNumbers() {
|
|
||||||
// Cập nhật lại tất cả FAQ ID và số thứ tự
|
|
||||||
const faqItems = document.querySelectorAll('.faq-item');
|
|
||||||
faqItems.forEach((item, index) => {
|
|
||||||
const number = index + 1;
|
|
||||||
const idInput = item.querySelector('.faq-id');
|
|
||||||
const titleElement = item.querySelector('h6');
|
|
||||||
|
|
||||||
if (idInput) {
|
|
||||||
idInput.value = `faq-${number}`;
|
|
||||||
}
|
|
||||||
if (titleElement) {
|
|
||||||
titleElement.textContent = `FAQ ${number}`;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateAllJsonInputs(data) {
|
|
||||||
// Collect basic details data
|
|
||||||
const details = {
|
|
||||||
title: data.details?.title || data.name,
|
|
||||||
description: data.details?.description || data.description,
|
|
||||||
mainImage: data.details?.mainImage || '',
|
|
||||||
overviewTitle: data.details?.overviewTitle || 'Service Overview',
|
|
||||||
overviewDescription: data.details?.overviewDescription || '',
|
|
||||||
additionalDescription: data.details?.additionalDescription || '',
|
|
||||||
keyFeaturesTitle: data.details?.keyFeaturesTitle || 'Key Features',
|
|
||||||
keyFeaturesImage: data.details?.keyFeaturesImage || '',
|
|
||||||
faqTitle: data.details?.faqTitle || 'Frequently Asked Questions',
|
|
||||||
faqImage: data.details?.faqImage || ''
|
|
||||||
};
|
|
||||||
|
|
||||||
document.getElementById('detailsJson').value = JSON.stringify(details);
|
|
||||||
document.getElementById('featuresJson').value = JSON.stringify(data.details?.features || []);
|
|
||||||
document.getElementById('faqJson').value = JSON.stringify(data.details?.faq || []);
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateJsonData() {
|
|
||||||
// Collect basic details data
|
|
||||||
const details = {
|
|
||||||
title: document.getElementById('detailsTitle').value,
|
|
||||||
description: document.getElementById('detailsDescription').value,
|
|
||||||
mainImage: document.getElementById('mainImage').value,
|
|
||||||
overviewTitle: document.getElementById('overviewTitle').value,
|
|
||||||
overviewDescription: document.getElementById('overviewDescription').value,
|
|
||||||
additionalDescription: document.getElementById('additionalDescription').value,
|
|
||||||
keyFeaturesTitle: document.getElementById('keyFeaturesTitle').value,
|
|
||||||
keyFeaturesImage: document.getElementById('keyFeaturesImage').value,
|
|
||||||
faqTitle: document.getElementById('faqTitle').value,
|
|
||||||
faqImage: document.getElementById('faqImage').value
|
|
||||||
};
|
|
||||||
|
|
||||||
// Collect features data
|
|
||||||
const features = [];
|
|
||||||
document.querySelectorAll('.feature-item').forEach(item => {
|
|
||||||
features.push({
|
|
||||||
title: item.querySelector('.feature-title').value,
|
|
||||||
description: item.querySelector('.feature-description').value
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Collect FAQ data
|
|
||||||
const faq = [];
|
|
||||||
document.querySelectorAll('.faq-item').forEach(item => {
|
|
||||||
faq.push({
|
|
||||||
id: item.querySelector('.faq-id').value,
|
|
||||||
question: item.querySelector('.faq-question').value,
|
|
||||||
answer: item.querySelector('.faq-answer').value,
|
|
||||||
isExpanded: item.querySelector('.faq-expanded').value === 'true'
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById('detailsJson').value = JSON.stringify(details);
|
|
||||||
document.getElementById('featuresJson').value = JSON.stringify(features);
|
|
||||||
document.getElementById('faqJson').value = JSON.stringify(faq);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper function để tạo full URL cho ảnh - tương tự như server-side helper
|
|
||||||
function getFullImageUrlJS(imagePath) {
|
|
||||||
if (!imagePath) return '';
|
|
||||||
|
|
||||||
// Nếu đã là full URL thì return luôn
|
|
||||||
if (imagePath.startsWith('http')) {
|
|
||||||
return imagePath;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Lấy backend URL
|
|
||||||
const backendUrl = '<%= (process.env.BACKEND_URL || "http://localhost:3001").replace(/\/$/, "") %>';
|
|
||||||
|
|
||||||
// Xử lý đường dẫn
|
|
||||||
let imgSrc = imagePath;
|
|
||||||
if (!imgSrc.startsWith('/')) {
|
|
||||||
imgSrc = '/uploads/' + imgSrc;
|
|
||||||
}
|
|
||||||
|
|
||||||
return backendUrl + imgSrc;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Function để cập nhật image preview sau khi upload
|
|
||||||
function updateImagePreviewAfterUpload(targetInput, imagePath) {
|
|
||||||
const fullImageUrl = getFullImageUrlJS(imagePath);
|
|
||||||
|
|
||||||
switch(targetInput) {
|
|
||||||
case 'mainImage':
|
|
||||||
const mainPreview = document.getElementById('mainImagePreview');
|
|
||||||
if (mainPreview) {
|
|
||||||
mainPreview.innerHTML = `<img src="${fullImageUrl}" class="img-thumbnail" style="max-height: 250px; width: auto; max-width: 100%; object-fit: contain;" alt="Main image preview">`;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'keyFeaturesImage':
|
|
||||||
const featuresPreview = document.getElementById('keyFeaturesImagePreview');
|
|
||||||
if (featuresPreview) {
|
|
||||||
featuresPreview.innerHTML = `<img src="${fullImageUrl}" class="img-thumbnail" style="max-height: 180px; width: auto; max-width: 100%; object-fit: contain;" alt="Key Features image preview">`;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'faqImage':
|
|
||||||
const faqPreview = document.getElementById('faqImagePreview');
|
|
||||||
if (faqPreview) {
|
|
||||||
faqPreview.innerHTML = `<img src="${fullImageUrl}" class="img-thumbnail" style="max-height: 180px; width: auto; max-width: 100%; object-fit: contain;" alt="FAQ image preview">`;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function openImageUploader(targetInput, imageType) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
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 reject(new Error('No file selected'));
|
|
||||||
|
|
||||||
try {
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append('image', file);
|
|
||||||
|
|
||||||
const uploadBtn = document.querySelector(`[data-target-input="${targetInput}"]`);
|
|
||||||
const originalBtnHtml = uploadBtn ? uploadBtn.innerHTML : null;
|
|
||||||
if (uploadBtn) {
|
|
||||||
uploadBtn.disabled = true;
|
|
||||||
uploadBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Uploading...';
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await fetch(`/admin/upload/image?imageType=${encodeURIComponent(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');
|
|
||||||
|
|
||||||
input.value = result.path;
|
|
||||||
|
|
||||||
// Update image preview based on target input
|
|
||||||
updateImagePreviewAfterUpload(targetInput, result.path);
|
|
||||||
|
|
||||||
if (uploadBtn) {
|
|
||||||
uploadBtn.disabled = false;
|
|
||||||
uploadBtn.innerHTML = originalBtnHtml;
|
|
||||||
}
|
|
||||||
|
|
||||||
resolve(result);
|
|
||||||
} catch (err) {
|
|
||||||
if (uploadBtn) {
|
|
||||||
uploadBtn.disabled = false;
|
|
||||||
uploadBtn.innerHTML = originalBtnHtml;
|
|
||||||
}
|
|
||||||
console.error('Upload error:', err);
|
|
||||||
showError('Upload failed: ' + (err.message || 'Unknown error'));
|
|
||||||
reject(err);
|
|
||||||
} finally {
|
|
||||||
fileInput.remove();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fileInput.click();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function showSuccess(message) {
|
|
||||||
// Create and show success alert
|
|
||||||
const alert = document.createElement('div');
|
|
||||||
alert.className = 'alert alert-success alert-dismissible fade show position-fixed';
|
|
||||||
alert.style.cssText = 'top: 20px; right: 20px; z-index: 9999; min-width: 300px;';
|
|
||||||
alert.innerHTML = `
|
|
||||||
${message}
|
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
|
||||||
`;
|
|
||||||
document.body.appendChild(alert);
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
if (alert.parentNode) {
|
|
||||||
alert.parentNode.removeChild(alert);
|
|
||||||
}
|
|
||||||
}, 5000);
|
|
||||||
}
|
|
||||||
|
|
||||||
function showError(message) {
|
|
||||||
// Create and show error alert
|
|
||||||
const alert = document.createElement('div');
|
|
||||||
alert.className = 'alert alert-danger alert-dismissible fade show position-fixed';
|
|
||||||
alert.style.cssText = 'top: 20px; right: 20px; z-index: 9999; min-width: 300px;';
|
|
||||||
alert.innerHTML = `
|
|
||||||
${message}
|
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
|
||||||
`;
|
|
||||||
document.body.appendChild(alert);
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
if (alert.parentNode) {
|
|
||||||
alert.parentNode.removeChild(alert);
|
|
||||||
}
|
|
||||||
}, 5000);
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.bottom-buttons {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
gap: 10px;
|
|
||||||
margin-top: 20px;
|
|
||||||
padding: 15px 0;
|
|
||||||
border-top: 1px solid #dee2e6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content-with-fixed-buttons {
|
|
||||||
/* Remove bottom padding since buttons are no longer fixed */
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-group .btn {
|
|
||||||
margin-right: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-group .btn:last-child {
|
|
||||||
margin-right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-header h6 {
|
|
||||||
color: var(--primary-dark);
|
|
||||||
}
|
|
||||||
|
|
||||||
.text-decoration-underline {
|
|
||||||
text-decoration: underline;
|
|
||||||
color: var(--primary-dark);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Image Preview Styles */
|
|
||||||
#mainImagePreview, #keyFeaturesImagePreview, #faqImagePreview {
|
|
||||||
min-height: 60px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
background-color: #f8f9fa;
|
|
||||||
border: 2px dashed #dee2e6;
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 10px;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
#mainImagePreview:empty::before,
|
|
||||||
#keyFeaturesImagePreview:empty::before,
|
|
||||||
#faqImagePreview:empty::before {
|
|
||||||
content: "No image selected";
|
|
||||||
color: #6c757d;
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
#mainImagePreview img,
|
|
||||||
#keyFeaturesImagePreview img,
|
|
||||||
#faqImagePreview img {
|
|
||||||
border-radius: 6px;
|
|
||||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,440 +0,0 @@
|
|||||||
<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);">Edit Service: <%= service.name %></h1>
|
|
||||||
<p class="text-muted mb-0">Update service information and settings</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<a href="/admin/service" class="btn btn-outline-primary">
|
|
||||||
<i class="fas fa-arrow-left me-2"></i>Back to Services
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-12">
|
|
||||||
<form action="/admin/service/<%= service.slug %>/edit" method="POST" id="editServiceForm">
|
|
||||||
<div class="card shadow-sm border-0 mb-4">
|
|
||||||
<div class="card-header bg-white border-bottom">
|
|
||||||
<h5 class="mb-0" style="color: var(--primary-dark);">
|
|
||||||
<i class="fas fa-edit me-2"></i>Service Information
|
|
||||||
</h5>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="row g-3">
|
|
||||||
<div class="col-md-12">
|
|
||||||
<label class="form-label fw-medium">Service Name</label>
|
|
||||||
<input type="text" class="form-control" id="serviceName" name="name"
|
|
||||||
value="<%= service.name %>" required>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row g-3 mt-2">
|
|
||||||
<div class="col-md-6">
|
|
||||||
<label class="form-label fw-medium">
|
|
||||||
Slug
|
|
||||||
<small class="text-muted">(generated from name)</small>
|
|
||||||
<span id="slugAutoIndicator" class="badge bg-info ms-1" style="font-size: 0.7em;">EXISTING</span>
|
|
||||||
</label>
|
|
||||||
<div class="input-group">
|
|
||||||
<input type="text" class="form-control" id="serviceSlug" name="slug"
|
|
||||||
value="<%= service.slug %>" readonly>
|
|
||||||
<button type="button" class="btn btn-primary" id="generateSlugBtn" title="Generate slug from name">
|
|
||||||
<i class="fas fa-magic me-1"></i>Generate
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<small class="form-text text-muted">URL-friendly version of the service name.</small>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-6">
|
|
||||||
<label class="form-label fw-medium">Layout</label>
|
|
||||||
<select class="form-control" id="serviceLayout" name="layout">
|
|
||||||
<option value="left" <%= service.layout === 'left' ? 'selected' : '' %>>Left</option>
|
|
||||||
<option value="right" <%= service.layout === 'right' ? 'selected' : '' %>>Right</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row g-3 mt-2">
|
|
||||||
<div class="col-md-8">
|
|
||||||
<label class="form-label fw-medium">Image</label>
|
|
||||||
<div class="input-group">
|
|
||||||
<input type="text" class="form-control" id="serviceImage" name="image"
|
|
||||||
value="<%= service.image || '' %>">
|
|
||||||
<button type="button" class="btn btn-outline-primary btn-upload-image"
|
|
||||||
data-target-input="serviceImage" data-image-type="service">
|
|
||||||
<i class="fas fa-upload me-1"></i>Upload
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-4">
|
|
||||||
<div id="serviceImagePreview">
|
|
||||||
<% if (service.image) { %>
|
|
||||||
<img src="<%= getFullImageUrl(service.image) %>" class="img-thumbnail"
|
|
||||||
style="height: 80px; width: 100%; object-fit: cover;" alt="Preview">
|
|
||||||
<% } %>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row g-3 mt-2">
|
|
||||||
<div class="col-12">
|
|
||||||
<label class="form-label fw-medium">Description</label>
|
|
||||||
<textarea class="form-control" id="serviceDescription" name="description"
|
|
||||||
rows="3" required><%= service.description %></textarea>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Bottom Buttons -->
|
|
||||||
<div class="bottom-buttons">
|
|
||||||
<a href="/admin/service" class="btn btn-secondary">
|
|
||||||
<i class="fas fa-times me-2"></i>Cancel
|
|
||||||
</a>
|
|
||||||
<button type="submit" class="btn btn-primary" id="submitBtn">
|
|
||||||
<i class="fas fa-save me-2"></i>Update Service
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Scripts -->
|
|
||||||
<script>
|
|
||||||
let servicesData = []; // Will be populated for duplicate checking
|
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
// Load existing services data for duplicate checking
|
|
||||||
loadServicesData();
|
|
||||||
|
|
||||||
// Initialize form handlers
|
|
||||||
initializeFormHandlers();
|
|
||||||
});
|
|
||||||
|
|
||||||
async function loadServicesData() {
|
|
||||||
try {
|
|
||||||
const response = await fetch('/api/service');
|
|
||||||
const data = await response.json();
|
|
||||||
servicesData = data.services?.items || [];
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error loading services data:', error);
|
|
||||||
servicesData = [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function initializeFormHandlers() {
|
|
||||||
// Form submission
|
|
||||||
const form = document.getElementById('editServiceForm');
|
|
||||||
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>Updating...';
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Check for duplicate slug before submitting
|
|
||||||
const slug = document.getElementById('serviceSlug').value.trim();
|
|
||||||
const currentSlug = '<%= service.slug %>';
|
|
||||||
|
|
||||||
if (slug !== currentSlug && isSlugDuplicate(slug, -1)) {
|
|
||||||
showError('Service with this slug already exists. Please generate a new slug.');
|
|
||||||
submitBtn.disabled = false;
|
|
||||||
submitBtn.innerHTML = '<i class="fas fa-save me-2"></i>Update Service';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.submit();
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error updating service:', error);
|
|
||||||
showError('Failed to update service. Please try again.');
|
|
||||||
submitBtn.disabled = false;
|
|
||||||
submitBtn.innerHTML = '<i class="fas fa-save me-2"></i>Update Service';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Initialize image upload buttons
|
|
||||||
document.querySelectorAll('.btn-upload-image').forEach(button => {
|
|
||||||
button.addEventListener('click', function() {
|
|
||||||
const targetInput = this.dataset.targetInput;
|
|
||||||
const imageType = this.dataset.imageType;
|
|
||||||
openImageUploader(targetInput, imageType);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Image preview for service image
|
|
||||||
const serviceImageInput = document.getElementById('serviceImage');
|
|
||||||
if (serviceImageInput) {
|
|
||||||
serviceImageInput.addEventListener('input', function() {
|
|
||||||
updateImagePreview('serviceImagePreview', this.value);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate slug from service name
|
|
||||||
const serviceNameInput = document.getElementById('serviceName');
|
|
||||||
const serviceSlugInput = document.getElementById('serviceSlug');
|
|
||||||
const slugAutoIndicator = document.getElementById('slugAutoIndicator');
|
|
||||||
const generateSlugBtn = document.getElementById('generateSlugBtn');
|
|
||||||
|
|
||||||
if (serviceNameInput && serviceSlugInput && generateSlugBtn) {
|
|
||||||
// Generate slug button
|
|
||||||
generateSlugBtn.addEventListener('click', async function() {
|
|
||||||
const serviceName = serviceNameInput.value.trim();
|
|
||||||
if (serviceName) {
|
|
||||||
// Show loading state
|
|
||||||
const originalBtnHtml = this.innerHTML;
|
|
||||||
this.disabled = true;
|
|
||||||
this.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Generating...';
|
|
||||||
|
|
||||||
try {
|
|
||||||
const slug = await generateSlugFromText(serviceName);
|
|
||||||
const currentSlug = '<%= service.slug %>';
|
|
||||||
|
|
||||||
// Check for duplicate slug (excluding current service)
|
|
||||||
if (slug !== currentSlug && isSlugDuplicate(slug, -1)) {
|
|
||||||
const uniqueSlug = generateUniqueSlug(slug);
|
|
||||||
serviceSlugInput.value = uniqueSlug;
|
|
||||||
showWarning(`Slug "${slug}" already exists. Generated unique slug: "${uniqueSlug}"`);
|
|
||||||
} else {
|
|
||||||
serviceSlugInput.value = slug;
|
|
||||||
showSuccess('Slug generated successfully!');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (slugAutoIndicator) {
|
|
||||||
slugAutoIndicator.textContent = 'GENERATED';
|
|
||||||
slugAutoIndicator.className = 'badge bg-success ms-1';
|
|
||||||
slugAutoIndicator.style.fontSize = '0.7em';
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error generating slug:', error);
|
|
||||||
showError('Failed to generate slug. Please try again.');
|
|
||||||
} finally {
|
|
||||||
// Restore button state
|
|
||||||
this.disabled = false;
|
|
||||||
this.innerHTML = originalBtnHtml;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
showError('Please enter a service name first.');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate slug using backend API
|
|
||||||
async function generateSlugFromText(text) {
|
|
||||||
try {
|
|
||||||
const response = await fetch('/admin/service/generate-slug', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ text: text })
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await response.json();
|
|
||||||
if (result.success) {
|
|
||||||
return result.slug;
|
|
||||||
} else {
|
|
||||||
throw new Error(result.message || 'Failed to generate slug');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error generating slug:', error);
|
|
||||||
// Fallback to simple slug generation if API fails
|
|
||||||
return text
|
|
||||||
.toString()
|
|
||||||
.toLowerCase()
|
|
||||||
.trim()
|
|
||||||
.replace(/\s+/g, '-')
|
|
||||||
.replace(/[^\w\-]+/g, '')
|
|
||||||
.replace(/\-\-+/g, '-')
|
|
||||||
.replace(/^-+/, '')
|
|
||||||
.replace(/-+$/, '');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if slug already exists
|
|
||||||
function isSlugDuplicate(slug, excludeIndex = -1) {
|
|
||||||
return servicesData.some((service, index) => {
|
|
||||||
return service && service.slug === slug && index !== excludeIndex;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate unique slug by appending number
|
|
||||||
function generateUniqueSlug(baseSlug) {
|
|
||||||
let counter = 1;
|
|
||||||
let uniqueSlug = baseSlug;
|
|
||||||
|
|
||||||
while (isSlugDuplicate(uniqueSlug, -1)) {
|
|
||||||
uniqueSlug = baseSlug + '-' + counter;
|
|
||||||
counter++;
|
|
||||||
}
|
|
||||||
|
|
||||||
return uniqueSlug;
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateImagePreview(previewId, imagePath) {
|
|
||||||
const preview = document.getElementById(previewId);
|
|
||||||
if (imagePath) {
|
|
||||||
const fullImageUrl = getFullImageUrlJS(imagePath);
|
|
||||||
preview.innerHTML = `<img src="${fullImageUrl}" class="img-thumbnail" style="height: 80px; width: 100%; object-fit: cover;" alt="Preview">`;
|
|
||||||
} else {
|
|
||||||
preview.innerHTML = '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper function để tạo full URL cho ảnh
|
|
||||||
function getFullImageUrlJS(imagePath) {
|
|
||||||
if (!imagePath) return '';
|
|
||||||
|
|
||||||
if (imagePath.startsWith('http')) {
|
|
||||||
return imagePath;
|
|
||||||
}
|
|
||||||
|
|
||||||
const backendUrl = '<%= (process.env.BACKEND_URL || "http://localhost:3001").replace(/\/$/, "") %>';
|
|
||||||
|
|
||||||
let imgSrc = imagePath;
|
|
||||||
if (!imgSrc.startsWith('/')) {
|
|
||||||
imgSrc = '/uploads/' + imgSrc;
|
|
||||||
}
|
|
||||||
|
|
||||||
return backendUrl + imgSrc;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function openImageUploader(targetInput, imageType) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
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 reject(new Error('No file selected'));
|
|
||||||
|
|
||||||
try {
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append('image', file);
|
|
||||||
|
|
||||||
const uploadBtn = document.querySelector(`[data-target-input="${targetInput}"]`);
|
|
||||||
const originalBtnHtml = uploadBtn ? uploadBtn.innerHTML : null;
|
|
||||||
if (uploadBtn) {
|
|
||||||
uploadBtn.disabled = true;
|
|
||||||
uploadBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Uploading...';
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await fetch(`/admin/upload/image?imageType=service`, {
|
|
||||||
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);
|
|
||||||
if (!input) throw new Error('Target input not found');
|
|
||||||
|
|
||||||
input.value = result.path;
|
|
||||||
|
|
||||||
// Update preview
|
|
||||||
if (targetInput === 'serviceImage') {
|
|
||||||
updateImagePreview('serviceImagePreview', result.path);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (uploadBtn) {
|
|
||||||
uploadBtn.disabled = false;
|
|
||||||
uploadBtn.innerHTML = originalBtnHtml;
|
|
||||||
}
|
|
||||||
|
|
||||||
resolve(result);
|
|
||||||
} catch (err) {
|
|
||||||
if (uploadBtn) {
|
|
||||||
uploadBtn.disabled = false;
|
|
||||||
uploadBtn.innerHTML = originalBtnHtml;
|
|
||||||
}
|
|
||||||
console.error('Upload error:', err);
|
|
||||||
showError('Upload failed: ' + (err.message || 'Unknown error'));
|
|
||||||
reject(err);
|
|
||||||
} finally {
|
|
||||||
fileInput.remove();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fileInput.click();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function showSuccess(message) {
|
|
||||||
const alert = document.createElement('div');
|
|
||||||
alert.className = 'alert alert-success alert-dismissible fade show position-fixed';
|
|
||||||
alert.style.cssText = 'top: 20px; right: 20px; z-index: 9999; min-width: 300px;';
|
|
||||||
alert.innerHTML = `
|
|
||||||
${message}
|
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
|
||||||
`;
|
|
||||||
document.body.appendChild(alert);
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
if (alert.parentNode) {
|
|
||||||
alert.parentNode.removeChild(alert);
|
|
||||||
}
|
|
||||||
}, 5000);
|
|
||||||
}
|
|
||||||
|
|
||||||
function showWarning(message) {
|
|
||||||
const alert = document.createElement('div');
|
|
||||||
alert.className = 'alert alert-warning alert-dismissible fade show position-fixed';
|
|
||||||
alert.style.cssText = 'top: 20px; right: 20px; z-index: 9999; min-width: 300px;';
|
|
||||||
alert.innerHTML = `
|
|
||||||
${message}
|
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
|
||||||
`;
|
|
||||||
document.body.appendChild(alert);
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
if (alert.parentNode) {
|
|
||||||
alert.parentNode.removeChild(alert);
|
|
||||||
}
|
|
||||||
}, 5000);
|
|
||||||
}
|
|
||||||
|
|
||||||
function showError(message) {
|
|
||||||
const alert = document.createElement('div');
|
|
||||||
alert.className = 'alert alert-danger alert-dismissible fade show position-fixed';
|
|
||||||
alert.style.cssText = 'top: 20px; right: 20px; z-index: 9999; min-width: 300px;';
|
|
||||||
alert.innerHTML = `
|
|
||||||
${message}
|
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
|
||||||
`;
|
|
||||||
document.body.appendChild(alert);
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
if (alert.parentNode) {
|
|
||||||
alert.parentNode.removeChild(alert);
|
|
||||||
}
|
|
||||||
}, 5000);
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.bottom-buttons {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
gap: 10px;
|
|
||||||
margin-top: 20px;
|
|
||||||
padding: 15px 0;
|
|
||||||
border-top: 1px solid #dee2e6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-header h5 {
|
|
||||||
color: var(--primary-dark);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,353 +0,0 @@
|
|||||||
<div class="container">
|
|
||||||
<div class="d-flex justify-content-between align-items-center mt-4 mb-4">
|
|
||||||
<div>
|
|
||||||
<h1 class="h3 mb-2" style="color: var(--primary-dark);">
|
|
||||||
Travel Information Editor
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
<div class="d-flex gap-2">
|
|
||||||
<button type="button" class="btn btn-outline-primary preview-btn">
|
|
||||||
<i class="fas fa-eye me-2"></i>Preview
|
|
||||||
</button>
|
|
||||||
<button type="submit" form="travelForm" class="btn btn-primary" id="saveBtn">
|
|
||||||
<i class="fas fa-save me-2"></i>Save Changes
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form id="travelForm" action="/admin/travel/update" method="POST" class="needs-validation" novalidate>
|
|
||||||
<input type="hidden" name="hero" id="heroJson">
|
|
||||||
<input type="hidden" name="page" id="pageJson">
|
|
||||||
<input type="hidden" name="content" id="contentJson">
|
|
||||||
<input type="hidden" name="enableScrollspy" id="enableScrollspyInput">
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-lg-8">
|
|
||||||
<div class="card shadow-sm border-0 mb-4">
|
|
||||||
<div class="card-header bg-white py-3">
|
|
||||||
<h5 class="card-title mb-0">Hero Section</h5>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-5">
|
|
||||||
<div class="mb-3">
|
|
||||||
<label class="form-label">Background Image</label>
|
|
||||||
<div class="input-group">
|
|
||||||
<input type="text" class="form-control" id="heroBackgroundImage"
|
|
||||||
value="<%= data.hero?.backgroundImage || '' %>" readonly>
|
|
||||||
<button type="button" class="btn btn-outline-primary btn-upload-image"
|
|
||||||
data-target-input="heroBackgroundImage" data-image-type="travel">
|
|
||||||
<i class="fas fa-upload"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<small class="form-text text-muted">Recommended size: 1920x1080px</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-7">
|
|
||||||
<div id="heroImagePreview" style="height: 200px; width: 100%;">
|
|
||||||
<% if (data.hero?.backgroundImage) { %>
|
|
||||||
<img src="<%= data.hero.backgroundImage %>" class="img-thumbnail"
|
|
||||||
style="height: 200px; width: 100%; object-fit: cover;" alt="Background image preview">
|
|
||||||
<% } else { %>
|
|
||||||
<div class="border rounded d-flex align-items-center justify-content-center h-100 bg-light">
|
|
||||||
<span class="text-muted">No image selected</span>
|
|
||||||
</div>
|
|
||||||
<% } %>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row mt-3">
|
|
||||||
<div class="col-md-12">
|
|
||||||
<label class="form-label">Hero Title</label>
|
|
||||||
<textarea class="form-control" id="heroTitle" rows="2"><%= data.hero?.title || 'Travel Information' %></textarea>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card shadow-sm border-0 mb-4">
|
|
||||||
<div class="card-header bg-white py-3">
|
|
||||||
<h5 class="card-title mb-0">Page Information</h5>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="mb-3">
|
|
||||||
<label class="form-label">Page Title</label>
|
|
||||||
<input type="text" class="form-control" id="pageTitle" value="<%= data.page?.title || 'Go and Grow Camp Travel Information' %>">
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-6 mb-3">
|
|
||||||
<label class="form-label">As of / Year</label>
|
|
||||||
<input type="text" class="form-control" id="pageYear" value="<%= data.page?.year || '' %>">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card shadow-sm border-0">
|
|
||||||
<div class="card-header bg-white py-3">
|
|
||||||
<h5 class="card-title mb-0">Content Editor</h5>
|
|
||||||
<p class="text-muted mb-0 small">Write content using the blog editor</p>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<div id="editorjs" class="border rounded p-3" style="min-height: 500px;"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-lg-4">
|
|
||||||
<div class="card shadow-sm border-0 mb-4">
|
|
||||||
<div class="card-header bg-white py-3">
|
|
||||||
<h5 class="card-title mb-0">SEO Settings</h5>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="mb-3">
|
|
||||||
<label class="form-label">Meta Title</label>
|
|
||||||
<input type="text" class="form-control" id="metadataTitle" value="<%= data.page?.metadata?.title || '' %>">
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label class="form-label">Meta Description</label>
|
|
||||||
<textarea class="form-control" id="metadataDescription" rows="3"><%= data.page?.metadata?.description || '' %></textarea>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card shadow-sm border-0 mb-4">
|
|
||||||
<div class="card-header bg-white py-3">
|
|
||||||
<h5 class="card-title mb-0">Page Settings</h5>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="mb-3">
|
|
||||||
<div class="form-check form-switch">
|
|
||||||
<input class="form-check-input" type="checkbox" id="enableScrollspy" <%= data.enableScrollspy ? 'checked' : '' %>>
|
|
||||||
<label class="form-check-label" for="enableScrollspy">Enable Scrollspy Navigation</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card shadow-sm border-0">
|
|
||||||
<div class="card-header bg-white py-3">
|
|
||||||
<h5 class="card-title mb-0">Content Tips</h5>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="alert alert-info">
|
|
||||||
<h6><i class="fas fa-lightbulb me-2"></i>Tips for Terms & Conditions:</h6>
|
|
||||||
<ul class="mb-0 small">
|
|
||||||
<li>Use <strong>Header 2</strong> for main sections</li>
|
|
||||||
<li>Use <strong>Header 3</strong> for subsections</li>
|
|
||||||
<li>Use <strong>Lists</strong> for terms items</li>
|
|
||||||
<li>Use <strong>Conclusion</strong> tool for important notes</li>
|
|
||||||
<li>Use <strong>Quote</strong> for legal references</li>
|
|
||||||
</ul>
|
|
||||||
<hr class="my-2">
|
|
||||||
<h6><i class="fas fa-keyboard me-2"></i>Keyboard Shortcuts:</h6>
|
|
||||||
<ul class="mb-0 small">
|
|
||||||
<li><kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>H</kbd>: Convert list item to header</li>
|
|
||||||
<li><kbd>Tab</kbd> in list: Indent item</li>
|
|
||||||
<li><kbd>Backspace</kbd> at start: Exit list</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="modal fade" id="previewModal" tabindex="-1">
|
|
||||||
<div class="modal-dialog modal-xl">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h5 class="modal-title">Travel Information Preview</h5>
|
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body p-0">
|
|
||||||
<iframe id="previewFrame" style="width: 100%; height: 600px; border: none;"></iframe>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<input type="file" id="directImageUpload" style="display: none;" accept="image/*">
|
|
||||||
<input type="hidden" id="currentImageType">
|
|
||||||
<input type="hidden" id="currentTargetInput">
|
|
||||||
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/@editorjs/editorjs@2.28.2"></script>
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/@editorjs/header@2.7.0"></script>
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/@editorjs/paragraph@2.11.3"></script>
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/@editorjs/list@1.8.0"></script>
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/@editorjs/image@2.8.1"></script>
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/@editorjs/quote@2.5.0"></script>
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/@editorjs/marker@1.3.0"></script>
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/@editorjs/embed@2.5.3"></script>
|
|
||||||
|
|
||||||
<script type="module">
|
|
||||||
import BlogEditor from '/js/editor.js';
|
|
||||||
|
|
||||||
// Logic xử lý lọc dữ liệu để tránh duplicate video và xóa paragraph rỗng
|
|
||||||
class TravelContentManager {
|
|
||||||
cleanEditorData(editorData) {
|
|
||||||
const cleanedBlocks = [];
|
|
||||||
const seenVideoIds = new Set();
|
|
||||||
const youtubeRegex = /(?:https?:\/\/)?(?:www\.)?(?:youtube\.com|youtu\.be)\/(?:watch\?v=|embed\/|v\/|shorts\/)?([A-Za-z0-9_-]{11})/;
|
|
||||||
|
|
||||||
(editorData.blocks || []).forEach(block => {
|
|
||||||
if (!block) return;
|
|
||||||
|
|
||||||
// 1. Xử lý Video Embed (Deduplication)
|
|
||||||
if (block.type === 'embed') {
|
|
||||||
const bd = block.data || {};
|
|
||||||
const source = bd.source || bd.embed || '';
|
|
||||||
const match = source.match(youtubeRegex);
|
|
||||||
const vid = bd.videoId || (match ? match[1] : null);
|
|
||||||
|
|
||||||
if (vid) {
|
|
||||||
if (seenVideoIds.has(vid)) return; // Bỏ qua nếu đã có video này
|
|
||||||
seenVideoIds.add(vid);
|
|
||||||
}
|
|
||||||
cleanedBlocks.push(block);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Xử lý Paragraph (Xóa dòng trống hoặc dòng chỉ chứa link đã embed)
|
|
||||||
if (block.type === 'paragraph') {
|
|
||||||
const text = (block.data?.text || '').toString().trim();
|
|
||||||
if (text === '') return; // Xóa paragraph rỗng
|
|
||||||
|
|
||||||
const match = text.match(youtubeRegex);
|
|
||||||
if (match && match[1]) {
|
|
||||||
// Nếu paragraph chỉ chứa link YouTube, và ta sẽ có/đã có block embed cho nó, thì bỏ qua paragraph
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cleanedBlocks.push(block);
|
|
||||||
});
|
|
||||||
|
|
||||||
return { ...editorData, blocks: cleanedBlocks };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', async () => {
|
|
||||||
let blogEditorInstance = null;
|
|
||||||
const travelData = <%- JSON.stringify(data) %>;
|
|
||||||
const initialContent = travelData?.content || { blocks: [] };
|
|
||||||
|
|
||||||
try {
|
|
||||||
blogEditorInstance = new BlogEditor('editorjs', initialContent, 'travel');
|
|
||||||
window.blogEditorInstance = blogEditorInstance;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error initializing BlogEditor:', error);
|
|
||||||
}
|
|
||||||
|
|
||||||
const form = document.getElementById('travelForm');
|
|
||||||
form.addEventListener('submit', async (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
const saveBtn = document.getElementById('saveBtn');
|
|
||||||
saveBtn.disabled = true;
|
|
||||||
saveBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i>Saving...';
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Lấy dữ liệu thô
|
|
||||||
const rawData = await blogEditorInstance.save();
|
|
||||||
|
|
||||||
// Làm sạch dữ liệu trước khi đóng gói JSON
|
|
||||||
const travelManager = new TravelContentManager();
|
|
||||||
const cleanedData = travelManager.cleanEditorData(rawData);
|
|
||||||
|
|
||||||
const heroData = {
|
|
||||||
title: document.getElementById('heroTitle').value.trim(),
|
|
||||||
backgroundImage: document.getElementById('heroBackgroundImage').value.trim(),
|
|
||||||
};
|
|
||||||
|
|
||||||
const pageData = {
|
|
||||||
title: document.getElementById('pageTitle').value.trim(),
|
|
||||||
year: document.getElementById('pageYear')?.value.trim(),
|
|
||||||
metadata: {
|
|
||||||
title: document.getElementById('metadataTitle').value.trim(),
|
|
||||||
description: document.getElementById('metadataDescription').value.trim(),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
document.getElementById('heroJson').value = JSON.stringify(heroData);
|
|
||||||
document.getElementById('pageJson').value = JSON.stringify(pageData);
|
|
||||||
document.getElementById('contentJson').value = JSON.stringify(cleanedData);
|
|
||||||
document.getElementById('enableScrollspyInput').value = document.getElementById('enableScrollspy').checked;
|
|
||||||
|
|
||||||
form.submit();
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Save error:', error);
|
|
||||||
saveBtn.disabled = false;
|
|
||||||
saveBtn.innerHTML = '<i class="fas fa-save me-2"></i>Save Changes';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Preview
|
|
||||||
const previewBtn = document.querySelector('.preview-btn');
|
|
||||||
const previewModal = new bootstrap.Modal(document.getElementById('previewModal'));
|
|
||||||
previewBtn.addEventListener('click', async function () {
|
|
||||||
try {
|
|
||||||
const editorData = await blogEditorInstance.save();
|
|
||||||
const travelManager = new TravelContentManager();
|
|
||||||
const cleanedData = travelManager.cleanEditorData(editorData);
|
|
||||||
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append('content', JSON.stringify(cleanedData));
|
|
||||||
formData.append('heroTitle', document.getElementById('heroTitle').value);
|
|
||||||
formData.append('heroBackgroundImage', document.getElementById('heroBackgroundImage').value);
|
|
||||||
formData.append('pageTitle', document.getElementById('pageTitle').value);
|
|
||||||
formData.append('pageYear', document.getElementById('pageYear')?.value || '');
|
|
||||||
|
|
||||||
const response = await fetch('/admin/travel/preview', { method: 'POST', body: formData });
|
|
||||||
const html = await response.text();
|
|
||||||
const previewFrame = document.getElementById('previewFrame');
|
|
||||||
const blob = new Blob([html], { type: 'text/html' });
|
|
||||||
previewFrame.src = URL.createObjectURL(blob);
|
|
||||||
previewModal.show();
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Preview error:', error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Image Upload Helpers
|
|
||||||
document.querySelectorAll('.btn-upload-image').forEach(btn => {
|
|
||||||
btn.addEventListener('click', function () {
|
|
||||||
document.getElementById('currentImageType').value = this.getAttribute('data-image-type');
|
|
||||||
document.getElementById('currentTargetInput').value = this.getAttribute('data-target-input');
|
|
||||||
document.getElementById('directImageUpload').click();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById('directImageUpload').addEventListener('change', async function () {
|
|
||||||
if (!this.files || !this.files[0]) return;
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append('image', this.files[0]);
|
|
||||||
const imageType = document.getElementById('currentImageType').value;
|
|
||||||
const targetInput = document.getElementById('currentTargetInput').value;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const resp = await fetch(`/admin/upload/image?imageType=${imageType}`, { method: 'POST', body: formData });
|
|
||||||
const result = await resp.json();
|
|
||||||
if (result.success) {
|
|
||||||
document.getElementById(targetInput).value = result.path;
|
|
||||||
if (targetInput === 'heroBackgroundImage') updateHeroImagePreview(result.path);
|
|
||||||
showToast('Uploaded successfully', 'success');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
showToast('Upload failed', 'danger');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
function updateHeroImagePreview(imageUrl) {
|
|
||||||
document.getElementById('heroImagePreview').innerHTML = `<img src="${imageUrl}" class="img-thumbnail" style="height: 200px; width: 100%; object-fit: cover;">`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function showToast(message, type) {
|
|
||||||
const toast = document.createElement('div');
|
|
||||||
toast.className = `toast align-items-center text-bg-${type} border-0 position-fixed bottom-0 end-0 m-3`;
|
|
||||||
toast.innerHTML = `<div class="d-flex"><div class="toast-body">${message}</div><button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button></div>`;
|
|
||||||
document.body.appendChild(toast);
|
|
||||||
new bootstrap.Toast(toast, { delay: 3000 }).show();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
+1
-1
@@ -105,7 +105,7 @@
|
|||||||
<div class="card-body p-4 p-lg-5">
|
<div class="card-body p-4 p-lg-5">
|
||||||
<div class="row align-items-center">
|
<div class="row align-items-center">
|
||||||
<div class="col-lg-8 mx-auto text-center">
|
<div class="col-lg-8 mx-auto text-center">
|
||||||
<h2 class="fw-bold mb-4 text-white">Start Using CMS.HAILearning Today</h2>
|
<h2 class="fw-bold mb-4 text-white">Start Using CMS.LAMS Today</h2>
|
||||||
<p class="lead mb-4 text-white-50">Experience simple and effective API management system</p>
|
<p class="lead mb-4 text-white-50">Experience simple and effective API management system</p>
|
||||||
<a href="/admin/dashboard" class="btn btn-lg"
|
<a href="/admin/dashboard" class="btn btn-lg"
|
||||||
style="background-color: white; color: var(--primary-color); font-weight: 600; border-radius: 10px; padding: 12px 30px;">
|
style="background-color: white; color: var(--primary-color); font-weight: 600; border-radius: 10px; padding: 12px 30px;">
|
||||||
|
|||||||
@@ -724,16 +724,9 @@
|
|||||||
<a class="nav-link <%= currentPath === '/admin/about-us' ? 'active' : '' %>"
|
<a class="nav-link <%= currentPath === '/admin/about-us' ? 'active' : '' %>"
|
||||||
href="/admin/about-us">About</a>
|
href="/admin/about-us">About</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link <%= currentPath === '/admin/service' ? 'active' : '' %>"
|
|
||||||
href="/admin/service">Services</a>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link <%= currentPath === '/admin/blog' ? 'active' : '' %>" href="/admin/blog">Blog</a>
|
<a class="nav-link <%= currentPath === '/admin/blog' ? 'active' : '' %>" href="/admin/blog">Blog</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link <%= currentPath === '/admin/visa' ? 'active' : '' %>" href="/admin/visa">Visa</a>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link <%= currentPath === '/admin/contact' ? 'active' : '' %>" href="/admin/contact">Contact
|
<a class="nav-link <%= currentPath === '/admin/contact' ? 'active' : '' %>" href="/admin/contact">Contact
|
||||||
Us</a>
|
Us</a>
|
||||||
@@ -742,14 +735,6 @@
|
|||||||
<a class="nav-link <%= currentPath === '/admin/student-support' ? 'active' : '' %>"
|
<a class="nav-link <%= currentPath === '/admin/student-support' ? 'active' : '' %>"
|
||||||
href="/admin/student-support">Student Support</a>
|
href="/admin/student-support">Student Support</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link <%= currentPath === '/admin/appointment' ? 'active' : '' %>"
|
|
||||||
href="/admin/appointment">Appointment</a>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link <%= currentPath === '/admin/pricing' ? 'active' : '' %>"
|
|
||||||
href="/admin/pricing">Pricing</a>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" href="/admin/programme">Programmes</a>
|
<a class="nav-link" href="/admin/programme">Programmes</a>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
Reference in New Issue
Block a user