forked from UKSOURCE/cms.hailearning.edu.vn
first commit
This commit is contained in:
1024
views/admin/about/index.ejs
Normal file
1024
views/admin/about/index.ejs
Normal file
File diff suppressed because it is too large
Load Diff
1276
views/admin/aboutUs/index.ejs
Normal file
1276
views/admin/aboutUs/index.ejs
Normal file
File diff suppressed because it is too large
Load Diff
4149
views/admin/activity/form.ejs
Normal file
4149
views/admin/activity/form.ejs
Normal file
File diff suppressed because it is too large
Load Diff
1684
views/admin/activity/index.ejs
Normal file
1684
views/admin/activity/index.ejs
Normal file
File diff suppressed because it is too large
Load Diff
2034
views/admin/booking/index.ejs
Normal file
2034
views/admin/booking/index.ejs
Normal file
File diff suppressed because it is too large
Load Diff
1481
views/admin/campLocation/index.ejs
Normal file
1481
views/admin/campLocation/index.ejs
Normal file
File diff suppressed because it is too large
Load Diff
1123
views/admin/contact/index.ejs
Normal file
1123
views/admin/contact/index.ejs
Normal file
File diff suppressed because it is too large
Load Diff
555
views/admin/dashboard.ejs
Normal file
555
views/admin/dashboard.ejs
Normal file
@@ -0,0 +1,555 @@
|
||||
<div class="container mt-4 mb-4">
|
||||
<h1 class="page-title mb-4">Dashboard</h1>
|
||||
|
||||
<!-- Quick Actions -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0" style="color: var(--primary-color);">Quick Management</h5>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="row g-0">
|
||||
<div class="col-md-4 border-end">
|
||||
<div class="p-4">
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
<div class="rounded-circle d-flex align-items-center justify-content-center me-3"
|
||||
style="width: 50px; height: 50px; background-color: rgba(184, 183, 106, 0.1);">
|
||||
<i class="fas fa-home fa-lg" style="color: var(--primary-color);"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h5 class="mb-0">Home</h5>
|
||||
<p class="text-muted mb-0 small">Manage homepage</p>
|
||||
</div>
|
||||
</div>
|
||||
<a href="/admin/home" class="btn btn-sm btn-primary w-100 mt-2">
|
||||
<i class="fas fa-edit me-2"></i>Edit
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4 border-end">
|
||||
<div class="p-4">
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
<div class="rounded-circle d-flex align-items-center justify-content-center me-3"
|
||||
style="width: 50px; height: 50px; background-color: rgba(184, 183, 106, 0.1);">
|
||||
<i class="fas fa-bars fa-lg" style="color: var(--primary-color);"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h5 class="mb-0">Header & Menu</h5>
|
||||
<p class="text-muted mb-0 small">Manage header & menu</p>
|
||||
</div>
|
||||
</div>
|
||||
<a href="/admin/header" class="btn btn-sm btn-primary w-100 mt-2">
|
||||
<i class="fas fa-edit me-2"></i>Edit
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<div class="p-4">
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
<div class="rounded-circle d-flex align-items-center justify-content-center me-3"
|
||||
style="width: 50px; height: 50px; background-color: rgba(184, 183, 106, 0.1);">
|
||||
<i class="fas fa-window-minimize fa-lg" style="color: var(--primary-color);"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h5 class="mb-0">Footer</h5>
|
||||
<p class="text-muted mb-0 small">Manage footer</p>
|
||||
</div>
|
||||
</div>
|
||||
<a href="/admin/footer" class="btn btn-sm btn-primary w-100 mt-2">
|
||||
<i class="fas fa-edit me-2"></i>Edit
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="col-md-4 border-end border-top">
|
||||
<div class="p-4">
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
<div class="rounded-circle d-flex align-items-center justify-content-center me-3"
|
||||
style="width: 50px; height: 50px; background-color: rgba(184, 183, 106, 0.1);">
|
||||
<i class="fas fa-users fa-lg" style="color: var(--primary-color);"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h5 class="mb-0">About Us</h5>
|
||||
<p class="text-muted mb-0 small">Manage about us</p>
|
||||
</div>
|
||||
</div>
|
||||
<a href="/admin/about-us" class="btn btn-sm btn-primary w-100 mt-2">
|
||||
<i class="fas fa-edit me-2"></i>Edit
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4 border-end border-top">
|
||||
<div class="p-4">
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
<div class="rounded-circle d-flex align-items-center justify-content-center me-3"
|
||||
style="width: 50px; height: 50px; background-color: rgba(184, 183, 106, 0.1);">
|
||||
<i class="fas fa-envelope fa-lg" style="color: var(--primary-color);"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h5 class="mb-0">Contact</h5>
|
||||
<p class="text-muted mb-0 small">Manage contact</p>
|
||||
</div>
|
||||
</div>
|
||||
<a href="/admin/contact" class="btn btn-sm btn-primary w-100 mt-2">
|
||||
<i class="fas fa-edit me-2"></i>Edit
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4 border-top">
|
||||
<div class="p-4">
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
<div class="rounded-circle d-flex align-items-center justify-content-center me-3"
|
||||
style="width: 50px; height: 50px; background-color: rgba(184, 183, 106, 0.1);">
|
||||
<i class="fas fa-question-circle fa-lg" style="color: var(--primary-color);"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h5 class="mb-0">FAQ</h5>
|
||||
<p class="text-muted mb-0 small">Manage FAQ</p>
|
||||
</div>
|
||||
</div>
|
||||
<a href="/admin/faq" class="btn btn-sm btn-primary w-100 mt-2">
|
||||
<i class="fas fa-edit me-2"></i>Edit
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4 border-end border-top">
|
||||
<div class="p-4">
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
<div class="rounded-circle d-flex align-items-center justify-content-center me-3"
|
||||
style="width: 50px; height: 50px; background-color: rgba(184, 183, 106, 0.1);">
|
||||
<i class="fas fa-file-contract fa-lg" style="color: var(--primary-color);"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h5 class="mb-0">Terms & Conditions</h5>
|
||||
<p class="text-muted mb-0 small">Manage terms</p>
|
||||
</div>
|
||||
</div>
|
||||
<a href="/admin/terms-conditions" class="btn btn-sm btn-primary w-100 mt-2">
|
||||
<i class="fas fa-edit me-2"></i>Edit
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4 border-end border-top">
|
||||
<div class="p-4">
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
<div class="rounded-circle d-flex align-items-center justify-content-center me-3"
|
||||
style="width: 50px; height: 50px; background-color: rgba(184, 183, 106, 0.1);">
|
||||
<i class="fas fa-plane fa-lg" style="color: var(--primary-color);"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h5 class="mb-0">Travel</h5>
|
||||
<p class="text-muted mb-0 small">Manage travel</p>
|
||||
</div>
|
||||
</div>
|
||||
<a href="/admin/travel" class="btn btn-sm btn-primary w-100 mt-2">
|
||||
<i class="fas fa-edit me-2"></i>Edit
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4 border-top">
|
||||
<div class="p-4">
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
<div class="rounded-circle d-flex align-items-center justify-content-center me-3"
|
||||
style="width: 50px; height: 50px; background-color: rgba(184, 183, 106, 0.1);">
|
||||
<i class="fas fa-shield-alt fa-lg" style="color: var(--primary-color);"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h5 class="mb-0">Safety</h5>
|
||||
<p class="text-muted mb-0 small">Manage safety</p>
|
||||
</div>
|
||||
</div>
|
||||
<a href="/admin/safety" class="btn btn-sm btn-primary w-100 mt-2">
|
||||
<i class="fas fa-edit me-2"></i>Edit
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4 border-end border-top">
|
||||
<div class="p-4">
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
<div class="rounded-circle d-flex align-items-center justify-content-center me-3"
|
||||
style="width: 50px; height: 50px; background-color: rgba(184, 183, 106, 0.1);">
|
||||
<i class="fas fa-campground fa-lg" style="color: var(--primary-color);"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h5 class="mb-0">Camp Location</h5>
|
||||
<p class="text-muted mb-0 small">Manage camp location</p>
|
||||
</div>
|
||||
</div>
|
||||
<a href="/admin/camp-location" class="btn btn-sm btn-primary w-100 mt-2">
|
||||
<i class="fas fa-edit me-2"></i>Edit
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4 border-end border-top">
|
||||
<div class="p-4">
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
<div class="rounded-circle d-flex align-items-center justify-content-center me-3"
|
||||
style="width: 50px; height: 50px; background-color: rgba(184, 183, 106, 0.1);">
|
||||
<i class="fas fa-running fa-lg" style="color: var(--primary-color);"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h5 class="mb-0">Activities</h5>
|
||||
<p class="text-muted mb-0 small">Manage activities</p>
|
||||
</div>
|
||||
</div>
|
||||
<a href="/admin/activity" class="btn btn-sm btn-primary w-100 mt-2">
|
||||
<i class="fas fa-edit me-2"></i>Edit
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- API Links Section -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0">API Endpoints</h5>
|
||||
<span class="badge bg-primary">12 APIs</span>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>API Name</th>
|
||||
<th>Endpoint</th>
|
||||
<th>Method</th>
|
||||
<th>Description</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<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-bars" style="color: var(--primary-color);"></i>
|
||||
</div>
|
||||
<span>Menu Header API</span>
|
||||
</div>
|
||||
</td>
|
||||
<td><code>/api/header</code></td>
|
||||
<td><span class="badge" style="background-color: var(--primary-color);">GET</span></td>
|
||||
<td>API to get menu header data</td>
|
||||
<td>
|
||||
<a href="/api/header" class="btn btn-sm btn-outline-primary" target="_blank">
|
||||
<i class="fas fa-external-link-alt me-1"></i>View
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<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-home" style="color: var(--primary-color);"></i>
|
||||
</div>
|
||||
<span>Home API</span>
|
||||
</div>
|
||||
</td>
|
||||
<td><code>/api/home</code></td>
|
||||
<td><span class="badge" style="background-color: var(--primary-color);">GET</span></td>
|
||||
<td>API to get homepage data</td>
|
||||
<td>
|
||||
<a href="/api/home" class="btn btn-sm btn-outline-primary" target="_blank">
|
||||
<i class="fas fa-external-link-alt me-1"></i>View
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<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-info-circle" style="color: var(--primary-color);"></i>
|
||||
</div>
|
||||
<span>About API</span>
|
||||
</div>
|
||||
</td>
|
||||
<td><code>/api/about</code></td>
|
||||
<td><span class="badge" style="background-color: var(--primary-color);">GET</span></td>
|
||||
<td>API to get about page data</td>
|
||||
<td>
|
||||
<a href="/api/about" class="btn btn-sm btn-outline-primary" target="_blank">
|
||||
<i class="fas fa-external-link-alt me-1"></i>View
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<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-users" style="color: var(--primary-color);"></i>
|
||||
</div>
|
||||
<span>About Us API</span>
|
||||
</div>
|
||||
</td>
|
||||
<td><code>/api/about-us</code></td>
|
||||
<td><span class="badge" style="background-color: var(--primary-color);">GET</span></td>
|
||||
<td>API to get about us data</td>
|
||||
<td>
|
||||
<a href="/api/about-us" class="btn btn-sm btn-outline-primary" target="_blank">
|
||||
<i class="fas fa-external-link-alt me-1"></i>View
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<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-question-circle" style="color: var(--primary-color);"></i>
|
||||
</div>
|
||||
<span>FAQ API</span>
|
||||
</div>
|
||||
</td>
|
||||
<td><code>/api/faq</code></td>
|
||||
<td><span class="badge" style="background-color: var(--primary-color);">GET</span></td>
|
||||
<td>API to get FAQ data</td>
|
||||
<td>
|
||||
<a href="/api/faq" class="btn btn-sm btn-outline-primary" target="_blank">
|
||||
<i class="fas fa-external-link-alt me-1"></i>View
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<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-file-contract" style="color: var(--primary-color);"></i>
|
||||
</div>
|
||||
<span>Terms & Conditions API</span>
|
||||
</div>
|
||||
</td>
|
||||
<td><code>/api/terms</code></td>
|
||||
<td><span class="badge" style="background-color: var(--primary-color);">GET</span></td>
|
||||
<td>API to get terms & conditions data</td>
|
||||
<td>
|
||||
<a href="/api/terms" class="btn btn-sm btn-outline-primary" target="_blank">
|
||||
<i class="fas fa-external-link-alt me-1"></i>View
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<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-plane" style="color: var(--primary-color);"></i>
|
||||
</div>
|
||||
<span>Travel API</span>
|
||||
</div>
|
||||
</td>
|
||||
<td><code>/api/travel</code></td>
|
||||
<td><span class="badge" style="background-color: var(--primary-color);">GET</span></td>
|
||||
<td>API to get travel data</td>
|
||||
<td>
|
||||
<a href="/api/travel" class="btn btn-sm btn-outline-primary" target="_blank">
|
||||
<i class="fas fa-external-link-alt me-1"></i>View
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<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-shield-alt" style="color: var(--primary-color);"></i>
|
||||
</div>
|
||||
<span>Safety API</span>
|
||||
</div>
|
||||
</td>
|
||||
<td><code>/api/safety</code></td>
|
||||
<td><span class="badge" style="background-color: var(--primary-color);">GET</span></td>
|
||||
<td>API to get safety data</td>
|
||||
<td>
|
||||
<a href="/api/safety" class="btn btn-sm btn-outline-primary" target="_blank">
|
||||
<i class="fas fa-external-link-alt me-1"></i>View
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<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-campground" style="color: var(--primary-color);"></i>
|
||||
</div>
|
||||
<span>Camp Location API</span>
|
||||
</div>
|
||||
</td>
|
||||
<td><code>/api/camp-location</code></td>
|
||||
<td><span class="badge" style="background-color: var(--primary-color);">GET</span></td>
|
||||
<td>API to get camp location data</td>
|
||||
<td>
|
||||
<a href="/api/camp-location" class="btn btn-sm btn-outline-primary" target="_blank">
|
||||
<i class="fas fa-external-link-alt me-1"></i>View
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<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-sitemap" style="color: var(--primary-color);"></i>
|
||||
</div>
|
||||
<span>Menu Tree API</span>
|
||||
</div>
|
||||
</td>
|
||||
<td><code>/api/menu-tree</code></td>
|
||||
<td><span class="badge" style="background-color: var(--primary-color);">GET</span></td>
|
||||
<td>API to get menu tree data</td>
|
||||
<td>
|
||||
<a href="/api/menu-tree" class="btn btn-sm btn-outline-primary" target="_blank">
|
||||
<i class="fas fa-external-link-alt me-1"></i>View
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<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-envelope" style="color: var(--primary-color);"></i>
|
||||
</div>
|
||||
<span>Contact API</span>
|
||||
</div>
|
||||
</td>
|
||||
<td><code>/api/contact</code></td>
|
||||
<td><span class="badge" style="background-color: var(--primary-color);">GET</span></td>
|
||||
<td>API to get contact data</td>
|
||||
<td>
|
||||
<a href="/api/contact" class="btn btn-sm btn-outline-primary" target="_blank">
|
||||
<i class="fas fa-external-link-alt me-1"></i>View
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<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-campground" style="color: var(--primary-color);"></i>
|
||||
</div>
|
||||
<span>Camp Location API</span>
|
||||
</div>
|
||||
</td>
|
||||
<td><code>/api/camp-location</code></td>
|
||||
<td><span class="badge" style="background-color: var(--primary-color);">GET</span></td>
|
||||
<td>API to get camp location data</td>
|
||||
<td>
|
||||
<a href="/api/camp-location" class="btn btn-sm btn-outline-primary" target="_blank">
|
||||
<i class="fas fa-external-link-alt me-1"></i>View
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- System Info -->
|
||||
<div class="card">
|
||||
<div class="card-header" style="background: linear-gradient(135deg, #b8b76a, #9a994a); color: white;">
|
||||
<h5 class="mb-0">System Information</h5>
|
||||
</div>
|
||||
<div class="card-body" style="background-color: #f8faf8;">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
<div class="rounded-circle d-flex align-items-center justify-content-center me-3"
|
||||
style="width: 40px; height: 40px; background-color: rgba(184, 183, 106, 0.1);">
|
||||
<i class="fas fa-info-circle" style="color: var(--primary-color);"></i>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-muted small">Version</div>
|
||||
<div class="fw-bold" style="color: var(--primary-color);">CMS-SIMS v1.0.0</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
<div class="rounded-circle d-flex align-items-center justify-content-center me-3"
|
||||
style="width: 40px; height: 40px; background-color: rgba(184, 183, 106, 0.1);">
|
||||
<i class="fas fa-user" style="color: var(--primary-color);"></i>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-muted small">Logged in as</div>
|
||||
<div class="fw-bold" style="color: var(--primary-color);"><%= user.username %></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="alert mt-3 mb-0" style="background-color: rgba(184, 183, 106, 0.05); border-left: 4px solid var(--primary-color);" role="alert">
|
||||
<div class="d-flex">
|
||||
<div class="me-3">
|
||||
<i class="fas fa-lightbulb fa-lg" style="color: var(--primary-color);"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h6 class="mb-1" style="color: var(--primary-color);">Quick Tip</h6>
|
||||
<p class="mb-0 text-muted">Click the Edit button to make changes to your data.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.page-title {
|
||||
color: var(--primary-color);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: var(--primary-color);
|
||||
border-color: var(--primary-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background-color: var(--primary-dark);
|
||||
border-color: var(--primary-dark);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-outline-primary {
|
||||
color: var(--primary-color);
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.btn-outline-primary:hover {
|
||||
background-color: var(--primary-color);
|
||||
border-color: var(--primary-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.card-header h5 {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.badge {
|
||||
background-color: var(--primary-color) !important;
|
||||
color: white;
|
||||
}
|
||||
</style>
|
||||
98
views/admin/department/create.ejs
Normal file
98
views/admin/department/create.ejs
Normal file
@@ -0,0 +1,98 @@
|
||||
<!-- Header section -->
|
||||
<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);">Create New Department</h1>
|
||||
<p class="text-muted mb-0">Add a new department to the system</p>
|
||||
</div>
|
||||
<div>
|
||||
<a href="/admin/department" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-arrow-left me-1"></i>Back to Department Management
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-8 mx-auto">
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-body p-4">
|
||||
<form action="/admin/department/create" method="POST" id="createForm">
|
||||
<div class="mb-4">
|
||||
<label for="name" class="form-label fw-medium">Department Name</label>
|
||||
<input type="text" class="form-control" id="name" name="name" required
|
||||
placeholder="Enter department name">
|
||||
<div class="form-text">
|
||||
<i class="fas fa-info-circle me-1"></i>
|
||||
Enter the full name of the department (e.g. Business, Engineering)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-info">
|
||||
<i class="fas fa-lightbulb me-2"></i>
|
||||
<strong>Tips:</strong>
|
||||
<ul class="mb-0 mt-2">
|
||||
<li>Department name should be clear and descriptive</li>
|
||||
<li>Use proper capitalization</li>
|
||||
<li>Avoid abbreviations unless commonly known</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="d-grid gap-2 mt-4">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fas fa-plus-circle me-1"></i>Create Department
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Custom Modal -->
|
||||
<div id="customModal" class="custom-modal">
|
||||
<div class="custom-modal-content">
|
||||
<div class="custom-modal-header">
|
||||
<h5 class="custom-modal-title">Notification</h5>
|
||||
<button type="button" class="custom-modal-close">×</button>
|
||||
</div>
|
||||
<div class="custom-modal-body">
|
||||
<p id="modalMessage">Content of the notification will appear here.</p>
|
||||
</div>
|
||||
<div class="custom-modal-footer">
|
||||
<button type="button" class="btn btn-primary custom-modal-ok">OK</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Import custom modal CSS -->
|
||||
<link rel="stylesheet" href="/css/custom-modal.css">
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Initialize custom modal
|
||||
CustomModal.init('customModal', {
|
||||
closeOnOutsideClick: true,
|
||||
animationDuration: 300
|
||||
});
|
||||
|
||||
const form = document.getElementById('createForm');
|
||||
const nameInput = document.getElementById('name');
|
||||
|
||||
form.addEventListener('submit', function(e) {
|
||||
// Validate department name
|
||||
if (!nameInput.value.trim()) {
|
||||
e.preventDefault();
|
||||
CustomModal.alert('Please enter a department name.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for special characters
|
||||
if (/[^a-zA-Z0-9\s-]/.test(nameInput.value.trim())) {
|
||||
e.preventDefault();
|
||||
CustomModal.alert('Department name can only contain letters, numbers, spaces and hyphens.');
|
||||
return;
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
100
views/admin/department/edit.ejs
Normal file
100
views/admin/department/edit.ejs
Normal file
@@ -0,0 +1,100 @@
|
||||
<!-- Header section -->
|
||||
<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 Department</h1>
|
||||
<p class="text-muted mb-0">
|
||||
Editing department: <span class="badge bg-primary"><%= department.name %></span>
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<a href="/admin/department" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-arrow-left me-1"></i>Back to Department Management
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-8 mx-auto">
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-body p-4">
|
||||
<form action="/admin/department/edit/<%= department._id %>" method="POST" id="editForm">
|
||||
<div class="mb-4">
|
||||
<label for="name" class="form-label fw-medium">Department Name</label>
|
||||
<input type="text" class="form-control" id="name" name="name" required
|
||||
value="<%= department.name %>" placeholder="Enter department name">
|
||||
<div class="form-text">
|
||||
<i class="fas fa-info-circle me-1"></i>
|
||||
Enter the full name of the department (e.g. Business, Engineering)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-info">
|
||||
<i class="fas fa-lightbulb me-2"></i>
|
||||
<strong>Tips:</strong>
|
||||
<ul class="mb-0 mt-2">
|
||||
<li>Department name should be clear and descriptive</li>
|
||||
<li>Use proper capitalization</li>
|
||||
<li>Avoid abbreviations unless commonly known</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="d-grid gap-2 mt-4">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fas fa-save me-1"></i>Save Changes
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Custom Modal -->
|
||||
<div id="customModal" class="custom-modal">
|
||||
<div class="custom-modal-content">
|
||||
<div class="custom-modal-header">
|
||||
<h5 class="custom-modal-title">Notification</h5>
|
||||
<button type="button" class="custom-modal-close">×</button>
|
||||
</div>
|
||||
<div class="custom-modal-body">
|
||||
<p id="modalMessage">Content of the notification will appear here.</p>
|
||||
</div>
|
||||
<div class="custom-modal-footer">
|
||||
<button type="button" class="btn btn-primary custom-modal-ok">OK</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Import custom modal CSS -->
|
||||
<link rel="stylesheet" href="/css/custom-modal.css">
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Initialize custom modal
|
||||
CustomModal.init('customModal', {
|
||||
closeOnOutsideClick: true,
|
||||
animationDuration: 300
|
||||
});
|
||||
|
||||
const form = document.getElementById('editForm');
|
||||
const nameInput = document.getElementById('name');
|
||||
|
||||
form.addEventListener('submit', function(e) {
|
||||
// Validate department name
|
||||
if (!nameInput.value.trim()) {
|
||||
e.preventDefault();
|
||||
CustomModal.alert('Please enter a department name.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for special characters
|
||||
if (/[^a-zA-Z0-9\s-]/.test(nameInput.value.trim())) {
|
||||
e.preventDefault();
|
||||
CustomModal.alert('Department name can only contain letters, numbers, spaces and hyphens.');
|
||||
return;
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
126
views/admin/department/index.ejs
Normal file
126
views/admin/department/index.ejs
Normal file
@@ -0,0 +1,126 @@
|
||||
<!-- Header section -->
|
||||
<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);">Department Management</h1>
|
||||
<p class="text-muted mb-0">Manage all departments in the system</p>
|
||||
</div>
|
||||
<div>
|
||||
<a href="/admin/department/create" class="btn btn-primary">
|
||||
<i class="fas fa-plus-circle me-1"></i>Create New Department
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-body p-4">
|
||||
<% if (departments && departments.length > 0) { %>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" style="width: 50px">#</th>
|
||||
<th scope="col">Department Name</th>
|
||||
<th scope="col">Slug</th>
|
||||
<th scope="col" style="width: 200px">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% departments.forEach((department, index) => { %>
|
||||
<tr>
|
||||
<td><%= index + 1 %></td>
|
||||
<td>
|
||||
<span class="fw-medium"><%= department.name %></span>
|
||||
</td>
|
||||
<td>
|
||||
<code class="text-muted"><%= department.slug %></code>
|
||||
</td>
|
||||
<td>
|
||||
<div class="btn-group" role="group">
|
||||
<a href="/admin/department/edit/<%= department._id %>" class="btn btn-sm btn-outline-primary">
|
||||
<i class="fas fa-edit me-1"></i>Edit
|
||||
</a>
|
||||
<button type="button" class="btn btn-sm btn-outline-danger"
|
||||
data-custom-modal="open"
|
||||
data-id="<%= department._id %>"
|
||||
data-name="<%= department.name %>">
|
||||
<i class="fas fa-trash-alt me-1"></i>Delete
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<% }); %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<% } else { %>
|
||||
<div class="text-center py-5">
|
||||
<i class="fas fa-folder-open text-muted mb-3" style="font-size: 3rem;"></i>
|
||||
<h5 class="text-muted mb-3">No Departments Found</h5>
|
||||
<a href="/admin/department/create" class="btn btn-primary">
|
||||
<i class="fas fa-plus-circle me-1"></i>Create First Department
|
||||
</a>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Custom Modal -->
|
||||
<div id="customModal" class="custom-modal">
|
||||
<div class="custom-modal-content">
|
||||
<div class="custom-modal-header">
|
||||
<h5 class="custom-modal-title">Delete Confirmation</h5>
|
||||
<button type="button" class="custom-modal-close">×</button>
|
||||
</div>
|
||||
<div class="custom-modal-body">
|
||||
<p id="modalMessage">Are you sure you want to delete this department?</p>
|
||||
</div>
|
||||
<div class="custom-modal-footer">
|
||||
<button type="button" class="btn btn-secondary custom-modal-cancel">Cancel</button>
|
||||
<button type="button" class="btn btn-danger custom-modal-ok">Delete Permanently</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Import custom modal CSS -->
|
||||
<link rel="stylesheet" href="/css/custom-modal.css">
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Khởi tạo modal tùy chỉnh
|
||||
CustomModal.init('customModal', {
|
||||
closeOnOutsideClick: true,
|
||||
animationDuration: 300
|
||||
});
|
||||
|
||||
// Lắng nghe click vào nút xóa
|
||||
document.addEventListener('click', function(e) {
|
||||
if (e.target.getAttribute('data-custom-modal') === 'open' ||
|
||||
e.target.parentElement.getAttribute('data-custom-modal') === 'open') {
|
||||
|
||||
// Lấy button hoặc icon parent nếu click vào icon
|
||||
const button = e.target.getAttribute('data-custom-modal') === 'open' ?
|
||||
e.target : e.target.parentElement;
|
||||
|
||||
const id = button.getAttribute('data-id');
|
||||
const name = button.getAttribute('data-name');
|
||||
|
||||
// Sử dụng CustomModal.confirm thay vì xử lý trực tiếp
|
||||
CustomModal.confirm(
|
||||
`Are you sure you want to delete department "${name}"? This action cannot be undone.`,
|
||||
function() {
|
||||
// Hành động khi xác nhận
|
||||
window.location.href = `/admin/department/delete/${id}`;
|
||||
},
|
||||
null,
|
||||
'Delete Confirmation'
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
1019
views/admin/faq/index.ejs
Normal file
1019
views/admin/faq/index.ejs
Normal file
File diff suppressed because it is too large
Load Diff
760
views/admin/footer/index.ejs
Normal file
760
views/admin/footer/index.ejs
Normal file
@@ -0,0 +1,760 @@
|
||||
<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)">
|
||||
Footer Management
|
||||
</h1>
|
||||
<p class="text-muted mb-0">Edit footer content and structure</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<form action="/admin/footer/update" method="POST" class="content-with-fixed-buttons" id="footerForm">
|
||||
<!-- Hidden consolidated JSON for footer (used for single-payload submit) -->
|
||||
<input type="hidden" name="footerJson" id="footerJson">
|
||||
<input type="hidden" name="activeTab" id="activeTabInput" value="about">
|
||||
<!-- 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="#about" role="tab">
|
||||
<i class="fas fa-info-circle me-2"></i>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-bs-toggle="tab" href="#logo" role="tab">
|
||||
<i class="fas fa-image me-2"></i>Logo
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-bs-toggle="tab" href="#contact" role="tab">
|
||||
<i class="fas fa-address-book me-2"></i>Contact & Address
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-bs-toggle="tab" href="#columns" role="tab">
|
||||
<i class="fas fa-columns me-2"></i>Footer Columns
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-bs-toggle="tab" href="#social" role="tab">
|
||||
<i class="fas fa-share-alt me-2"></i>Social Links
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-bs-toggle="tab" href="#copyright" role="tab">
|
||||
<i class="fas fa-copyright me-2"></i>Copyright
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<div class="tab-content">
|
||||
<!-- About GGC Tab -->
|
||||
<div class="tab-pane fade show active" id="about" role="tabpanel">
|
||||
<div class="card border shadow-sm">
|
||||
<div class="card-header bg-light">
|
||||
<h6 class="mb-0">About GGC Section</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row mb-3">
|
||||
<div class="col-12">
|
||||
<label class="form-label fw-medium">Title</label>
|
||||
<input type="text" class="form-control" name="about[title]"
|
||||
value="<%= (data.about && data.about.title) ? data.about.title : 'About GGC' %>" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col-12">
|
||||
<label class="form-label fw-medium">Description</label>
|
||||
<textarea class="form-control" name="about[description]"
|
||||
rows="4"><%= (data.about && data.about.description) ? data.about.description : '' %></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Map Link Text</label>
|
||||
<input type="text" class="form-control" name="about[mapLink][text]"
|
||||
value="<%= (data.about && data.about.mapLink && data.about.mapLink.text) ? data.about.mapLink.text : 'Check on google map' %>" />
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Map Link URL</label>
|
||||
<input type="url" class="form-control" name="about[mapLink][url]"
|
||||
value="<%= (data.about && data.about.mapLink && data.about.mapLink.url) ? data.about.mapLink.url : '' %>" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Logo Tab -->
|
||||
<div class="tab-pane fade" id="logo" role="tabpanel">
|
||||
<div class="card border shadow-sm">
|
||||
<div class="card-header bg-light">
|
||||
<h6 class="mb-0">Logo Configuration</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Logo Image URL</label>
|
||||
<div class="input-group mb-2">
|
||||
<input type="text" class="form-control" id="logoImage" name="logo[src]"
|
||||
value="<%= (data.logo && data.logo.src) ? data.logo.src : '' %>" />
|
||||
<button type="button" class="btn btn-outline-primary btn-upload-image"
|
||||
data-target-input="logoImage" data-image-type="layout">
|
||||
<i class="fas fa-upload me-1"></i>Upload
|
||||
</button>
|
||||
</div>
|
||||
<small class="form-text text-muted">Recommended size: 200x60px</small>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Logo Alt Text</label>
|
||||
<input type="text" class="form-control" id="logoAlt" name="logo[alt]"
|
||||
value="<%= (data.logo && data.logo.alt) ? data.logo.alt : '' %>" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-3">
|
||||
<div class="col-12">
|
||||
<% if (data.logo && data.logo.src) { %>
|
||||
<img src="<%= data.logo.src %>" class="img-thumbnail" style="
|
||||
max-height: 100px;
|
||||
max-width: 300px;
|
||||
object-fit: contain;
|
||||
background: var(--primary-color);
|
||||
" alt="Logo preview" />
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Contact & Address Tab -->
|
||||
<div class="tab-pane fade" id="contact" role="tabpanel">
|
||||
<div class="card border shadow-sm">
|
||||
<div class="card-header bg-light">
|
||||
<h6 class="mb-0">Contact & Address Information</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<!-- Address -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<h6 class="fw-medium mb-3">Address</h6>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Address Text</label>
|
||||
<input type="text" class="form-control" name="address[text]" value="<%= data.address.text %>" />
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Address Line 2</label>
|
||||
<input type="text" class="form-control" name="address[address2]"
|
||||
value="<%= (data.address && data.address.address2) ? data.address.address2 : '' %>" />
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Map URL</label>
|
||||
<input type="url" class="form-control" name="address[mapUrl]"
|
||||
value="<%= data.address.mapUrl %>" />
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Map/Link 2 (optional)</label>
|
||||
<input type="url" class="form-control" name="address[link2]"
|
||||
value="<%= (data.address && data.address.link2) ? data.address.link2 : '' %>" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Contact Info -->
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<h6 class="fw-medium mb-3">Contact Information</h6>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label fw-medium">Phone Number</label>
|
||||
<input type="text" class="form-control" name="contact[phone]"
|
||||
value="<%= data.contact.phone %>" />
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label fw-medium">Working Hours</label>
|
||||
<input type="text" class="form-control" name="contact[hours]"
|
||||
value="<%= data.contact.hours %>" />
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label fw-medium">Email</label>
|
||||
<input type="email" class="form-control" name="contact[email]"
|
||||
value="<%= data.contact.email %>" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer Columns Tab -->
|
||||
<div class="tab-pane fade" id="columns" role="tabpanel">
|
||||
<div class="card border shadow-sm">
|
||||
<div class="card-header bg-light d-flex justify-content-between align-items-center">
|
||||
<h6 class="mb-0">Footer Columns</h6>
|
||||
<button type="button" class="btn btn-outline-primary btn-sm" id="addColumn">
|
||||
<i class="fas fa-plus me-1"></i>Add Column
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="columnsContainer">
|
||||
<% data.columns.forEach((column, columnIndex)=> { %>
|
||||
<div class="card mb-3 border" data-column-index="<%= columnIndex %>">
|
||||
<div class="card-header bg-light">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<h6 class="mb-0">Column <%= columnIndex + 1 %>
|
||||
</h6>
|
||||
<button type="button" class="btn btn-outline-danger btn-sm remove-column"
|
||||
data-column-index="<%= columnIndex %>">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Column Title</label>
|
||||
<input type="text" class="form-control" name="columns[<%= columnIndex %>][title]"
|
||||
value="<%= column.title %>" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<h6 class="fw-medium mb-3">Links</h6>
|
||||
<div class="column-links-container" data-column-index="<%= columnIndex %>">
|
||||
<% column.links.forEach((link, linkIndex)=> {
|
||||
%>
|
||||
<div class="card mb-2 border" data-link-index="<%= linkIndex %>">
|
||||
<div class="card-body">
|
||||
<div class="row g-2">
|
||||
<div class="col-md-5">
|
||||
<label class="form-label form-label-sm">Link Title</label>
|
||||
<input type="text" class="form-control form-control-sm"
|
||||
name="columns[<%= columnIndex %>][links][<%= linkIndex %>][title]"
|
||||
value="<%= link.title %>" />
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label form-label-sm">URL</label>
|
||||
<input type="text" class="form-control form-control-sm"
|
||||
name="columns[<%= columnIndex %>][links][<%= linkIndex %>][url]"
|
||||
value="<%= link.url %>" placeholder="/about-us/" />
|
||||
</div>
|
||||
<div class="col-md-1">
|
||||
<label class="form-label form-label-sm"> </label>
|
||||
<button type="button"
|
||||
class="btn btn-outline-danger btn-sm w-100 remove-link"
|
||||
data-column-index="<%= columnIndex %>" data-link-index="<%= linkIndex %>">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% }); %>
|
||||
</div>
|
||||
<button type="button" class="btn btn-outline-primary btn-sm add-link"
|
||||
data-column-index="<%= columnIndex %>">
|
||||
<i class="fas fa-plus me-1"></i>Add Link
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% }); %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Social Links Tab -->
|
||||
<div class="tab-pane fade" id="social" role="tabpanel">
|
||||
<div class="card border shadow-sm">
|
||||
<div class="card-header bg-light d-flex justify-content-between align-items-center">
|
||||
<h6 class="mb-0">Social Media Links</h6>
|
||||
<button type="button" class="btn btn-outline-primary btn-sm" id="addSocialLink">
|
||||
<i class="fas fa-plus me-1"></i>Add Social Link
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="socialLinksContainer">
|
||||
<% data.social.links.forEach((link, index)=> { %>
|
||||
<div class="card mb-3 border" data-social-index="<%= index %>">
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-3">
|
||||
<label class="form-label fw-medium">Platform</label>
|
||||
<input type="text" class="form-control" name="social[links][<%= index %>][platform]"
|
||||
value="<%= link.platform %>" />
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label fw-medium">URL</label>
|
||||
<input type="text" class="form-control" name="social[links][<%= index %>][url]"
|
||||
value="<%= link.url %>" placeholder="https://facebook.com/..." />
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label fw-medium">Icon Class</label>
|
||||
<input type="text" class="form-control" name="social[links][<%= index %>][icon]"
|
||||
value="<%= link.icon %>" />
|
||||
</div>
|
||||
<div class="col-md-1">
|
||||
<label class="form-label"> </label>
|
||||
<button type="button" class="btn btn-outline-danger btn-sm w-100 remove-social-link"
|
||||
data-social-index="<%= index %>">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% }); %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Copyright Tab -->
|
||||
<div class="tab-pane fade" id="copyright" role="tabpanel">
|
||||
<div class="card border shadow-sm">
|
||||
<div class="card-header bg-light">
|
||||
<h6 class="mb-0">Copyright Information</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<label class="form-label fw-medium">Copyright Text</label>
|
||||
<textarea class="form-control" name="copyright[text]" rows="3">
|
||||
<%= data.copyright.text %></textarea>
|
||||
<small class="form-text text-muted">You can use HTML tags for formatting</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Move buttons to fixed bottom -->
|
||||
<div class="fixed-bottom-buttons">
|
||||
<button type="reset" class="btn btn-secondary">
|
||||
<i class="fas fa-undo"></i>
|
||||
<span>Reset</span>
|
||||
</button>
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fas fa-save"></i>
|
||||
<span>Save Changes</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.content-with-fixed-buttons {
|
||||
padding-bottom: 80px;
|
||||
}
|
||||
|
||||
.fixed-bottom-buttons {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
padding: 1rem;
|
||||
z-index: 1000;
|
||||
width: 25%;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.fixed-bottom-buttons .btn {
|
||||
min-width: 120px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.form-label-sm {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.card-body .form-control-sm {
|
||||
font-size: 0.75rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
// Derive current counts from DOM to avoid template injection issues
|
||||
let columnIndex =
|
||||
document.querySelectorAll("#columnsContainer [data-column-index]")
|
||||
.length || 0;
|
||||
let socialLinkIndex =
|
||||
document.querySelectorAll("#socialLinksContainer [data-social-index]")
|
||||
.length || 0;
|
||||
|
||||
// Handle Active Tab Persistence
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const activeTabObj = urlParams.get('activeTab');
|
||||
|
||||
if (activeTabObj) {
|
||||
const tabTrigger = document.querySelector(`a[href="#${activeTabObj}"]`);
|
||||
if (tabTrigger) {
|
||||
new bootstrap.Tab(tabTrigger).show();
|
||||
// Update hidden input to match restored tab
|
||||
document.getElementById('activeTabInput').value = activeTabObj;
|
||||
}
|
||||
}
|
||||
|
||||
// Update hidden input on tab change
|
||||
document.querySelectorAll('a[data-bs-toggle="tab"]').forEach(tab => {
|
||||
tab.addEventListener('shown.bs.tab', function (event) {
|
||||
const targetId = event.target.getAttribute('href').substring(1); // remove #
|
||||
document.getElementById('activeTabInput').value = targetId;
|
||||
});
|
||||
});
|
||||
|
||||
// Add Column
|
||||
document.getElementById("addColumn").addEventListener("click", function () {
|
||||
const container = document.getElementById("columnsContainer");
|
||||
const newColumn = document.createElement("div");
|
||||
newColumn.className = "card mb-3 border";
|
||||
newColumn.dataset.columnIndex = columnIndex;
|
||||
newColumn.innerHTML = `
|
||||
<div class="card-header bg-light">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<h6 class="mb-0">Column ${columnIndex + 1}</h6>
|
||||
<button type="button" class="btn btn-outline-danger btn-sm remove-column" data-column-index="${columnIndex}">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Column Title</label>
|
||||
<input type="text" class="form-control" name="columns[${columnIndex}][title]" value="">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<h6 class="fw-medium mb-3">Links</h6>
|
||||
<div class="column-links-container" data-column-index="${columnIndex}">
|
||||
</div>
|
||||
<button type="button" class="btn btn-outline-primary btn-sm add-link" data-column-index="${columnIndex}">
|
||||
<i class="fas fa-plus me-1"></i>Add Link
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
container.appendChild(newColumn);
|
||||
columnIndex++;
|
||||
});
|
||||
|
||||
// Add Social Link
|
||||
document
|
||||
.getElementById("addSocialLink")
|
||||
.addEventListener("click", function () {
|
||||
const container = document.getElementById("socialLinksContainer");
|
||||
const newLink = document.createElement("div");
|
||||
newLink.className = "card mb-3 border";
|
||||
newLink.dataset.socialIndex = socialLinkIndex;
|
||||
newLink.innerHTML = `
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-3">
|
||||
<label class="form-label fw-medium">Platform</label>
|
||||
<input type="text" class="form-control" name="social[links][${socialLinkIndex}][platform]" value="">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label fw-medium">URL</label>
|
||||
<input type="text" class="form-control" name="social[links][${socialLinkIndex}][url]" value="" placeholder="https://facebook.com/...">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label fw-medium">Icon Class</label>
|
||||
<input type="text" class="form-control" name="social[links][${socialLinkIndex}][icon]" value="">
|
||||
</div>
|
||||
<div class="col-md-1">
|
||||
<label class="form-label"> </label>
|
||||
<button type="button" class="btn btn-outline-danger btn-sm w-100 remove-social-link" data-social-index="${socialLinkIndex}">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
container.appendChild(newLink);
|
||||
socialLinkIndex++;
|
||||
});
|
||||
|
||||
// Remove Column
|
||||
document.addEventListener("click", function (e) {
|
||||
if (e.target.closest(".remove-column")) {
|
||||
e.target.closest(".card").remove();
|
||||
}
|
||||
});
|
||||
|
||||
// Remove Social Link
|
||||
document.addEventListener("click", function (e) {
|
||||
if (e.target.closest(".remove-social-link")) {
|
||||
e.target.closest(".card").remove();
|
||||
}
|
||||
});
|
||||
|
||||
// Add Link to Column
|
||||
document.addEventListener("click", function (e) {
|
||||
if (e.target.closest(".add-link")) {
|
||||
const columnIndex = e.target.closest(".add-link").dataset.columnIndex;
|
||||
const container = e.target
|
||||
.closest(".card-body")
|
||||
.querySelector(".column-links-container");
|
||||
const linkIndex = container.children.length;
|
||||
|
||||
const newLink = document.createElement("div");
|
||||
newLink.className = "card mb-2 border";
|
||||
newLink.dataset.linkIndex = linkIndex;
|
||||
newLink.innerHTML = `
|
||||
<div class="card-body">
|
||||
<div class="row g-2">
|
||||
<div class="col-md-5">
|
||||
<label class="form-label form-label-sm">Link Title</label>
|
||||
<input type="text" class="form-control form-control-sm" name="columns[${columnIndex}][links][${linkIndex}][title]" value="">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label form-label-sm">URL</label>
|
||||
<input type="text" class="form-control form-control-sm" name="columns[${columnIndex}][links][${linkIndex}][url]" value="" placeholder="/about-us/">
|
||||
</div>
|
||||
<div class="col-md-1">
|
||||
<label class="form-label form-label-sm"> </label>
|
||||
<button type="button" class="btn btn-outline-danger btn-sm w-100 remove-link" data-column-index="${columnIndex}" data-link-index="${linkIndex}">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
container.appendChild(newLink);
|
||||
}
|
||||
});
|
||||
|
||||
// Remove Link from Column
|
||||
document.addEventListener("click", function (e) {
|
||||
if (e.target.closest(".remove-link")) {
|
||||
e.target.closest(".card").remove();
|
||||
}
|
||||
});
|
||||
|
||||
// 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);
|
||||
});
|
||||
});
|
||||
|
||||
// Form submission
|
||||
document
|
||||
.getElementById("footerForm")
|
||||
.addEventListener("submit", function (e) {
|
||||
// Build consolidated JSON payload for footer and set hidden input
|
||||
try {
|
||||
const buildFooterData = () => {
|
||||
const getVal = (selector) => {
|
||||
const el = document.querySelector(selector);
|
||||
return el ? el.value : '';
|
||||
};
|
||||
|
||||
const about = {
|
||||
title: getVal('input[name="about[title]"]'),
|
||||
description: getVal('textarea[name="about[description]"]'),
|
||||
mapLink: {
|
||||
text: getVal('input[name="about[mapLink][text]"]'),
|
||||
url: getVal('input[name="about[mapLink][url]"]')
|
||||
}
|
||||
};
|
||||
|
||||
const logo = {
|
||||
src: getVal('input[name="logo[src]"]'),
|
||||
alt: getVal('input[name="logo[alt]"]')
|
||||
};
|
||||
|
||||
const address = {
|
||||
text: getVal('input[name="address[text]"]'),
|
||||
address2: getVal('input[name="address[address2]"]'),
|
||||
mapUrl: getVal('input[name="address[mapUrl]"]'),
|
||||
link2: getVal('input[name="address[link2]"]')
|
||||
};
|
||||
|
||||
const contact = {
|
||||
phone: getVal('input[name="contact[phone]"]'),
|
||||
hours: getVal('input[name="contact[hours]"]'),
|
||||
email: getVal('input[name="contact[email]"]')
|
||||
};
|
||||
|
||||
// Columns
|
||||
const columns = [];
|
||||
document.querySelectorAll('#columnsContainer > .card[data-column-index]').forEach(colCard => {
|
||||
const colIndex = colCard.dataset.columnIndex;
|
||||
const titleEl = colCard.querySelector(`input[name="columns[${colIndex}][title]"]`);
|
||||
const column = { title: titleEl ? titleEl.value : '', links: [] };
|
||||
const linksContainer = colCard.querySelector('.column-links-container');
|
||||
if (linksContainer) {
|
||||
linksContainer.querySelectorAll('.card[data-link-index]').forEach(linkCard => {
|
||||
const linkTitle = linkCard.querySelector('input[name^="columns"][name$="[title]"]');
|
||||
const linkUrl = linkCard.querySelector('input[name^="columns"][name$="[url]"]');
|
||||
if (linkTitle && linkUrl) {
|
||||
column.links.push({ title: linkTitle.value, url: linkUrl.value });
|
||||
}
|
||||
});
|
||||
}
|
||||
columns.push(column);
|
||||
});
|
||||
|
||||
// Social links
|
||||
const socialLinks = [];
|
||||
document.querySelectorAll('#socialLinksContainer > .card[data-social-index]').forEach(sCard => {
|
||||
const platform = sCard.querySelector('input[name^="social"][name$="[platform]"]')?.value || '';
|
||||
const url = sCard.querySelector('input[name^="social"][name$="[url]"]')?.value || '';
|
||||
const icon = sCard.querySelector('input[name^="social"][name$="[icon]"]')?.value || '';
|
||||
if (platform || url || icon) socialLinks.push({ platform, url, icon });
|
||||
});
|
||||
|
||||
const copyright = { text: getVal('textarea[name="copyright[text]"]') };
|
||||
|
||||
return { about, logo, address, contact, columns, social: { links: socialLinks }, copyright };
|
||||
};
|
||||
|
||||
const payload = buildFooterData();
|
||||
document.getElementById('footerJson').value = JSON.stringify(payload);
|
||||
} catch (err) {
|
||||
console.error('Error building footerJson payload:', err);
|
||||
}
|
||||
// Show loading state
|
||||
const submitBtn = this.querySelector('button[type="submit"]');
|
||||
const originalText = submitBtn.innerHTML;
|
||||
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Saving...';
|
||||
submitBtn.disabled = true;
|
||||
});
|
||||
});
|
||||
|
||||
function openImageUploader(targetInput, imageType) {
|
||||
// Tạo input file ẩn
|
||||
const fileInput = document.createElement("input");
|
||||
fileInput.type = "file";
|
||||
fileInput.accept = "image/*";
|
||||
fileInput.style.display = "none";
|
||||
document.body.appendChild(fileInput);
|
||||
|
||||
// Xử lý khi chọn file
|
||||
fileInput.onchange = async function (e) {
|
||||
const file = e.target.files[0];
|
||||
if (!file) return;
|
||||
|
||||
try {
|
||||
// Tạo FormData
|
||||
const formData = new FormData();
|
||||
formData.append("image", file);
|
||||
|
||||
// Disable nút upload và hiển thị loading
|
||||
const uploadBtn = document.querySelector(
|
||||
`[data-target-input="${targetInput}"]`
|
||||
);
|
||||
const originalBtnHtml = uploadBtn.innerHTML;
|
||||
uploadBtn.disabled = true;
|
||||
uploadBtn.innerHTML =
|
||||
'<i class="fas fa-spinner fa-spin me-1"></i>Uploading...';
|
||||
|
||||
// Gửi request upload
|
||||
const response = await fetch(
|
||||
`/admin/upload/image?imageType=${imageType}`,
|
||||
{
|
||||
method: "POST",
|
||||
body: formData,
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Upload failed");
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || "Upload failed");
|
||||
}
|
||||
|
||||
// Cập nhật đường dẫn ảnh vào input (support name or id)
|
||||
const input = document.querySelector(`input[name="${targetInput}"]`) || document.getElementById(targetInput) || document.querySelector(`#${targetInput}`);
|
||||
if (!input) {
|
||||
throw new Error("Target input not found");
|
||||
}
|
||||
|
||||
input.value = result.path;
|
||||
|
||||
// Tìm hoặc tạo preview container
|
||||
let imgPreview = input.parentElement.nextElementSibling;
|
||||
while (imgPreview && !imgPreview.classList.contains("mt-3")) {
|
||||
imgPreview = imgPreview.nextElementSibling;
|
||||
}
|
||||
|
||||
if (!imgPreview) {
|
||||
// Tạo mới phần tử preview nếu chưa có
|
||||
imgPreview = document.createElement("div");
|
||||
imgPreview.className = "mt-3";
|
||||
const img = document.createElement("img");
|
||||
img.className = "img-thumbnail";
|
||||
img.style.maxHeight = "100px";
|
||||
img.style.maxWidth = "300px";
|
||||
img.style.objectFit = "contain";
|
||||
img.style.background = "var(--primary-color)";
|
||||
img.alt = "Image preview";
|
||||
imgPreview.appendChild(img);
|
||||
input.parentElement.parentElement.appendChild(imgPreview);
|
||||
}
|
||||
|
||||
// Cập nhật ảnh preview
|
||||
const img = imgPreview.querySelector("img");
|
||||
if (img) {
|
||||
img.src = result.path;
|
||||
}
|
||||
|
||||
// Restore nút upload
|
||||
uploadBtn.disabled = false;
|
||||
uploadBtn.innerHTML = originalBtnHtml;
|
||||
|
||||
// Cleanup
|
||||
document.body.removeChild(fileInput);
|
||||
} catch (error) {
|
||||
console.error("Upload error:", error);
|
||||
alert("Upload failed: " + error.message);
|
||||
|
||||
// Restore nút upload
|
||||
const uploadBtn = document.querySelector(
|
||||
`[data-target-input="${targetInput}"]`
|
||||
);
|
||||
uploadBtn.disabled = false;
|
||||
uploadBtn.innerHTML = originalBtnHtml;
|
||||
|
||||
// Cleanup
|
||||
if (document.body.contains(fileInput)) {
|
||||
document.body.removeChild(fileInput);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Trigger file selection
|
||||
fileInput.click();
|
||||
}
|
||||
</script>
|
||||
661
views/admin/form/index.ejs
Normal file
661
views/admin/form/index.ejs
Normal file
@@ -0,0 +1,661 @@
|
||||
<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);">Form Management</h1>
|
||||
<p class="text-muted mb-0">Edit form content for Academics page</p>
|
||||
</div>
|
||||
<div>
|
||||
<a href="/admin/academics" class="btn btn-outline-primary">
|
||||
<i class="fas fa-arrow-left me-1"></i>Back to Academics
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<form id="formManagementForm" action="/admin/form/update" method="POST" class="content-with-fixed-buttons">
|
||||
<input type="hidden" name="admissionJson" id="admissionJson">
|
||||
<input type="hidden" name="applyJson" id="applyJson">
|
||||
<input type="hidden" name="applicationFormJson" id="applicationFormJson">
|
||||
|
||||
<!-- 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="#admission" role="tab">
|
||||
<i class="fas fa-door-open me-2"></i>Admission
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-bs-toggle="tab" href="#apply" role="tab">
|
||||
<i class="fas fa-check-circle me-2"></i>Apply
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-bs-toggle="tab" href="#application-form" role="tab">
|
||||
<i class="fas fa-file-alt me-2"></i>Application Form
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<div class="tab-content">
|
||||
<!-- Admission Tab -->
|
||||
<div class="tab-pane fade show active" id="admission" role="tabpanel">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-medium">Background Image</label>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" id="admissionBgImage" name="admissionBgImage" value="<%= form.admission && form.admission.background_image ? form.admission.background_image : '' %>">
|
||||
<button type="button" class="btn btn-outline-primary btn-upload-image" data-target-input="admissionBgImage" data-image-type="academics">
|
||||
<i class="fas fa-upload me-1"></i>Upload
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="image-preview mt-2">
|
||||
<% if (form.admission && form.admission.background_image) { %>
|
||||
<img src="<%= form.admission.background_image %>" alt="Background preview" class="img-thumbnail" style="height:200px;width:100%;object-fit:cover;">
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Title</label>
|
||||
<input type="text" class="form-control" id="admissionTitle" name="admissionTitle" value="<%= form.admission && form.admission.title ? form.admission.title : '' %>">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Year</label>
|
||||
<input type="text" class="form-control" id="admissionYear" name="admissionYear" value="<%= form.admission && form.admission.year ? form.admission.year : '' %>">
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-medium">Description</label>
|
||||
<textarea class="form-control" id="admissionDescription" name="admissionDescription" rows="3"><%= form.admission && form.admission.description ? form.admission.description : '' %></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Admission Form Fields -->
|
||||
<div class="col-md-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">Form Fields</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="admissionFormFields">
|
||||
<% (form.admission && form.admission.form && form.admission.form.fields || []).forEach((field, index) => { %>
|
||||
<div class="mb-3 border p-3">
|
||||
<div class="row g-2">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Field Type</label>
|
||||
<input type="text" class="form-control" value="<%= field.type %>">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Placeholder</label>
|
||||
<input type="text" class="form-control" value="<%= field.placeholder %>">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% }); %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Form Button Text</label>
|
||||
<input type="text" class="form-control" id="admissionFormButton" name="admissionFormButton" value="<%= form.admission && form.admission.form && form.admission.form.button ? form.admission.form.button.text : '' %>">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Form Button URL</label>
|
||||
<input type="text" class="form-control" id="admissionFormButtonUrl" name="admissionFormButtonUrl" value="<%= form.admission && form.admission.form && form.admission.form.button ? form.admission.form.button.url : '' %>">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Apply Tab -->
|
||||
<div class="tab-pane fade" id="apply" role="tabpanel">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-medium">Title</label>
|
||||
<input type="text" class="form-control" id="applyTitle" name="applyTitle" value="<%= form.apply && form.apply.title ? form.apply.title : '' %>">
|
||||
</div>
|
||||
|
||||
<!-- Apply Steps -->
|
||||
<div class="col-md-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">Application Steps</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="applySteps">
|
||||
<% (form.apply && form.apply.steps || []).forEach((step, index) => { %>
|
||||
<div class="mb-3 border p-3">
|
||||
<div class="row g-2">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Title</label>
|
||||
<input type="text" class="form-control" name="applyStepTitle_<%= index %>" value="<%= step.title %>">
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label">Description</label>
|
||||
<textarea class="form-control" name="applyStepDescription_<%= index %>" rows="2"><%= step.description %></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% }); %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Application Form Tab -->
|
||||
<div class="tab-pane fade" id="application-form" role="tabpanel">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-medium">Title</label>
|
||||
<input type="text" class="form-control" id="applicationFormTitle" name="applicationFormTitle" value="<%= form.application_form && form.application_form.title ? form.application_form.title : '' %>">
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-medium">Question</label>
|
||||
<input type="text" class="form-control" id="applicationFormQuestion" name="applicationFormQuestion" value="<%= form.application_form && form.application_form.question ? form.application_form.question : '' %>">
|
||||
</div>
|
||||
|
||||
<!-- Button Section -->
|
||||
<div class="col-md-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">Button</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-2">
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Text</label>
|
||||
<input type="text" class="form-control" id="applicationFormButtonText" name="applicationFormButtonText" value="<%= form.application_form && form.application_form.button ? form.application_form.button.text : '' %>">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Icon</label>
|
||||
<input type="text" class="form-control" id="applicationFormButtonIcon" name="applicationFormButtonIcon" value="<%= form.application_form && form.application_form.button ? form.application_form.button.icon : '' %>">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">URL</label>
|
||||
<input type="text" class="form-control" id="applicationFormButtonUrl" name="applicationFormButtonUrl" value="<%= form.application_form && form.application_form.button ? form.application_form.button.url : '' %>">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Links Section -->
|
||||
<div class="col-md-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">Links</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="applicationFormLinks">
|
||||
<% (form.application_form && form.application_form.links || []).forEach((link, index) => { %>
|
||||
<div class="mb-3 border p-3">
|
||||
<div class="row g-2">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Text</label>
|
||||
<input type="text" class="form-control" name="applicationFormLinkText_<%= index %>" value="<%= link.text %>">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">URL</label>
|
||||
<input type="text" class="form-control" name="applicationFormLinkUrl_<%= index %>" value="<%= link.url %>">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% }); %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Fixed Bottom Buttons -->
|
||||
<div class="fixed-bottom-buttons">
|
||||
<button type="reset" class="btn btn-secondary">
|
||||
<i class="fas fa-undo"></i>
|
||||
<span>Reset</span>
|
||||
</button>
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fas fa-save"></i>
|
||||
<span>Save Changes</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Thêm input file ẩn cho upload ảnh -->
|
||||
<input type="file" id="directImageUpload" style="display: none;">
|
||||
<input type="hidden" id="currentImageType" name="imageType">
|
||||
<input type="hidden" id="currentTargetInput" name="targetInput">
|
||||
|
||||
<script>
|
||||
let originalFormData = null;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Lưu trữ dữ liệu gốc để so sánh thay đổi
|
||||
originalFormData = <%- JSON.stringify(form) %>;
|
||||
|
||||
// Khởi tạo form handlers
|
||||
initializeFormHandlers();
|
||||
});
|
||||
|
||||
function initializeFormHandlers() {
|
||||
// Form submission
|
||||
const form = document.querySelector('form');
|
||||
form.addEventListener('submit', handleFormSubmit);
|
||||
|
||||
// Upload ảnh
|
||||
document.querySelectorAll('.btn-upload-image').forEach(button => {
|
||||
button.addEventListener('click', function() {
|
||||
const targetInput = this.dataset.targetInput;
|
||||
const imageType = this.dataset.imageType || 'academics';
|
||||
openImageUploader(targetInput, imageType);
|
||||
});
|
||||
});
|
||||
|
||||
// Khởi tạo direct image upload
|
||||
initializeDirectImageUpload();
|
||||
}
|
||||
|
||||
function initializeDirectImageUpload() {
|
||||
const fileInput = document.getElementById('directImageUpload');
|
||||
fileInput.addEventListener('change', handleDirectImageUpload);
|
||||
}
|
||||
|
||||
function openImageUploader(targetInput, imageType) {
|
||||
// Lưu thông tin upload hiện tại
|
||||
document.getElementById('currentImageType').value = imageType;
|
||||
document.getElementById('currentTargetInput').value = targetInput;
|
||||
|
||||
// Kích hoạt input file
|
||||
const fileInput = document.getElementById('directImageUpload');
|
||||
fileInput.click();
|
||||
}
|
||||
|
||||
async function handleDirectImageUpload(e) {
|
||||
if (!this.files || !this.files[0]) return;
|
||||
|
||||
const file = this.files[0];
|
||||
const imageType = document.getElementById('currentImageType').value;
|
||||
const targetInput = document.getElementById('currentTargetInput').value;
|
||||
|
||||
try {
|
||||
// Disable nút upload và hiển thị loading
|
||||
const uploadButton = document.querySelector(`[data-target-input="${targetInput}"]`);
|
||||
if (uploadButton) {
|
||||
uploadButton.disabled = true;
|
||||
uploadButton.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Uploading...';
|
||||
}
|
||||
|
||||
// Tạo form data
|
||||
const formData = new FormData();
|
||||
formData.append('image', file);
|
||||
|
||||
// Upload ảnh
|
||||
const response = await fetch(`/admin/upload/image?imageType=${imageType}`, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success && result.path) {
|
||||
// Tìm input cần cập nhật, ưu tiên theo name, nếu không có thì tìm theo id
|
||||
const inputElement = document.querySelector(`[name="${targetInput}"]`) || document.getElementById(targetInput);
|
||||
if (inputElement) {
|
||||
inputElement.value = result.path;
|
||||
// Cập nhật preview
|
||||
updateImagePreview(inputElement, result.path);
|
||||
}
|
||||
|
||||
showToast('Success', 'Image uploaded successfully', 'success');
|
||||
} else {
|
||||
throw new Error(result.error || 'Upload failed');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Upload error:', error);
|
||||
showToast('Error', 'Failed to upload image: ' + error.message, 'error');
|
||||
} finally {
|
||||
// Reset nút upload
|
||||
const uploadButton = document.querySelector(`[data-target-input="${targetInput}"]`);
|
||||
if (uploadButton) {
|
||||
uploadButton.disabled = false;
|
||||
uploadButton.innerHTML = '<i class="fas fa-upload me-1"></i>Upload';
|
||||
}
|
||||
// Reset file input
|
||||
this.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
function updateImagePreview(inputElement, imagePath) {
|
||||
// Tìm hoặc tạo phần tử preview theo cách giống như Partnerships
|
||||
let imgPreview = inputElement.parentElement.nextElementSibling;
|
||||
while (imgPreview && !imgPreview.classList.contains('mt-2')) {
|
||||
imgPreview = imgPreview.nextElementSibling;
|
||||
}
|
||||
|
||||
if (!imgPreview) {
|
||||
// Tạo mới phần tử preview nếu chưa có
|
||||
imgPreview = document.createElement('div');
|
||||
imgPreview.className = 'mt-2';
|
||||
const img = document.createElement('img');
|
||||
img.className = 'img-thumbnail';
|
||||
|
||||
// Set style dựa vào loại ảnh
|
||||
const targetInput = inputElement.name || inputElement.id;
|
||||
if (targetInput.toLowerCase().includes('logo')) {
|
||||
img.style.height = '100px';
|
||||
img.style.objectFit = 'contain';
|
||||
} else {
|
||||
img.style.height = '200px';
|
||||
img.style.width = '100%';
|
||||
img.style.objectFit = 'cover';
|
||||
}
|
||||
|
||||
img.alt = 'Image preview';
|
||||
imgPreview.appendChild(img);
|
||||
inputElement.parentElement.parentElement.appendChild(imgPreview);
|
||||
}
|
||||
|
||||
// Cập nhật đường dẫn hình ảnh
|
||||
const img = imgPreview.querySelector('img');
|
||||
if (img) {
|
||||
img.src = imagePath;
|
||||
}
|
||||
}
|
||||
|
||||
function handleFormSubmit(e) {
|
||||
e.preventDefault();
|
||||
|
||||
try {
|
||||
console.log('=== Form Submit Debug ===');
|
||||
|
||||
// Chuẩn bị dữ liệu form
|
||||
const formData = prepareFormData();
|
||||
console.log('Complete form data:', formData);
|
||||
|
||||
// Log từng section
|
||||
Object.keys(formData).forEach(key => {
|
||||
console.log(`${key} data:`, formData[key]);
|
||||
});
|
||||
|
||||
// Validate dữ liệu
|
||||
const errors = validateFormData(formData);
|
||||
if (errors.length > 0) {
|
||||
console.log('Validation errors:', errors);
|
||||
showToast('Error', errors.join('. '), 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// Log hidden inputs trước khi cập nhật
|
||||
Object.keys(formData).forEach(key => {
|
||||
const input = document.getElementById(`${key}Json`);
|
||||
console.log(`Hidden input ${key}Json:`, {
|
||||
element: input,
|
||||
value: input?.value
|
||||
});
|
||||
});
|
||||
|
||||
// Cập nhật hidden inputs (map key -> input id)
|
||||
const keyToInputId = {
|
||||
admission: 'admissionJson',
|
||||
apply: 'applyJson',
|
||||
application_form: 'applicationFormJson'
|
||||
};
|
||||
|
||||
Object.keys(formData).forEach(key => {
|
||||
const inputId = keyToInputId[key] || `${key}Json`;
|
||||
const input = document.getElementById(inputId);
|
||||
if (input) {
|
||||
const jsonValue = JSON.stringify(formData[key]);
|
||||
input.value = jsonValue;
|
||||
console.log(`Updated ${inputId} with:`, jsonValue);
|
||||
} else {
|
||||
console.warn(`Hidden input not found: ${inputId}`);
|
||||
}
|
||||
});
|
||||
|
||||
// Thông báo đang lưu
|
||||
showToast('Info', 'Saving changes...', 'info');
|
||||
|
||||
// Log form final state
|
||||
console.log('Form final state:', {
|
||||
admissionJson: document.getElementById('admissionJson')?.value,
|
||||
applyJson: document.getElementById('applyJson')?.value,
|
||||
applicationFormJson: document.getElementById('applicationFormJson')?.value
|
||||
});
|
||||
|
||||
// Submit form
|
||||
this.submit();
|
||||
} catch (error) {
|
||||
console.error('Form submission error:', error);
|
||||
console.error('Error stack:', error.stack);
|
||||
showToast('Error', 'Error processing form: ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function prepareFormData() {
|
||||
return {
|
||||
admission: prepareAdmissionData(),
|
||||
apply: prepareApplyData(),
|
||||
application_form: prepareApplicationFormData()
|
||||
};
|
||||
}
|
||||
|
||||
function prepareAdmissionData() {
|
||||
try {
|
||||
// Basic info
|
||||
const data = {
|
||||
background_image: document.getElementById('admissionBgImage')?.value || '',
|
||||
title: document.getElementById('admissionTitle')?.value || '',
|
||||
year: document.getElementById('admissionYear')?.value || '',
|
||||
description: document.getElementById('admissionDescription')?.value || '',
|
||||
form: {
|
||||
fields: [],
|
||||
button: {
|
||||
text: document.getElementById('admissionFormButton')?.value || '',
|
||||
url: document.getElementById('admissionFormButtonUrl')?.value || '#'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Form fields
|
||||
const formFields = document.querySelectorAll('#admissionFormFields > div');
|
||||
if (formFields) {
|
||||
data.form.fields = Array.from(formFields).map(field => {
|
||||
const inputs = field.querySelectorAll('input');
|
||||
return {
|
||||
type: inputs[0]?.value || '',
|
||||
placeholder: inputs[1]?.value || ''
|
||||
};
|
||||
}).filter(field => field.type || field.placeholder);
|
||||
}
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('Error preparing admission data:', error);
|
||||
return {
|
||||
background_image: '',
|
||||
title: '',
|
||||
year: '',
|
||||
description: '',
|
||||
form: {
|
||||
fields: [],
|
||||
button: {
|
||||
text: '',
|
||||
url: '#'
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function prepareApplyData() {
|
||||
try {
|
||||
const data = {
|
||||
title: document.getElementById('applyTitle')?.value || '',
|
||||
steps: []
|
||||
};
|
||||
|
||||
// Steps
|
||||
const steps = document.querySelectorAll('#applySteps > div');
|
||||
if (steps) {
|
||||
data.steps = Array.from(steps).map(step => ({
|
||||
title: step.querySelector('input[name^="applyStepTitle_"]')?.value || '',
|
||||
description: step.querySelector('textarea[name^="applyStepDescription_"]')?.value || ''
|
||||
})).filter(step => step.title || step.description);
|
||||
}
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('Error preparing apply data:', error);
|
||||
return {
|
||||
title: '',
|
||||
steps: []
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function prepareApplicationFormData() {
|
||||
try {
|
||||
console.log('=== Debug Application Form Data ===');
|
||||
|
||||
// Log các elements
|
||||
console.log('Title element:', document.getElementById('applicationFormTitle'));
|
||||
console.log('Question element:', document.getElementById('applicationFormQuestion'));
|
||||
console.log('Button text element:', document.getElementById('applicationFormButtonText'));
|
||||
console.log('Button icon element:', document.getElementById('applicationFormButtonIcon'));
|
||||
|
||||
const data = {
|
||||
title: document.getElementById('applicationFormTitle')?.value || '',
|
||||
question: document.getElementById('applicationFormQuestion')?.value || '',
|
||||
button: {
|
||||
text: document.getElementById('applicationFormButtonText')?.value || '',
|
||||
icon: document.getElementById('applicationFormButtonIcon')?.value || '',
|
||||
url: document.getElementById('applicationFormButtonUrl')?.value || '#'
|
||||
},
|
||||
links: []
|
||||
};
|
||||
|
||||
// Log giá trị cơ bản
|
||||
console.log('Basic data:', {
|
||||
title: data.title,
|
||||
question: data.question,
|
||||
button: data.button
|
||||
});
|
||||
|
||||
// Debug links
|
||||
const linksContainer = document.getElementById('applicationFormLinks');
|
||||
console.log('Links container:', linksContainer);
|
||||
|
||||
const linkDivs = document.querySelectorAll('#applicationFormLinks > div');
|
||||
console.log('Found link divs:', linkDivs.length);
|
||||
|
||||
if (linkDivs.length > 0) {
|
||||
data.links = Array.from(linkDivs).map((link, index) => {
|
||||
const textInput = link.querySelector('input[name^="applicationFormLinkText_"]');
|
||||
const urlInput = link.querySelector('input[name^="applicationFormLinkUrl_"]');
|
||||
|
||||
return {
|
||||
text: textInput?.value || '',
|
||||
url: urlInput?.value || ''
|
||||
};
|
||||
}).filter(link => link.text || link.url);
|
||||
}
|
||||
|
||||
console.log('Final application form data:', data);
|
||||
return data;
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error preparing application form data:', error);
|
||||
console.error('Error stack:', error.stack);
|
||||
return {
|
||||
title: '',
|
||||
question: '',
|
||||
button: {
|
||||
text: '',
|
||||
icon: '',
|
||||
url: '#'
|
||||
},
|
||||
links: []
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function validateFormData(data) {
|
||||
const errors = [];
|
||||
|
||||
// Validate admission
|
||||
if (!data.admission.title) errors.push('Admission title is required');
|
||||
if (!data.admission.year) errors.push('Admission year is required');
|
||||
|
||||
// Validate apply
|
||||
if (!data.apply.title) errors.push('Apply title is required');
|
||||
if (!Array.isArray(data.apply.steps)) errors.push('Apply steps must be an array');
|
||||
|
||||
// Validate application form
|
||||
if (!data.application_form.title) errors.push('Application form title is required');
|
||||
if (!data.application_form.question) errors.push('Application form question is required');
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
function showToast(title, message, type = 'info') {
|
||||
if (window.toastManager) {
|
||||
window.toastManager[type](message);
|
||||
return;
|
||||
}
|
||||
|
||||
const toast = document.createElement('div');
|
||||
toast.className = `toast align-items-center text-white bg-${type === 'error' ? 'danger' : type} border-0`;
|
||||
toast.setAttribute('role', 'alert');
|
||||
toast.setAttribute('aria-live', 'assertive');
|
||||
toast.setAttribute('aria-atomic', 'true');
|
||||
|
||||
toast.innerHTML = `
|
||||
<div class="d-flex">
|
||||
<div class="toast-body">
|
||||
<strong>${title}:</strong> ${message}
|
||||
</div>
|
||||
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Add toast to container
|
||||
let container = document.querySelector('.toast-container');
|
||||
if (!container) {
|
||||
container = document.createElement('div');
|
||||
container.className = 'toast-container position-fixed top-0 end-0 p-3';
|
||||
document.body.appendChild(container);
|
||||
}
|
||||
container.appendChild(toast);
|
||||
|
||||
// Show toast
|
||||
const bsToast = new bootstrap.Toast(toast, {
|
||||
animation: true,
|
||||
autohide: true,
|
||||
delay: 3000
|
||||
});
|
||||
bsToast.show();
|
||||
|
||||
// Remove toast after hidden
|
||||
toast.addEventListener('hidden.bs.toast', () => {
|
||||
toast.remove();
|
||||
});
|
||||
}
|
||||
</script>
|
||||
934
views/admin/header/index.ejs
Normal file
934
views/admin/header/index.ejs
Normal file
@@ -0,0 +1,934 @@
|
||||
<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);">Header Management</h1>
|
||||
<p class="text-muted mb-0">Edit header content and menu structure</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<form action="/admin/header/update" method="POST" class="content-with-fixed-buttons" id="headerForm">
|
||||
<!-- Hidden inputs for JSON data -->
|
||||
<input type="hidden" name="topbarJson" id="topbarJson">
|
||||
<input type="hidden" name="logo" id="logoInput">
|
||||
<input type="hidden" name="activeTab" id="activeTabInput" value="topbar">
|
||||
<input type="hidden" name="menuUpdates" id="menuUpdates">
|
||||
|
||||
<!-- 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="#topbar" role="tab">
|
||||
<i class="fas fa-bars me-2"></i>Topbar
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-bs-toggle="tab" href="#logo" role="tab">
|
||||
<i class="fas fa-image me-2"></i>Logo
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-bs-toggle="tab" href="#menu" role="tab">
|
||||
<i class="fas fa-sitemap me-2"></i>Menu Structure
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<div class="tab-content">
|
||||
<!-- Topbar Tab -->
|
||||
<div class="tab-pane fade show active" id="topbar" role="tabpanel">
|
||||
<div class="card border shadow-sm">
|
||||
<div class="card-header bg-light">
|
||||
<h6 class="mb-0">Topbar Configuration</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<!-- Contact Info -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<h6 class="fw-medium mb-3">Contact Information</h6>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Phone Number</label>
|
||||
<input type="text" class="form-control" id="contactPhone"
|
||||
value="<%= data.topbar.contactInfo.phone %>">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Email</label>
|
||||
<input type="email" class="form-control" id="contactEmail"
|
||||
value="<%= data.topbar.contactInfo.email || '' %>">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick Links -->
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<h6 class="fw-medium mb-3">Quick Links</h6>
|
||||
<div id="quickLinksContainer">
|
||||
<% data.topbar.links.forEach((link, index)=> { %>
|
||||
<div class="card mb-3 border">
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-5">
|
||||
<label class="form-label fw-medium">Link Text</label>
|
||||
<input type="text" class="form-control quick-link-text" value="<%= link.text %>"
|
||||
data-index="<%= index %>">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">URL</label>
|
||||
<input type="url" class="form-control quick-link-url" value="<%= link.url %>"
|
||||
data-index="<%= index %>">
|
||||
</div>
|
||||
<div class="col-md-1">
|
||||
<label class="form-label"> </label>
|
||||
<button type="button" class="btn btn-outline-danger btn-sm w-100 remove-quick-link"
|
||||
data-index="<%= index %>">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% }); %>
|
||||
</div>
|
||||
<button type="button" class="btn btn-outline-primary btn-sm" id="addQuickLink">
|
||||
<i class="fas fa-plus me-1"></i>Add Quick Link
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Logo Tab -->
|
||||
<div class="tab-pane fade" id="logo" role="tabpanel">
|
||||
<div class="card border shadow-sm">
|
||||
<div class="card-header bg-light">
|
||||
<h6 class="mb-0">Logo Configuration</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-5">
|
||||
<label class="form-label fw-medium">Logo Image</label>
|
||||
<div class="input-group mb-2">
|
||||
<input type="text" class="form-control" id="logoImage" value="<%= data.logo %>">
|
||||
<button type="button" class="btn btn-outline-primary btn-upload-image"
|
||||
data-target-input="logoImage" data-image-type="layout">
|
||||
<i class="fas fa-upload me-1"></i>Upload
|
||||
</button>
|
||||
</div>
|
||||
<small class="form-text text-muted">Recommended size: 200x60px</small>
|
||||
</div>
|
||||
<div class="col-md-7">
|
||||
<% if (data.logo) { %>
|
||||
<img src="<%= data.logo %>" class="img-thumbnail"
|
||||
style="max-height: 100px; max-width: 300px; object-fit: contain; background: #b8b76a;"
|
||||
alt="Logo preview">
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Menu Structure Tab -->
|
||||
<div class="tab-pane fade" id="menu" role="tabpanel">
|
||||
<div class="card border shadow-sm">
|
||||
<div class="card-header bg-light d-flex justify-content-between align-items-center">
|
||||
<h6 class="mb-0">Menu Structure</h6>
|
||||
<div>
|
||||
<button type="button" class="btn btn-outline-success btn-sm me-2" id="saveMenuChanges"
|
||||
style="display: none;">
|
||||
<i class="fas fa-save me-1"></i>Save Changes
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-primary btn-sm" id="refreshMenuTree">
|
||||
<i class="fas fa-sync-alt me-1"></i>Refresh Menu Tree
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="alert alert-info">
|
||||
<i class="fas fa-info-circle me-2"></i>
|
||||
Click "Save Changes" on top right to apply changes.
|
||||
</div>
|
||||
|
||||
<div id="menuTreeContainer">
|
||||
<div class="text-center">
|
||||
<div class="spinner-border text-primary" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
<p class="mt-2">Loading menu structure...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Move buttons to fixed bottom -->
|
||||
<div class="fixed-bottom-buttons">
|
||||
<button type="reset" class="btn btn-secondary">
|
||||
<i class="fas fa-undo"></i>
|
||||
<span>Reset</span>
|
||||
</button>
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fas fa-save"></i>
|
||||
<span>Save Changes</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Menu Tree Template -->
|
||||
<template id="menuTreeTemplate">
|
||||
<div class="menu-tree">
|
||||
<div class="menu-items">
|
||||
<!-- Menu items will be populated here -->
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Menu Item Template -->
|
||||
<template id="menuItemTemplate">
|
||||
<div class="menu-item card mb-2 border" data-menuid="">
|
||||
<div class="card-body p-3">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="flex-grow-1">
|
||||
<div class="d-flex align-items-center mb-2">
|
||||
<span class="badge bg-primary me-2 menu-type"></span>
|
||||
<h6 class="mb-0 menu-title"></h6>
|
||||
<small class="text-muted ms-2 menu-url"></small>
|
||||
</div>
|
||||
<div class="menu-details">
|
||||
<div class="row g-2">
|
||||
<div class="col-md-3">
|
||||
<label class="form-label form-label-sm mb-1">Title:</label>
|
||||
<input type="text" class="form-control form-control-sm menu-title-input" style="width: 120px;">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label form-label-sm mb-1">Order:</label>
|
||||
<input type="number" class="form-control form-control-sm menu-order-input" min="0" style="width: 80px;">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label form-label-sm mb-1">Type:</label>
|
||||
<select class="form-select form-select-sm menu-type-select" style="width: 100px;">
|
||||
<option value="static">Static</option>
|
||||
<option value="page">Page</option>
|
||||
<option value="level">Level</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label form-label-sm mb-1">Menu Parent:</label>
|
||||
<select class="form-select form-select-sm menu-parent-select" style="width: 130px;">
|
||||
<option value="">Main menu</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<div class="form-check form-switch fetch-toggle" style="display: none;">
|
||||
<input class="form-check-input" type="checkbox" role="switch" data-menuid="">
|
||||
<label class="form-check-label form-label-sm">Programmes</label>
|
||||
</div>
|
||||
<div class="form-check form-switch active-toggle">
|
||||
<input class="form-check-input" type="checkbox" role="switch" data-menuid="" checked>
|
||||
<label class="form-check-label form-label-sm">Active</label>
|
||||
</div>
|
||||
<small class="text-muted menu-fetch-display"></small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ms-2">
|
||||
<button type="button" class="btn btn-outline-info btn-sm view-programmes" style="display: none;">
|
||||
<i class="fas fa-list me-1"></i>Programmes
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="menu-children mt-2" style="display: none;">
|
||||
<!-- Children will be populated here -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.menu-tree {
|
||||
max-height: 600px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.menu-item:hover {
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.menu-children {
|
||||
margin-left: 20px;
|
||||
border-left: 2px solid #e9ecef;
|
||||
padding-left: 15px;
|
||||
}
|
||||
|
||||
.menu-type {
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
|
||||
.content-with-fixed-buttons {
|
||||
padding-bottom: 80px;
|
||||
}
|
||||
|
||||
.fixed-bottom {
|
||||
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.form-label-sm {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.menu-item .form-control-sm,
|
||||
.menu-item .form-select-sm {
|
||||
font-size: 0.75rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
}
|
||||
|
||||
.menu-item .card-body {
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.menu-item .row.g-2 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.menu-item .col-md-3,
|
||||
.menu-item .col-md-6 {
|
||||
padding: 0 0.25rem;
|
||||
}
|
||||
|
||||
/* Toggle switch styling */
|
||||
.form-check-input:checked {
|
||||
background-color: #b8b76a;
|
||||
border-color: #b8b76a;
|
||||
}
|
||||
|
||||
.form-check-input:focus {
|
||||
border-color: #b8b76a;
|
||||
box-shadow: 0 0 0 0.25rem rgba(4, 78, 39, 0.25);
|
||||
}
|
||||
|
||||
.form-check-label {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
margin-bottom: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.fetch-toggle {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
let quickLinkIndex = <%= data.topbar.links.length %>;
|
||||
|
||||
// Initialize form data
|
||||
updateHiddenInputs();
|
||||
|
||||
// Handle Active Tab Persistence
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const activeTabObj = urlParams.get('activeTab');
|
||||
|
||||
if (activeTabObj) {
|
||||
const tabTrigger = document.querySelector(`a[href="#${activeTabObj}"]`);
|
||||
if (tabTrigger) {
|
||||
new bootstrap.Tab(tabTrigger).show();
|
||||
// Update hidden input to match restored tab
|
||||
document.getElementById('activeTabInput').value = activeTabObj;
|
||||
}
|
||||
}
|
||||
|
||||
// Update hidden input on tab change
|
||||
document.querySelectorAll('a[data-bs-toggle="tab"]').forEach(tab => {
|
||||
tab.addEventListener('shown.bs.tab', function (event) {
|
||||
const targetId = event.target.getAttribute('href').substring(1); // remove #
|
||||
document.getElementById('activeTabInput').value = targetId;
|
||||
});
|
||||
});
|
||||
|
||||
// Load menu tree on tab show
|
||||
document.querySelector('a[href="#menu"]').addEventListener('shown.bs.tab', function () {
|
||||
loadMenuTree();
|
||||
});
|
||||
|
||||
// Add Quick Link
|
||||
document.getElementById('addQuickLink').addEventListener('click', function () {
|
||||
const container = document.getElementById('quickLinksContainer');
|
||||
const newLink = document.createElement('div');
|
||||
newLink.className = 'card mb-3 border';
|
||||
newLink.innerHTML =
|
||||
'<div class="card-body">' +
|
||||
'<div class="row g-3">' +
|
||||
'<div class="col-md-5">' +
|
||||
'<label class="form-label fw-medium">Link Text</label>' +
|
||||
'<input type="text" class="form-control quick-link-text" value="" data-index="' + quickLinkIndex + '">' +
|
||||
'</div>' +
|
||||
'<div class="col-md-6">' +
|
||||
'<label class="form-label fw-medium">URL</label>' +
|
||||
'<input type="url" class="form-control quick-link-url" value="" data-index="' + quickLinkIndex + '">' +
|
||||
'</div>' +
|
||||
'<div class="col-md-1">' +
|
||||
'<label class="form-label"> </label>' +
|
||||
'<button type="button" class="btn btn-outline-danger btn-sm w-100 remove-quick-link" data-index="' + quickLinkIndex + '">' +
|
||||
'<i class="fas fa-trash"></i>' +
|
||||
'</button>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
container.appendChild(newLink);
|
||||
quickLinkIndex++;
|
||||
updateHiddenInputs();
|
||||
});
|
||||
|
||||
// Remove Quick Link
|
||||
document.addEventListener('click', function (e) {
|
||||
if (e.target.closest('.remove-quick-link')) {
|
||||
e.target.closest('.card').remove();
|
||||
updateHiddenInputs();
|
||||
}
|
||||
});
|
||||
|
||||
// Update hidden inputs when form changes
|
||||
document.addEventListener('input', updateHiddenInputs);
|
||||
document.addEventListener('change', updateHiddenInputs);
|
||||
|
||||
// Refresh Menu Tree
|
||||
document.getElementById('refreshMenuTree').addEventListener('click', function () {
|
||||
loadMenuTree();
|
||||
});
|
||||
|
||||
// Save Menu Changes
|
||||
document.getElementById('saveMenuChanges').addEventListener('click', function () {
|
||||
saveMenuChanges();
|
||||
});
|
||||
|
||||
// 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);
|
||||
});
|
||||
});
|
||||
|
||||
// Form submission
|
||||
document.getElementById('headerForm').addEventListener('submit', function (e) {
|
||||
e.preventDefault();
|
||||
updateHiddenInputs();
|
||||
|
||||
// Show loading state
|
||||
const submitBtn = this.querySelector('button[type="submit"]');
|
||||
const originalText = submitBtn.innerHTML;
|
||||
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Saving...';
|
||||
submitBtn.disabled = true;
|
||||
|
||||
// Submit form
|
||||
this.submit();
|
||||
});
|
||||
|
||||
function updateHiddenInputs() {
|
||||
// Update topbar JSON
|
||||
const topbarData = {
|
||||
contactInfo: {
|
||||
phone: document.getElementById('contactPhone').value,
|
||||
email: document.getElementById('contactEmail').value
|
||||
},
|
||||
links: []
|
||||
};
|
||||
|
||||
// Collect quick links
|
||||
document.querySelectorAll('.quick-link-text').forEach((input, index) => {
|
||||
const urlInput = document.querySelector(`.quick-link-url[data-index="${input.dataset.index}"]`);
|
||||
if (input.value.trim() && urlInput.value.trim()) {
|
||||
topbarData.links.push({
|
||||
text: input.value.trim(),
|
||||
url: urlInput.value.trim()
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('topbarJson').value = JSON.stringify(topbarData);
|
||||
document.getElementById('logoInput').value = document.getElementById('logoImage').value;
|
||||
|
||||
// Update menu updates hidden input
|
||||
try {
|
||||
const menuUpdates = collectMenuUpdates();
|
||||
document.getElementById('menuUpdates').value = JSON.stringify(menuUpdates);
|
||||
} catch (e) {
|
||||
console.error('Error collecting menu updates:', e);
|
||||
// Fallback to empty array to avoid server errors
|
||||
document.getElementById('menuUpdates').value = '[]';
|
||||
}
|
||||
}
|
||||
|
||||
function loadMenuTree() {
|
||||
const container = document.getElementById('menuTreeContainer');
|
||||
container.innerHTML = `
|
||||
<div class="text-center">
|
||||
<div class="spinner-border text-primary" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
<p class="mt-2">Loading menu structure...</p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
fetch('/admin/header/menu-tree')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
// Store all menu items for parent options
|
||||
allMenuItems = flattenMenuItems(data);
|
||||
renderMenuTree(data);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error loading menu tree:', error);
|
||||
container.innerHTML = `
|
||||
<div class="alert alert-danger">
|
||||
<i class="fas fa-exclamation-triangle me-2"></i>
|
||||
Error loading menu structure. Please try again.
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
}
|
||||
|
||||
function flattenMenuItems(menuItems) {
|
||||
const flattened = [];
|
||||
|
||||
function flatten(items) {
|
||||
items.forEach(item => {
|
||||
flattened.push({
|
||||
menuid: item.menuid,
|
||||
title: item.title,
|
||||
parent: item.parent
|
||||
});
|
||||
|
||||
if (item.children && item.children.length > 0) {
|
||||
flatten(item.children);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
flatten(menuItems);
|
||||
return flattened;
|
||||
}
|
||||
|
||||
function renderMenuTree(menuItems) {
|
||||
const container = document.getElementById('menuTreeContainer');
|
||||
const template = document.getElementById('menuTreeTemplate');
|
||||
const menuTree = template.content.cloneNode(true);
|
||||
const menuItemsContainer = menuTree.querySelector('.menu-items');
|
||||
|
||||
menuItems.forEach(item => {
|
||||
const menuItem = createMenuItem(item);
|
||||
menuItemsContainer.appendChild(menuItem);
|
||||
});
|
||||
|
||||
container.innerHTML = '';
|
||||
container.appendChild(menuTree);
|
||||
}
|
||||
|
||||
function createMenuItem(item) {
|
||||
const template = document.getElementById('menuItemTemplate');
|
||||
const menuItem = template.content.cloneNode(true);
|
||||
|
||||
const card = menuItem.querySelector('.menu-item');
|
||||
const typeBadge = card.querySelector('.menu-type');
|
||||
const title = card.querySelector('.menu-title');
|
||||
const url = card.querySelector('.menu-url');
|
||||
const titleInput = card.querySelector('.menu-title-input');
|
||||
const orderInput = card.querySelector('.menu-order-input');
|
||||
const typeSelect = card.querySelector('.menu-type-select');
|
||||
const parentSelect = card.querySelector('.menu-parent-select');
|
||||
const fetchToggle = card.querySelector('.fetch-toggle');
|
||||
const fetchCheckbox = card.querySelector('.fetch-toggle input');
|
||||
const activeToggle = card.querySelector('.active-toggle');
|
||||
const activeCheckbox = card.querySelector('.active-toggle input');
|
||||
const fetchDisplay = card.querySelector('.menu-fetch-display');
|
||||
const viewProgrammesBtn = card.querySelector('.view-programmes');
|
||||
const childrenContainer = card.querySelector('.menu-children');
|
||||
|
||||
// Set content
|
||||
card.dataset.menuid = item.menuid;
|
||||
typeBadge.textContent = item.type;
|
||||
title.textContent = item.title;
|
||||
url.textContent = item.url;
|
||||
titleInput.value = item.title;
|
||||
orderInput.value = item.order;
|
||||
typeSelect.value = item.type;
|
||||
|
||||
// Populate parent select options
|
||||
populateParentOptions(parentSelect, item.menuid);
|
||||
parentSelect.value = item.parent || '';
|
||||
|
||||
// Show fetch toggle for level type menus
|
||||
if (item.type === 'level') {
|
||||
fetchToggle.style.display = 'block';
|
||||
fetchCheckbox.checked = item.fetch;
|
||||
fetchCheckbox.dataset.menuid = item.menuid;
|
||||
|
||||
// Add change listener for fetch toggle
|
||||
fetchCheckbox.addEventListener('change', function () {
|
||||
showSaveButton();
|
||||
});
|
||||
}
|
||||
|
||||
// Set active toggle state
|
||||
activeCheckbox.checked = item.isActive !== false;
|
||||
activeCheckbox.dataset.menuid = item.menuid;
|
||||
|
||||
// Add change listener for active toggle
|
||||
activeCheckbox.addEventListener('change', function () {
|
||||
showSaveButton();
|
||||
});
|
||||
|
||||
// Add change listeners
|
||||
titleInput.addEventListener('input', function () {
|
||||
showSaveButton();
|
||||
});
|
||||
orderInput.addEventListener('change', function () {
|
||||
showSaveButton();
|
||||
});
|
||||
typeSelect.addEventListener('change', function () {
|
||||
showSaveButton();
|
||||
// Show/hide fetch toggle based on type
|
||||
if (this.value === 'level') {
|
||||
fetchToggle.style.display = 'block';
|
||||
} else {
|
||||
fetchToggle.style.display = 'none';
|
||||
fetchCheckbox.checked = false;
|
||||
}
|
||||
});
|
||||
parentSelect.addEventListener('change', function () {
|
||||
showSaveButton();
|
||||
});
|
||||
|
||||
// Show programmes button for level types with fetch=true
|
||||
if (item.type === 'level' && item.fetch) {
|
||||
viewProgrammesBtn.style.display = 'inline-block';
|
||||
viewProgrammesBtn.addEventListener('click', function () {
|
||||
loadProgrammes(item.menuid, childrenContainer);
|
||||
});
|
||||
}
|
||||
|
||||
// Add children if any
|
||||
if (item.children && item.children.length > 0) {
|
||||
childrenContainer.style.display = 'block';
|
||||
item.children.forEach(child => {
|
||||
const childItem = createMenuItem(child);
|
||||
childrenContainer.appendChild(childItem);
|
||||
});
|
||||
}
|
||||
|
||||
return menuItem;
|
||||
}
|
||||
|
||||
function loadProgrammes(menuId, container) {
|
||||
container.innerHTML = `
|
||||
<div class="text-center">
|
||||
<div class="spinner-border spinner-border-sm text-primary" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
<small class="text-muted">Loading programmes...</small>
|
||||
</div>
|
||||
`;
|
||||
|
||||
fetch(`/admin/header/programmes/${menuId}`)
|
||||
.then(response => response.json())
|
||||
.then(programmes => {
|
||||
renderProgrammes(programmes, container);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error loading programmes:', error);
|
||||
container.innerHTML = `
|
||||
<div class="alert alert-danger alert-sm">
|
||||
<i class="fas fa-exclamation-triangle me-1"></i>
|
||||
Error loading programmes
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
}
|
||||
|
||||
function renderProgrammes(programmes, container) {
|
||||
if (programmes.length === 0) {
|
||||
container.innerHTML = '<small class="text-muted">No programmes found</small>';
|
||||
return;
|
||||
}
|
||||
|
||||
const programmesList = document.createElement('div');
|
||||
programmesList.className = 'list-group list-group-flush';
|
||||
|
||||
programmes.forEach(programme => {
|
||||
const item = document.createElement('div');
|
||||
item.className = 'list-group-item d-flex justify-content-between align-items-center py-2';
|
||||
item.innerHTML = `
|
||||
<div>
|
||||
<strong>${programme.name}</strong>
|
||||
<br>
|
||||
<small class="text-muted">${programme.code}</small>
|
||||
</div>
|
||||
<a href="${programme.url}" class="btn btn-outline-primary btn-sm" target="_blank">
|
||||
<i class="fas fa-external-link-alt"></i>
|
||||
</a>
|
||||
`;
|
||||
programmesList.appendChild(item);
|
||||
});
|
||||
|
||||
container.innerHTML = '';
|
||||
container.appendChild(programmesList);
|
||||
}
|
||||
|
||||
// Global variable to store all menu items for parent options
|
||||
let allMenuItems = [];
|
||||
|
||||
function populateParentOptions(select, currentMenuId) {
|
||||
// Clear existing options except the first one
|
||||
while (select.children.length > 1) {
|
||||
select.removeChild(select.lastChild);
|
||||
}
|
||||
|
||||
// Add options for all menu items except the current one
|
||||
allMenuItems.forEach(item => {
|
||||
if (item.menuid !== currentMenuId) {
|
||||
const option = document.createElement('option');
|
||||
option.value = item.menuid;
|
||||
option.textContent = item.title;
|
||||
select.appendChild(option);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function showSaveButton() {
|
||||
document.getElementById('saveMenuChanges').style.display = 'inline-block';
|
||||
}
|
||||
|
||||
function collectMenuUpdates() {
|
||||
const menuItems = document.querySelectorAll('.menu-item');
|
||||
const updates = [];
|
||||
|
||||
menuItems.forEach(item => {
|
||||
const menuId = item.dataset.menuid;
|
||||
const titleInput = item.querySelector('.menu-title-input');
|
||||
const orderInput = item.querySelector('.menu-order-input');
|
||||
const typeSelect = item.querySelector('.menu-type-select');
|
||||
const parentSelect = item.querySelector('.menu-parent-select');
|
||||
const fetchCheckbox = item.querySelector('.fetch-toggle input');
|
||||
const activeCheckbox = item.querySelector('.active-toggle input');
|
||||
|
||||
const updateData = {
|
||||
menuid: menuId,
|
||||
title: titleInput.value.trim(),
|
||||
order: parseInt(orderInput.value) || 0,
|
||||
type: typeSelect.value,
|
||||
parent: parentSelect.value || null
|
||||
};
|
||||
|
||||
// Add fetch status for level type menus
|
||||
if (fetchCheckbox) {
|
||||
updateData.fetch = fetchCheckbox.checked;
|
||||
}
|
||||
|
||||
// Add isActive status
|
||||
if (activeCheckbox) {
|
||||
updateData.isActive = activeCheckbox.checked;
|
||||
}
|
||||
|
||||
updates.push(updateData);
|
||||
});
|
||||
|
||||
return updates;
|
||||
}
|
||||
|
||||
function saveMenuChanges() {
|
||||
const updates = collectMenuUpdates();
|
||||
|
||||
// Show loading state
|
||||
const saveBtn = document.getElementById('saveMenuChanges');
|
||||
const originalText = saveBtn.innerHTML;
|
||||
saveBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Saving...';
|
||||
saveBtn.disabled = true;
|
||||
|
||||
// Send update request
|
||||
fetch('/admin/header/update-menu', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ updates })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(result => {
|
||||
if (result.success) {
|
||||
// Hide save button
|
||||
saveBtn.style.display = 'none';
|
||||
saveBtn.disabled = false;
|
||||
saveBtn.innerHTML = originalText;
|
||||
|
||||
// Show success message
|
||||
showAlert('Menu structure updated successfully!', 'success');
|
||||
|
||||
// Reload menu tree after a short delay
|
||||
setTimeout(() => {
|
||||
loadMenuTree();
|
||||
}, 1000);
|
||||
} else {
|
||||
throw new Error(result.error || 'Failed to update menu');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error saving menu changes:', error);
|
||||
showAlert('Error updating menu: ' + error.message, 'danger');
|
||||
|
||||
// Restore button
|
||||
saveBtn.disabled = false;
|
||||
saveBtn.innerHTML = originalText;
|
||||
});
|
||||
}
|
||||
|
||||
function showAlert(message, type) {
|
||||
const alertDiv = document.createElement('div');
|
||||
alertDiv.className = `alert alert-${type} alert-dismissible fade show`;
|
||||
alertDiv.innerHTML = `
|
||||
${message}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
`;
|
||||
|
||||
const container = document.getElementById('menuTreeContainer');
|
||||
container.parentElement.insertBefore(alertDiv, container);
|
||||
|
||||
// Auto remove after 5 seconds
|
||||
setTimeout(() => {
|
||||
if (alertDiv.parentElement) {
|
||||
alertDiv.remove();
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
});
|
||||
|
||||
function openImageUploader(targetInput, imageType) {
|
||||
// Tạo input file ẩn
|
||||
const fileInput = document.createElement('input');
|
||||
fileInput.type = 'file';
|
||||
fileInput.accept = 'image/*';
|
||||
fileInput.style.display = 'none';
|
||||
document.body.appendChild(fileInput);
|
||||
|
||||
// Xử lý khi chọn file
|
||||
fileInput.onchange = async function (e) {
|
||||
const file = e.target.files[0];
|
||||
if (!file) return;
|
||||
|
||||
try {
|
||||
// Tạo FormData
|
||||
const formData = new FormData();
|
||||
formData.append('image', file);
|
||||
|
||||
// Disable nút upload và hiển thị loading
|
||||
const uploadBtn = document.querySelector(`[data-target-input="${targetInput}"]`);
|
||||
const originalBtnHtml = uploadBtn.innerHTML;
|
||||
uploadBtn.disabled = true;
|
||||
uploadBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Uploading...';
|
||||
|
||||
// Gửi request upload
|
||||
const response = await fetch(`/admin/upload/image?imageType=${imageType}`, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Upload failed');
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Upload failed');
|
||||
}
|
||||
|
||||
// Cập nhật đường dẫn ảnh vào input
|
||||
const input = document.getElementById(targetInput) || document.querySelector(`[name="${targetInput}"]`);
|
||||
if (!input) {
|
||||
throw new Error('Target input not found');
|
||||
}
|
||||
|
||||
input.value = result.path;
|
||||
|
||||
// Tìm hoặc tạo preview container
|
||||
let imgPreview = input.parentElement.nextElementSibling;
|
||||
while (imgPreview && !imgPreview.classList.contains('mt-2')) {
|
||||
imgPreview = imgPreview.nextElementSibling;
|
||||
}
|
||||
|
||||
if (!imgPreview) {
|
||||
// Tạo mới phần tử preview nếu chưa có
|
||||
imgPreview = document.createElement('div');
|
||||
imgPreview.className = 'mt-2';
|
||||
const img = document.createElement('img');
|
||||
img.className = 'img-thumbnail';
|
||||
|
||||
// Set style dựa vào loại ảnh
|
||||
if (targetInput.toLowerCase().includes('logo') || targetInput.toLowerCase().includes('icon')) {
|
||||
img.style.maxHeight = '100px';
|
||||
img.style.maxWidth = '300px';
|
||||
img.style.objectFit = 'contain';
|
||||
} else {
|
||||
img.style.height = '200px';
|
||||
img.style.width = '100%';
|
||||
img.style.objectFit = 'cover';
|
||||
}
|
||||
|
||||
img.alt = 'Image preview';
|
||||
imgPreview.appendChild(img);
|
||||
input.parentElement.parentElement.appendChild(imgPreview);
|
||||
}
|
||||
|
||||
// Cập nhật ảnh preview
|
||||
const img = imgPreview.querySelector('img');
|
||||
if (img) {
|
||||
img.src = result.path;
|
||||
}
|
||||
|
||||
// Restore nút upload
|
||||
uploadBtn.disabled = false;
|
||||
uploadBtn.innerHTML = originalBtnHtml;
|
||||
|
||||
// Cleanup
|
||||
document.body.removeChild(fileInput);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Upload error:', error);
|
||||
alert('Upload failed: ' + error.message);
|
||||
|
||||
// Restore nút upload
|
||||
const uploadBtn = document.querySelector(`[data-target-input="${targetInput}"]`);
|
||||
uploadBtn.disabled = false;
|
||||
uploadBtn.innerHTML = originalBtnHtml;
|
||||
|
||||
// Cleanup
|
||||
if (document.body.contains(fileInput)) {
|
||||
document.body.removeChild(fileInput);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Trigger file selection
|
||||
fileInput.click();
|
||||
}
|
||||
</script>
|
||||
2740
views/admin/home/index.ejs
Normal file
2740
views/admin/home/index.ejs
Normal file
File diff suppressed because it is too large
Load Diff
638
views/admin/insurance/index.ejs
Normal file
638
views/admin/insurance/index.ejs
Normal file
@@ -0,0 +1,638 @@
|
||||
<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);">
|
||||
<%= title %>
|
||||
</h1>
|
||||
<p class="text-muted mb-0">
|
||||
Manage insurance page content with hero section, page information, and rich content editor
|
||||
</p>
|
||||
</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="insuranceForm" class="btn btn-primary" id="saveBtn">
|
||||
<i class="fas fa-save me-2"></i>Save Changes
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form id="insuranceForm" action="/admin/insurance/update" method="POST" class="needs-validation" novalidate>
|
||||
<!-- Hidden inputs -->
|
||||
<input type="hidden" name="hero" id="heroJson">
|
||||
<input type="hidden" name="page" id="pageJson">
|
||||
<input type="hidden" name="content" id="contentJson">
|
||||
|
||||
<div class="row">
|
||||
<!-- Main Content Column -->
|
||||
<div class="col-lg-8">
|
||||
<!-- Page Information Card -->
|
||||
<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 <span class="text-danger">*</span></label>
|
||||
<textarea class="form-control" id="pageTitle" rows="2" required><%= data.page?.title || 'Insurance & Travel Information' %></textarea>
|
||||
<div class="invalid-feedback">Please enter a page title</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Hero Section Card -->
|
||||
<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 || '/uploads/banner/b13.jpg' %>" readonly>
|
||||
<button type="button" class="btn btn-outline-primary btn-upload-image"
|
||||
data-target-input="heroBackgroundImage"
|
||||
data-image-type="insurance">
|
||||
<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) { %>
|
||||
<%
|
||||
const heroImgSrc = data.hero.backgroundImage.startsWith('http')
|
||||
? data.hero.backgroundImage
|
||||
: (frontendUrl ? frontendUrl + '/' + data.hero.backgroundImage : data.hero.backgroundImage);
|
||||
%>
|
||||
<img src="<%= heroImgSrc %>" class="img-thumbnail"
|
||||
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; flex-direction: column;">
|
||||
<i class="fas fa-image fa-3x mb-2"></i><p class="mb-0">Image preview not available</p>
|
||||
</div>
|
||||
<% } else { %>
|
||||
<div class="border rounded p-5 text-center text-muted"
|
||||
style="height: 200px; display: flex; align-items: center; justify-content: center; flex-direction: column;">
|
||||
<i class="fas fa-image fa-3x mb-2"></i><p class="mb-0">No image selected</p>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Hero Title</label>
|
||||
<input type="text" class="form-control" id="heroTitle"
|
||||
value="<%= data.hero?.title || 'Insurance & Travel Cancellation Guarantee' %>">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Hero Subtitle</label>
|
||||
<input type="text" class="form-control" id="heroSubtitle"
|
||||
value="<%= data.hero?.subtitle || 'Comprehensive coverage for your peace of mind' %>">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Content Editor Section -->
|
||||
<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 your insurance page content just like a blog post</p>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="editorjs" class="border rounded p-3" style="min-height: 500px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sidebar Column -->
|
||||
<div class="col-lg-4">
|
||||
<!-- SEO Settings Card -->
|
||||
<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 || '' %>">
|
||||
<small class="text-muted">Title for search engines (max 60 characters)</small>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Meta Description</label>
|
||||
<textarea class="form-control" id="metadataDescription" rows="3"
|
||||
placeholder="Meta description for SEO"><%= data.page?.metadata?.description || '' %></textarea>
|
||||
<small class="text-muted">Description for search engines (max 160 characters)</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Page Settings Card -->
|
||||
<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="pageDivider"
|
||||
<%= data.page?.divider !== false ? 'checked' : '' %>>
|
||||
<label class="form-check-label" for="pageDivider">Show page divider</label>
|
||||
</div>
|
||||
<small class="text-muted">Display divider line below page title</small>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" id="enableScrollspy"
|
||||
<%= data.hero?.enableScrollspy ? 'checked' : '' %>>
|
||||
<label class="form-check-label" for="enableScrollspy">Enable Scrollspy Navigation</label>
|
||||
</div>
|
||||
<small class="text-muted">Creates table of contents from headers</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Content Tips Card -->
|
||||
<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 Insurance Content:</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 coverage items</li>
|
||||
<li>Use <strong>Quote</strong> for important notes</li>
|
||||
<li>Use <strong>Embed</strong> for video explanations</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Preview Modal -->
|
||||
<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">Insurance Page 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: 80vh; border: none;"></iframe>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Hidden inputs for file upload -->
|
||||
<input type="file" id="directImageUpload" style="display: none;">
|
||||
<input type="hidden" id="currentImageType">
|
||||
<input type="hidden" id="currentTargetInput">
|
||||
|
||||
<!-- Editor.js Dependencies -->
|
||||
<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>
|
||||
// Insurance Content Manager
|
||||
class InsuranceContentManager {
|
||||
// Convert Editor.js data to Insurance content structure
|
||||
convertEditorToInsurance(editorData) {
|
||||
const contentItems = [];
|
||||
|
||||
editorData.blocks.forEach(block => {
|
||||
switch (block.type) {
|
||||
case 'header':
|
||||
contentItems.push({
|
||||
type: 'header',
|
||||
level: block.data.level || 2,
|
||||
text: block.data.text || ''
|
||||
});
|
||||
break;
|
||||
|
||||
case 'paragraph':
|
||||
contentItems.push({
|
||||
type: 'paragraph',
|
||||
text: block.data.text || ''
|
||||
});
|
||||
break;
|
||||
|
||||
case 'list':
|
||||
contentItems.push({
|
||||
type: 'list',
|
||||
level: block.data.style === 'ordered' ? 1 : 0, // 1 for ordered, 0 for unordered
|
||||
items: block.data.items || []
|
||||
});
|
||||
break;
|
||||
|
||||
case 'quote':
|
||||
contentItems.push({
|
||||
type: 'note',
|
||||
text: block.data.text || '',
|
||||
caption: block.data.caption || ''
|
||||
});
|
||||
break;
|
||||
|
||||
case 'embed':
|
||||
contentItems.push({
|
||||
type: 'embed',
|
||||
source: block.data.service || 'youtube',
|
||||
url: block.data.embed || '',
|
||||
embed: block.data.embed || '',
|
||||
caption: block.data.caption || '',
|
||||
width: block.data.width || 560,
|
||||
height: block.data.height || 315
|
||||
});
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
return contentItems;
|
||||
}
|
||||
|
||||
// Convert Insurance content to Editor.js data
|
||||
convertInsuranceToEditor(insuranceContent) {
|
||||
const blocks = [];
|
||||
|
||||
insuranceContent.forEach(item => {
|
||||
switch (item.type) {
|
||||
case 'header':
|
||||
blocks.push({
|
||||
type: 'header',
|
||||
data: {
|
||||
text: item.text || '',
|
||||
level: item.level || 2
|
||||
}
|
||||
});
|
||||
break;
|
||||
|
||||
case 'paragraph':
|
||||
blocks.push({
|
||||
type: 'paragraph',
|
||||
data: {
|
||||
text: item.text || ''
|
||||
}
|
||||
});
|
||||
break;
|
||||
|
||||
case 'section':
|
||||
if (item.title) {
|
||||
blocks.push({
|
||||
type: 'header',
|
||||
data: {
|
||||
text: item.title,
|
||||
level: 3
|
||||
}
|
||||
});
|
||||
}
|
||||
if (item.content) {
|
||||
blocks.push({
|
||||
type: 'paragraph',
|
||||
data: {
|
||||
text: item.content
|
||||
}
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
case 'list':
|
||||
blocks.push({
|
||||
type: 'list',
|
||||
data: {
|
||||
style: item.level === 1 ? 'ordered' : 'unordered', // Check level to determine style
|
||||
items: item.items || []
|
||||
}
|
||||
});
|
||||
break;
|
||||
|
||||
case 'note':
|
||||
blocks.push({
|
||||
type: 'quote',
|
||||
data: {
|
||||
text: item.text || '',
|
||||
caption: item.caption || ''
|
||||
}
|
||||
});
|
||||
break;
|
||||
|
||||
case 'embed':
|
||||
blocks.push({
|
||||
type: 'embed',
|
||||
data: {
|
||||
service: item.source || 'youtube',
|
||||
embed: item.url || item.embed || '',
|
||||
caption: item.caption || '',
|
||||
width: item.width || 560,
|
||||
height: item.height || 315
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
time: Date.now(),
|
||||
blocks: blocks,
|
||||
version: "2.28.2"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize Editor
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
// Initialize content manager
|
||||
window.insuranceContentManager = new InsuranceContentManager();
|
||||
|
||||
// Load existing content if available
|
||||
let initialEditorData = {
|
||||
time: Date.now(),
|
||||
blocks: [],
|
||||
version: "2.28.2"
|
||||
};
|
||||
|
||||
if (window.INSURANCE_DATA?.content?.content) {
|
||||
try {
|
||||
const insuranceContent = window.INSURANCE_DATA.content.content;
|
||||
initialEditorData = window.insuranceContentManager.convertInsuranceToEditor(insuranceContent);
|
||||
} catch (error) {
|
||||
console.error('Error loading insurance content:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize Editor via BlogEditor module (adds uploader + autosave)
|
||||
let blogEditorInstance = null;
|
||||
try {
|
||||
if (window.BlogEditor) {
|
||||
const initialCustom = window.INSURANCE_DATA?.content?.content ? { blocks: window.INSURANCE_DATA.content.content } : { blocks: [] };
|
||||
blogEditorInstance = new window.BlogEditor('editorjs', initialCustom);
|
||||
window.blogEditorInstance = blogEditorInstance;
|
||||
console.log('BlogEditor instance created for insurance');
|
||||
} else {
|
||||
console.warn('BlogEditor not available. Falling back to EditorJS init.');
|
||||
window.insuranceEditor = new EditorJS({
|
||||
holder: 'editorjs',
|
||||
data: initialEditorData,
|
||||
tools: {
|
||||
header: Header,
|
||||
paragraph: {
|
||||
class: Paragraph,
|
||||
inlineToolbar: true
|
||||
},
|
||||
list: List,
|
||||
quote: Quote,
|
||||
marker: Marker,
|
||||
embed: Embed
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error initializing editor module:', err);
|
||||
window.insuranceEditor = new EditorJS({ holder: 'editorjs', data: initialEditorData });
|
||||
}
|
||||
|
||||
// Form submission handler
|
||||
const form = document.getElementById('insuranceForm');
|
||||
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 {
|
||||
// Save editor content
|
||||
let editorData = null;
|
||||
if (window.blogEditorInstance?.editor) {
|
||||
editorData = await window.blogEditorInstance.editor.save();
|
||||
} else if (window.insuranceEditor) {
|
||||
editorData = await window.insuranceEditor.save();
|
||||
} else {
|
||||
throw new Error('Editor not initialized');
|
||||
}
|
||||
|
||||
// Convert to Insurance content structure
|
||||
const insuranceContent = window.insuranceContentManager.convertEditorToInsurance(editorData);
|
||||
|
||||
// Prepare JSON data
|
||||
const heroData = {
|
||||
title: document.getElementById('heroTitle').value,
|
||||
subtitle: document.getElementById('heroSubtitle').value,
|
||||
backgroundImage: document.getElementById('heroBackgroundImage').value,
|
||||
sectionClass: "uk-section-default uk-section-overlap uk-preserve-color uk-light uk-position-relative",
|
||||
backgroundClasses: "uk-background-norepeat uk-background-cover uk-background-top-center uk-section uk-section-xlarge",
|
||||
overlayStyle: { backgroundColor: "rgba(0, 0, 0, 0)" },
|
||||
titleClass: "text-white text-[5vw] uk-text-center",
|
||||
subtitleClass: "uk-panel font-[Raleway] italic text-[1.5vw] uk-margin uk-text-center",
|
||||
enableScrollspy: true
|
||||
};
|
||||
|
||||
const pageData = {
|
||||
title: document.getElementById('pageTitle').value,
|
||||
divider: document.getElementById('pageDivider').checked,
|
||||
sectionClass: "uk-section-default uk-section-overlap uk-section",
|
||||
titleClass: "text-[2.5vw] text-[#292c3d] uk-text-left@m uk-text-center",
|
||||
dividerClass: "uk-divider-small uk-text-left@m uk-text-center"
|
||||
};
|
||||
|
||||
const contentData = {
|
||||
sectionClass: "uk-section-muted uk-section-overlap uk-section",
|
||||
textClass: "uk-panel uk-margin text-[1vw]",
|
||||
content: insuranceContent
|
||||
};
|
||||
|
||||
// Set hidden inputs
|
||||
document.getElementById('heroJson').value = JSON.stringify(heroData);
|
||||
document.getElementById('pageJson').value = JSON.stringify(pageData);
|
||||
document.getElementById('contentJson').value = JSON.stringify(contentData);
|
||||
|
||||
// Submit form
|
||||
form.submit();
|
||||
} catch (error) {
|
||||
console.error('Error saving insurance content:', error);
|
||||
alert('Error saving content. Please check console for details.');
|
||||
saveBtn.disabled = false;
|
||||
saveBtn.innerHTML = '<i class="fas fa-save me-2"></i>Save Changes';
|
||||
}
|
||||
});
|
||||
|
||||
// Preview functionality
|
||||
const previewBtn = document.querySelector('.preview-btn');
|
||||
const previewModal = new bootstrap.Modal(document.getElementById('previewModal'));
|
||||
|
||||
previewBtn.addEventListener('click', async function () {
|
||||
try {
|
||||
// Save editor content first
|
||||
let editorData = null;
|
||||
if (window.blogEditorInstance?.editor) {
|
||||
editorData = await window.blogEditorInstance.editor.save();
|
||||
} else if (window.insuranceEditor) {
|
||||
editorData = await window.insuranceEditor.save();
|
||||
} else {
|
||||
throw new Error('Editor not initialized');
|
||||
}
|
||||
const insuranceContent = window.insuranceContentManager.convertEditorToInsurance(editorData);
|
||||
|
||||
// Prepare data for preview
|
||||
const heroData = {
|
||||
title: document.getElementById('heroTitle').value,
|
||||
subtitle: document.getElementById('heroSubtitle').value,
|
||||
backgroundImage: document.getElementById('heroBackgroundImage').value
|
||||
};
|
||||
|
||||
const pageData = {
|
||||
title: document.getElementById('pageTitle').value,
|
||||
divider: document.getElementById('pageDivider').checked
|
||||
};
|
||||
|
||||
const contentData = {
|
||||
content: insuranceContent
|
||||
};
|
||||
|
||||
const response = await fetch('/admin/insurance/preview', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
hero: JSON.stringify(heroData),
|
||||
page: JSON.stringify(pageData),
|
||||
content: JSON.stringify(contentData)
|
||||
})
|
||||
});
|
||||
|
||||
const html = await response.text();
|
||||
const previewFrame = document.getElementById('previewFrame');
|
||||
previewFrame.srcdoc = html;
|
||||
|
||||
previewModal.show();
|
||||
} catch (error) {
|
||||
console.error('Error generating preview:', error);
|
||||
alert('Error generating preview. Please try again.');
|
||||
}
|
||||
});
|
||||
|
||||
// Image upload handling
|
||||
document.querySelectorAll('.btn-upload-image').forEach(btn => {
|
||||
btn.addEventListener('click', function () {
|
||||
const targetInput = this.getAttribute('data-target-input');
|
||||
const imageType = this.getAttribute('data-image-type');
|
||||
document.getElementById('currentImageType').value = imageType;
|
||||
document.getElementById('currentTargetInput').value = targetInput;
|
||||
document.getElementById('directImageUpload').click();
|
||||
});
|
||||
});
|
||||
|
||||
// Handle file selection
|
||||
document.getElementById('directImageUpload').addEventListener('change', async function (e) {
|
||||
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 {
|
||||
showToast('Uploading image...', 'info');
|
||||
|
||||
const uploadResponse = await fetch(`/admin/upload/image?imageType=${imageType}`, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
const uploadResult = await uploadResponse.json();
|
||||
|
||||
if (uploadResult.success && uploadResult.path) {
|
||||
const inputElement = document.getElementById(targetInput);
|
||||
if (inputElement) {
|
||||
inputElement.value = uploadResult.path;
|
||||
|
||||
// Update preview
|
||||
if (targetInput === 'heroBackgroundImage') {
|
||||
updateHeroImagePreview(uploadResult.path);
|
||||
}
|
||||
|
||||
// Show success message
|
||||
showToast('Image uploaded successfully', 'success');
|
||||
}
|
||||
} else {
|
||||
showToast(uploadResult.error || 'Error uploading image', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Upload error:', error);
|
||||
showToast('Error uploading image', 'error');
|
||||
}
|
||||
|
||||
this.value = '';
|
||||
});
|
||||
|
||||
function updateHeroImagePreview(imageUrl) {
|
||||
const previewDiv = document.getElementById('heroImagePreview');
|
||||
let img = previewDiv.querySelector('img');
|
||||
let fallback = previewDiv.querySelector('.border');
|
||||
|
||||
if (!img) {
|
||||
img = document.createElement('img');
|
||||
img.className = 'img-thumbnail';
|
||||
img.style.height = '200px';
|
||||
img.style.width = '100%';
|
||||
img.style.objectFit = 'cover';
|
||||
img.alt = 'Background image preview';
|
||||
img.onerror = function() {
|
||||
this.style.display = 'none';
|
||||
if (fallback) fallback.style.display = 'flex';
|
||||
};
|
||||
previewDiv.insertBefore(img, fallback);
|
||||
}
|
||||
|
||||
img.src = imageUrl;
|
||||
img.style.display = 'block';
|
||||
if (fallback) fallback.style.display = 'none';
|
||||
}
|
||||
|
||||
function showToast(message, type = 'info') {
|
||||
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.setAttribute('role', 'alert');
|
||||
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);
|
||||
|
||||
const bsToast = new bootstrap.Toast(toast, { delay: 3000 });
|
||||
bsToast.show();
|
||||
|
||||
toast.addEventListener('hidden.bs.toast', () => {
|
||||
toast.remove();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Store insurance data in global variable
|
||||
window.INSURANCE_DATA = <%- JSON.stringify(data) %>;
|
||||
</script>
|
||||
134
views/admin/level/create.ejs
Normal file
134
views/admin/level/create.ejs
Normal file
@@ -0,0 +1,134 @@
|
||||
<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);">Create New Level Type</h1>
|
||||
<p class="text-muted mb-0">Add a new level type to the system</p>
|
||||
</div>
|
||||
<div>
|
||||
<a href="/admin/level" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-arrow-left me-1"></i>Back to Level Management
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-8 mx-auto">
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-body p-4">
|
||||
<form action="/admin/level/create" method="POST">
|
||||
<div class="mb-4">
|
||||
<label for="newType" class="form-label fw-medium">New Level Type Name</label>
|
||||
<input type="text" class="form-control" id="newType" name="newType" required
|
||||
placeholder="Enter new level type (e.g. foundation, diploma, etc.)">
|
||||
<div class="form-text">
|
||||
<i class="fas fa-info-circle me-1"></i>
|
||||
This will be used in URLs and as an identifier in the system.
|
||||
Use only lowercase letters, numbers, and hyphens.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-info">
|
||||
<i class="fas fa-lightbulb me-2"></i>
|
||||
<strong>Tips:</strong>
|
||||
<ul class="mb-0 mt-2">
|
||||
<li>Choose a descriptive, concise name for the level type</li>
|
||||
<li>Avoid using special characters or spaces</li>
|
||||
<li>Use hyphens instead of spaces if needed (e.g. "pre-master" instead of "pre master")</li>
|
||||
<li>The system will automatically convert your entry to lowercase and replace spaces with hyphens</li>
|
||||
<li>Special characters will be removed automatically</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-success">
|
||||
<i class="fas fa-check-circle me-2"></i>
|
||||
<strong>Sample Data Included:</strong>
|
||||
<p class="mb-1 mt-2">When you create a new level type, it will be pre-populated with sample data including:</p>
|
||||
<ul class="mb-0">
|
||||
<li>Banner information</li>
|
||||
<li>Overview with paragraphs</li>
|
||||
<li>Contact and social information</li>
|
||||
<li>Entry requirements</li>
|
||||
<li>Quick links and action buttons</li>
|
||||
<li>"Why Study" section with 4 sample items</li>
|
||||
</ul>
|
||||
<p class="mt-2 mb-0">You can easily edit all this content after creation.</p>
|
||||
</div>
|
||||
|
||||
<% if (existingTypes && existingTypes.length > 0) { %>
|
||||
<div class="mb-4">
|
||||
<label class="form-label fw-medium">Existing Level Types</label>
|
||||
<div class="list-group">
|
||||
<% existingTypes.forEach(type => { %>
|
||||
<div class="list-group-item list-group-item-action d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<strong><%= type.charAt(0).toUpperCase() + type.slice(1) %></strong>
|
||||
</div>
|
||||
<a href="/admin/level?type=<%= type %>" class="btn btn-sm btn-outline-primary">
|
||||
<i class="fas fa-edit me-1"></i>Edit
|
||||
</a>
|
||||
</div>
|
||||
<% }); %>
|
||||
</div>
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
<div class="d-grid gap-2 mt-4">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fas fa-plus-circle me-1"></i>Create Level Type
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal tùy chỉnh -->
|
||||
<div id="customModal" class="custom-modal">
|
||||
<div class="custom-modal-content">
|
||||
<div class="custom-modal-header">
|
||||
<h5 class="custom-modal-title">Notification</h5>
|
||||
<button type="button" class="custom-modal-close">×</button>
|
||||
</div>
|
||||
<div class="custom-modal-body">
|
||||
<p id="modalMessage">Content of the notification will appear here.</p>
|
||||
</div>
|
||||
<div class="custom-modal-footer">
|
||||
<button type="button" class="btn btn-primary custom-modal-ok">OK</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Import custom modal CSS -->
|
||||
<link rel="stylesheet" href="/css/custom-modal.css">
|
||||
|
||||
<script>
|
||||
// Xử lý form
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Khởi tạo modal tùy chỉnh
|
||||
CustomModal.init('customModal');
|
||||
|
||||
const form = document.querySelector('form');
|
||||
|
||||
// Xử lý form submit
|
||||
form.addEventListener('submit', function(e) {
|
||||
const typeInput = document.getElementById('newType');
|
||||
const typeValue = typeInput.value.trim();
|
||||
|
||||
// Kiểm tra giá trị nhập vào
|
||||
if (!typeValue) {
|
||||
e.preventDefault();
|
||||
CustomModal.alert('Please enter a name for the level type.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Kiểm tra định dạng
|
||||
if (/[^a-zA-Z0-9\s\-]/.test(typeValue)) {
|
||||
e.preventDefault();
|
||||
CustomModal.alert('The level type name should only contain letters, numbers, hyphens, and spaces.');
|
||||
return;
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
743
views/admin/level/index.ejs
Normal file
743
views/admin/level/index.ejs
Normal file
@@ -0,0 +1,743 @@
|
||||
<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);">
|
||||
<% const formattedType = currentType.split('-').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' '); %>
|
||||
<%= formattedType %> Level
|
||||
</h1>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-outline-secondary dropdown-toggle" type="button" id="levelTypeDropdown" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
<%= formattedType %>
|
||||
</button>
|
||||
<ul class="dropdown-menu" aria-labelledby="levelTypeDropdown">
|
||||
<% levelTypes.forEach(type => { %>
|
||||
<li>
|
||||
<a class="dropdown-item <%= type === currentType ? 'active' : '' %>" href="/admin/level?type=<%= type %>">
|
||||
<%= type.split('-').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' ') %>
|
||||
</a>
|
||||
</li>
|
||||
<% }); %>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Thêm nút tạo mới type -->
|
||||
<a href="/admin/level/create" class="btn btn-primary">
|
||||
<i class="fas fa-plus me-1"></i>Add New Level Type
|
||||
</a>
|
||||
|
||||
<!-- Nút xóa type hiện tại (chỉ hiển thị với non-default types) -->
|
||||
<% if (!['undergraduate', 'postgraduate', 'pre-university'].includes(currentType)) { %>
|
||||
<button type="button" class="btn btn-outline-danger" data-custom-modal="open" data-type="<%= currentType %>">
|
||||
<i class="fas fa-trash me-1"></i>Delete This Level Type
|
||||
</button>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<form id="levelForm" action="/admin/level/update?type=<%= currentType %>" method="POST" class="content-with-fixed-buttons">
|
||||
<input type="hidden" name="type" value="<%= currentType %>">
|
||||
<input type="hidden" name="banner" id="bannerJson">
|
||||
<input type="hidden" name="overview" id="overviewJson">
|
||||
<input type="hidden" name="requirements" id="requirementsJson">
|
||||
<input type="hidden" name="action_buttons" id="actionbuttonsJson">
|
||||
<input type="hidden" name="why_study" id="whystudyJson">
|
||||
|
||||
<!-- Level Type and Brochure Field -->
|
||||
<div class="card shadow-sm border-0 mb-4">
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Level Type</label>
|
||||
<input type="text" class="form-control" value="<%= currentType.split('-').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' ') %>" readonly>
|
||||
<div class="form-text text-muted">This is the level type identifier. It cannot be changed.</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Brochure Link</label>
|
||||
<input type="text" class="form-control" id="levelBrochure" name="brochure" value="<%= data.brochure || '#' %>" placeholder="Enter brochure URL or # for no link">
|
||||
<div class="form-text text-muted">Link to the level brochure. Use # if no brochure is available.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 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="#banner" role="tab">
|
||||
<i class="fas fa-image me-2"></i>Banner
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-bs-toggle="tab" href="#overview" role="tab">
|
||||
<i class="fas fa-info-circle me-2"></i>Overview
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-bs-toggle="tab" href="#requirements" role="tab">
|
||||
<i class="fas fa-list-check me-2"></i>Requirements
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-bs-toggle="tab" href="#actions" role="tab">
|
||||
<i class="fas fa-link me-2"></i>Action Buttons
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-bs-toggle="tab" href="#why" role="tab">
|
||||
<i class="fas fa-question-circle me-2"></i>Why Study
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<div class="tab-content">
|
||||
<!-- Banner Tab -->
|
||||
<div class="tab-pane fade show active" id="banner" role="tabpanel">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-medium">Banner Image</label>
|
||||
<div class="row">
|
||||
<div class="col-md-6 d-flex align-items-baseline">
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" id="bannerImage" value="<%= data.banner.image %>">
|
||||
<button type="button" class="btn btn-outline-primary btn-upload-image" data-target-input="bannerImage">
|
||||
<i class="fas fa-upload me-1"></i>Upload
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<!-- Ảnh preview cho banner -->
|
||||
<div class="image-preview mt-2">
|
||||
<% if (data.banner.image) { %>
|
||||
<img src="<%= data.banner.image %>" alt="Banner preview" class="img-thumbnail" style="height:200px;width:100%;object-fit:cover;">
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-medium">Title</label>
|
||||
<input type="text" class="form-control" id="bannerTitle" value="<%= data.banner.title %>">
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-medium">Text</label>
|
||||
<textarea class="form-control" id="bannerText" rows="3"><%= data.banner.text %></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Overview Tab -->
|
||||
<div class="tab-pane fade" id="overview" role="tabpanel">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-medium">Title</label>
|
||||
<input type="text" class="form-control" id="overviewTitle" value="<%= data.overview.title %>">
|
||||
</div>
|
||||
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-medium">Paragraphs</label>
|
||||
<div id="overviewParagraphs">
|
||||
<% (data.overview.paragraphs || []).forEach((paragraph, index) => { %>
|
||||
<div class="mb-3 d-flex gap-2">
|
||||
<textarea class="form-control" rows="3"><%= paragraph %></textarea>
|
||||
<button type="button" class="btn btn-outline-danger" onclick="removeParagraph(this)">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
<% }); %>
|
||||
</div>
|
||||
<button type="button" class="btn btn-outline-primary mt-2" onclick="addParagraph('overviewParagraphs')">
|
||||
<i class="fas fa-plus me-1"></i>Add Paragraph
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Contact Info -->
|
||||
<div class="col-md-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">Contact Information</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Title</label>
|
||||
<input type="text" class="form-control" id="contactTitle" value="<%= data.overview.contact_info.title %>">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Subtitle</label>
|
||||
<input type="text" class="form-control" id="contactSubtitle" value="<%= data.overview.contact_info.subtitle %>">
|
||||
</div>
|
||||
<div id="contactItems">
|
||||
<% (data.overview.contact_info.items || []).forEach((item, index) => { %>
|
||||
<div class="mb-3">
|
||||
<input type="text" class="form-control" value="<%= item.text %>">
|
||||
</div>
|
||||
<% }); %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Social Info -->
|
||||
<div class="col-md-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">Social Information</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Title</label>
|
||||
<input type="text" class="form-control" id="socialTitle" value="<%= data.overview.social_info.title %>">
|
||||
</div>
|
||||
<div id="socialLinks">
|
||||
<% (data.overview.social_info.social_links || []).forEach((link, index) => { %>
|
||||
<div class="mb-3 border p-3">
|
||||
<div class="row g-2">
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Image</label>
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" value="<%= link.image %>">
|
||||
<button type="button" class="btn btn-outline-primary btn-upload-image">
|
||||
<i class="fas fa-upload"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">URL</label>
|
||||
<input type="text" class="form-control" value="<%= link.url %>">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Alt Text</label>
|
||||
<input type="text" class="form-control" value="<%= link.alt %>">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% }); %>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<h6>Apply Button</h6>
|
||||
<div class="row g-2">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Text</label>
|
||||
<input type="text" class="form-control" id="applyButtonText" value="<%= data.overview.social_info.apply_button.text %>">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">URL</label>
|
||||
<input type="text" class="form-control" id="applyButtonUrl" value="<%= data.overview.social_info.apply_button.url %>">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Requirements Tab -->
|
||||
<div class="tab-pane fade" id="requirements" role="tabpanel">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-medium">Title</label>
|
||||
<input type="text" class="form-control" id="requirementsTitle" value="<%= data.requirements.title %>">
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-medium">Requirements List</label>
|
||||
<div id="requirementItems">
|
||||
<% (data.requirements.items || []).forEach((item, index) => { %>
|
||||
<div class="mb-3 d-flex gap-2">
|
||||
<input type="text" class="form-control" value="<%= item %>">
|
||||
<button type="button" class="btn btn-outline-danger" onclick="removeItem(this)">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
<% }); %>
|
||||
</div>
|
||||
<button type="button" class="btn btn-outline-primary mt-2" onclick="addRequirementItem()">
|
||||
<i class="fas fa-plus me-1"></i>Add Requirement
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons Tab -->
|
||||
<div class="tab-pane fade" id="actions" role="tabpanel">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-medium">Title</label>
|
||||
<input type="text" class="form-control" id="actionButtonsTitle" value="<%= data.action_buttons.title %>">
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-medium">Buttons</label>
|
||||
<div id="actionButtonsList">
|
||||
<% (data.action_buttons.buttons || []).forEach((button, index) => { %>
|
||||
<div class="mb-3 border p-3">
|
||||
<div class="row g-2">
|
||||
<div class="col-md-5">
|
||||
<label class="form-label">Text</label>
|
||||
<input type="text" class="form-control" value="<%= button.text %>">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Link</label>
|
||||
<input type="text" class="form-control" value="<%= button.link %>">
|
||||
</div>
|
||||
<div class="col-md-1 d-flex align-items-end">
|
||||
<button type="button" class="btn btn-outline-danger" onclick="removeActionButton(this)">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% }); %>
|
||||
</div>
|
||||
<button type="button" class="btn btn-outline-primary mt-2" onclick="addActionButton()">
|
||||
<i class="fas fa-plus me-1"></i>Add Button
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Why Study Tab -->
|
||||
<div class="tab-pane fade" id="why" role="tabpanel">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-medium">Title</label>
|
||||
<input type="text" class="form-control" id="whyStudyTitle" value="<%= data.why_study.title %>">
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-medium">Items</label>
|
||||
<div id="whyStudyItems">
|
||||
<% (data.why_study.items || []).forEach((item, index) => { %>
|
||||
<div class="mb-3 border p-3">
|
||||
<div class="row g-2">
|
||||
<div class="col-md-2">
|
||||
<label class="form-label">Number</label>
|
||||
<input type="text" class="form-control" value="<%= item.number %>">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Title</label>
|
||||
<input type="text" class="form-control" value="<%= item.title %>">
|
||||
</div>
|
||||
<div class="col-md-5">
|
||||
<label class="form-label">Text</label>
|
||||
<textarea class="form-control" rows="2"><%= item.text %></textarea>
|
||||
</div>
|
||||
<div class="col-md-1 d-flex align-items-end">
|
||||
<button type="button" class="btn btn-outline-danger" onclick="removeWhyStudyItem(this)">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% }); %>
|
||||
</div>
|
||||
<button type="button" class="btn btn-outline-primary mt-2" onclick="addWhyStudyItem()">
|
||||
<i class="fas fa-plus me-1"></i>Add Item
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Fixed bottom buttons -->
|
||||
<div class="fixed-bottom-buttons">
|
||||
<button type="reset" class="btn btn-secondary">
|
||||
<i class="fas fa-undo me-1"></i>Reset
|
||||
</button>
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fas fa-save me-1"></i>Save Changes
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Thêm input file ẩn để sử dụng cho upload trực tiếp -->
|
||||
<input type="file" id="directImageUpload" style="display: none;">
|
||||
<input type="hidden" id="currentImageType" name="imageType">
|
||||
<input type="hidden" id="currentTargetInput" name="targetInput">
|
||||
|
||||
<script>
|
||||
// Helper function to create JSON from form data with validation
|
||||
function createJsonFromForm() {
|
||||
try {
|
||||
// Banner
|
||||
const bannerJson = {
|
||||
image: document.getElementById('bannerImage')?.value || '',
|
||||
title: document.getElementById('bannerTitle')?.value || '',
|
||||
text: document.getElementById('bannerText')?.value || ''
|
||||
};
|
||||
document.getElementById('bannerJson').value = JSON.stringify(bannerJson);
|
||||
|
||||
// Overview
|
||||
const overviewJson = {
|
||||
title: document.getElementById('overviewTitle')?.value || '',
|
||||
paragraphs: Array.from(document.querySelectorAll('#overviewParagraphs textarea')).map(ta => ta.value || ''),
|
||||
contact_info: {
|
||||
title: document.getElementById('contactTitle')?.value || '',
|
||||
subtitle: document.getElementById('contactSubtitle')?.value || '',
|
||||
items: Array.from(document.querySelectorAll('#contactItems input')).map(input => ({
|
||||
text: input.value || ''
|
||||
}))
|
||||
},
|
||||
social_info: {
|
||||
title: document.getElementById('socialTitle')?.value || '',
|
||||
social_links: Array.from(document.querySelectorAll('#socialLinks .mb-3')).map(div => ({
|
||||
image: div.querySelectorAll('input')[0]?.value || '',
|
||||
url: div.querySelectorAll('input')[1]?.value || '',
|
||||
alt: div.querySelectorAll('input')[2]?.value || ''
|
||||
})),
|
||||
apply_button: {
|
||||
text: document.getElementById('applyButtonText')?.value || '',
|
||||
url: document.getElementById('applyButtonUrl')?.value || ''
|
||||
}
|
||||
}
|
||||
};
|
||||
document.getElementById('overviewJson').value = JSON.stringify(overviewJson);
|
||||
|
||||
// Requirements
|
||||
const requirementsJson = {
|
||||
title: document.getElementById('requirementsTitle')?.value || '',
|
||||
items: Array.from(document.querySelectorAll('#requirementItems input')).map(input => input.value || '')
|
||||
};
|
||||
document.getElementById('requirementsJson').value = JSON.stringify(requirementsJson);
|
||||
|
||||
// Action Buttons
|
||||
const actionButtonsJson = {
|
||||
title: document.getElementById('actionButtonsTitle')?.value || '',
|
||||
buttons: Array.from(document.querySelectorAll('#actionButtonsList .mb-3')).map(div => ({
|
||||
text: div.querySelectorAll('input')[0]?.value || '',
|
||||
link: div.querySelectorAll('input')[1]?.value || ''
|
||||
}))
|
||||
};
|
||||
document.getElementById('actionbuttonsJson').value = JSON.stringify(actionButtonsJson);
|
||||
|
||||
// Why Study
|
||||
const whyStudyJson = {
|
||||
title: document.getElementById('whyStudyTitle')?.value || '',
|
||||
items: Array.from(document.querySelectorAll('#whyStudyItems .mb-3')).map(div => ({
|
||||
number: div.querySelectorAll('input')[0]?.value || '',
|
||||
title: div.querySelectorAll('input')[1]?.value || '',
|
||||
text: div.querySelector('textarea')?.value || ''
|
||||
}))
|
||||
};
|
||||
document.getElementById('whystudyJson').value = JSON.stringify(whyStudyJson);
|
||||
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Error creating JSON:', error);
|
||||
if (window.toastManager) {
|
||||
window.toastManager.error('Error preparing form data: ' + error.message);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Xử lý upload hình ảnh
|
||||
document.querySelectorAll('.btn-upload-image').forEach(btn => {
|
||||
btn.addEventListener('click', function() {
|
||||
const targetInput = this.getAttribute('data-target-input');
|
||||
const imageType = this.getAttribute('data-image-type') || 'level';
|
||||
|
||||
// Lưu thông tin vào hidden inputs
|
||||
document.getElementById('currentImageType').value = imageType;
|
||||
document.getElementById('currentTargetInput').value = targetInput;
|
||||
|
||||
// Kích hoạt input file ẩn
|
||||
document.getElementById('directImageUpload').click();
|
||||
});
|
||||
});
|
||||
|
||||
// Xử lý khi chọn file
|
||||
document.getElementById('directImageUpload').addEventListener('change', async function(e) {
|
||||
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 response = await fetch(`/admin/upload/image?imageType=${imageType}`, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success && result.path) {
|
||||
const inputElement = document.getElementById(targetInput);
|
||||
if (inputElement) {
|
||||
inputElement.value = result.path;
|
||||
|
||||
// Cập nhật hoặc tạo mới hình ảnh preview
|
||||
let previewContainer = inputElement.closest('.input-group').parentElement.querySelector('.image-preview');
|
||||
|
||||
// Tìm container image-preview trong cùng hàng nhưng ở cột bên cạnh
|
||||
if (!previewContainer) {
|
||||
previewContainer = inputElement.closest('.row').querySelector('.col-md-6:nth-child(2) .image-preview');
|
||||
}
|
||||
|
||||
// Nếu chưa có container preview thì tạo mới
|
||||
if (!previewContainer) {
|
||||
previewContainer = document.createElement('div');
|
||||
previewContainer.className = 'image-preview mt-2';
|
||||
const rightColumn = inputElement.closest('.row').querySelector('.col-md-6:nth-child(2)');
|
||||
if (rightColumn) {
|
||||
rightColumn.appendChild(previewContainer);
|
||||
} else {
|
||||
// Fallback: tạo tại cột hiện tại nếu không tìm thấy cột bên phải
|
||||
previewContainer = document.createElement('div');
|
||||
previewContainer.className = 'image-preview mt-2';
|
||||
inputElement.closest('.input-group').parentElement.appendChild(previewContainer);
|
||||
}
|
||||
}
|
||||
|
||||
// Tìm hoặc tạo mới thẻ img để preview ảnh
|
||||
let img = previewContainer.querySelector('img');
|
||||
if (!img) {
|
||||
img = document.createElement('img');
|
||||
img.className = 'img-thumbnail';
|
||||
previewContainer.appendChild(img);
|
||||
}
|
||||
|
||||
// Thiết lập style cho ảnh preview dựa vào loại ảnh
|
||||
if (targetInput.toLowerCase().includes('logo')) {
|
||||
img.style.height = '100px';
|
||||
img.style.objectFit = 'contain';
|
||||
img.style.width = '';
|
||||
} else {
|
||||
img.style.height = '200px';
|
||||
img.style.width = '100%';
|
||||
img.style.objectFit = 'cover';
|
||||
}
|
||||
|
||||
img.src = result.path;
|
||||
img.alt = 'Image preview';
|
||||
|
||||
if (window.toastManager) {
|
||||
window.toastManager.success('Tải ảnh thành công');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (window.toastManager) {
|
||||
window.toastManager.error(result.error || 'Error uploading image');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Upload error:', error);
|
||||
if (window.toastManager) {
|
||||
window.toastManager.error('Error uploading image');
|
||||
}
|
||||
}
|
||||
|
||||
// Reset input để có thể chọn lại cùng một file nếu cần
|
||||
this.value = '';
|
||||
});
|
||||
});
|
||||
|
||||
// Form submit handler with validation
|
||||
document.getElementById('levelForm').addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
try {
|
||||
// Create and validate JSON data
|
||||
if (!createJsonFromForm()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate required fields
|
||||
const requiredFields = ['bannerJson', 'overviewJson', 'requirementsJson'];
|
||||
const missingFields = requiredFields.filter(field => {
|
||||
const value = document.getElementById(field).value;
|
||||
return !value || value === '{}';
|
||||
});
|
||||
|
||||
if (missingFields.length > 0) {
|
||||
if (window.toastManager) {
|
||||
window.toastManager.error(`Missing required data: ${missingFields.join(', ')}`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Show loading toast
|
||||
if (window.toastManager) {
|
||||
window.toastManager.info('Saving changes...');
|
||||
}
|
||||
|
||||
this.submit();
|
||||
} catch (error) {
|
||||
console.error('Form submission error:', error);
|
||||
if (window.toastManager) {
|
||||
window.toastManager.error('Error submitting form: ' + error.message);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Helper functions for adding/removing items
|
||||
function addParagraph(containerId) {
|
||||
const container = document.getElementById(containerId);
|
||||
const div = document.createElement('div');
|
||||
div.className = 'mb-3 d-flex gap-2';
|
||||
div.innerHTML = `
|
||||
<textarea class="form-control" rows="3"></textarea>
|
||||
<button type="button" class="btn btn-outline-danger" onclick="removeParagraph(this)">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
`;
|
||||
container.appendChild(div);
|
||||
}
|
||||
|
||||
function removeParagraph(button) {
|
||||
button.closest('.mb-3').remove();
|
||||
}
|
||||
|
||||
function addRequirementItem() {
|
||||
const container = document.getElementById('requirementItems');
|
||||
const div = document.createElement('div');
|
||||
div.className = 'mb-3 d-flex gap-2';
|
||||
div.innerHTML = `
|
||||
<input type="text" class="form-control">
|
||||
<button type="button" class="btn btn-outline-danger" onclick="removeItem(this)">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
`;
|
||||
container.appendChild(div);
|
||||
}
|
||||
|
||||
function removeItem(button) {
|
||||
button.closest('.mb-3').remove();
|
||||
}
|
||||
|
||||
function addActionButton() {
|
||||
const container = document.getElementById('actionButtonsList');
|
||||
const div = document.createElement('div');
|
||||
div.className = 'mb-3 border p-3';
|
||||
div.innerHTML = `
|
||||
<div class="row g-2">
|
||||
<div class="col-md-5">
|
||||
<label class="form-label">Text</label>
|
||||
<input type="text" class="form-control">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Link</label>
|
||||
<input type="text" class="form-control">
|
||||
</div>
|
||||
<div class="col-md-1 d-flex align-items-end">
|
||||
<button type="button" class="btn btn-outline-danger" onclick="removeActionButton(this)">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
container.appendChild(div);
|
||||
}
|
||||
|
||||
function removeActionButton(button) {
|
||||
button.closest('.mb-3').remove();
|
||||
}
|
||||
|
||||
function addWhyStudyItem() {
|
||||
const container = document.getElementById('whyStudyItems');
|
||||
const div = document.createElement('div');
|
||||
div.className = 'mb-3 border p-3';
|
||||
div.innerHTML = `
|
||||
<div class="row g-2">
|
||||
<div class="col-md-2">
|
||||
<label class="form-label">Number</label>
|
||||
<input type="text" class="form-control">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Title</label>
|
||||
<input type="text" class="form-control">
|
||||
</div>
|
||||
<div class="col-md-5">
|
||||
<label class="form-label">Text</label>
|
||||
<textarea class="form-control" rows="2"></textarea>
|
||||
</div>
|
||||
<div class="col-md-1 d-flex align-items-end">
|
||||
<button type="button" class="btn btn-outline-danger" onclick="removeWhyStudyItem(this)">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
container.appendChild(div);
|
||||
}
|
||||
|
||||
function removeWhyStudyItem(button) {
|
||||
button.closest('.mb-3').remove();
|
||||
}
|
||||
|
||||
// (Removed) Helper functions for form/apply/application as Level now references Form only
|
||||
|
||||
// Tận dụng module ImageUploader đã được tải trong layout
|
||||
</script>
|
||||
|
||||
<!-- Modal tùy chỉnh xác nhận xóa level type -->
|
||||
<div id="customModal" class="custom-modal">
|
||||
<div class="custom-modal-content">
|
||||
<div class="custom-modal-header">
|
||||
<h5 class="custom-modal-title">Delete Confirmation</h5>
|
||||
<button type="button" class="custom-modal-close">×</button>
|
||||
</div>
|
||||
<div class="custom-modal-body">
|
||||
<p id="modalMessage">Are you sure you want to delete this level type?</p>
|
||||
</div>
|
||||
<div class="custom-modal-footer">
|
||||
<button type="button" class="btn btn-secondary custom-modal-cancel">Cancel</button>
|
||||
<button type="button" class="btn btn-danger custom-modal-ok">Delete Permanently</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Import custom modal CSS -->
|
||||
<link rel="stylesheet" href="/css/custom-modal.css">
|
||||
|
||||
<script>
|
||||
// Custom Modal Functions
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Khởi tạo modal tùy chỉnh
|
||||
CustomModal.init('customModal', {
|
||||
closeOnOutsideClick: true,
|
||||
animationDuration: 300
|
||||
});
|
||||
|
||||
// Mở modal
|
||||
document.addEventListener('click', function(e) {
|
||||
if (e.target.getAttribute('data-custom-modal') === 'open' ||
|
||||
e.target.parentElement.getAttribute('data-custom-modal') === 'open') {
|
||||
|
||||
// Lấy button hoặc icon parent nếu click vào icon
|
||||
const button = e.target.getAttribute('data-custom-modal') === 'open' ?
|
||||
e.target : e.target.parentElement;
|
||||
|
||||
const type = button.getAttribute('data-type') || '<%= currentType %>';
|
||||
|
||||
// Sử dụng CustomModal.confirm thay vì xử lý trực tiếp
|
||||
CustomModal.confirm(
|
||||
`Are you sure you want to delete level type "${type}"? This action cannot be undone. All related data will be permanently deleted.`,
|
||||
function() {
|
||||
// Hành động khi xác nhận
|
||||
window.location.href = `/admin/level/delete/${type}`;
|
||||
},
|
||||
null, // Không cần xử lý khi hủy
|
||||
'Delete Confirmation'
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
1102
views/admin/safety/index.ejs
Normal file
1102
views/admin/safety/index.ejs
Normal file
File diff suppressed because it is too large
Load Diff
1210
views/admin/terms/index.ejs
Normal file
1210
views/admin/terms/index.ejs
Normal file
File diff suppressed because it is too large
Load Diff
353
views/admin/travel/index.ejs
Normal file
353
views/admin/travel/index.ejs
Normal file
@@ -0,0 +1,353 @@
|
||||
<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>
|
||||
222
views/auth/login.ejs
Normal file
222
views/auth/login.ejs
Normal file
@@ -0,0 +1,222 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title><%= title %> | CMS-SIMS</title>
|
||||
<!-- Bootstrap CSS -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<!-- Font Awesome -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
<!-- Custom CSS -->
|
||||
<style>
|
||||
:root {
|
||||
--primary-color: #b8b76a;
|
||||
--primary-light: #c9c88a;
|
||||
--primary-dark: #9a994a;
|
||||
--text-on-primary: #ffffff;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
background-color: #3e1e00;
|
||||
}
|
||||
|
||||
body::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: url('/images/bg-login.jpg') center/cover no-repeat fixed;
|
||||
opacity: 0.4;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
padding: 20px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.login-container {
|
||||
background-color: #f6f6f6;
|
||||
padding: 40px;
|
||||
border-radius: 15px;
|
||||
box-shadow: 0 4px 15px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
.logo-container {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
.logo-container img {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
background-color: var(--primary-color);
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.img-shine {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.img-shine::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.2), transparent);
|
||||
transition: all 0.5s ease;
|
||||
}
|
||||
|
||||
.img-shine:hover::after {
|
||||
left: 100%;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary-color) !important;
|
||||
box-shadow: 0 0 0 2px rgba(255,106,0,0.12);
|
||||
}
|
||||
|
||||
.form-label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.btn-shine {
|
||||
background: linear-gradient(90deg, #9a994a, #b8b76a, #c9c88a, #d9d8aa);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 12px 30px;
|
||||
border-radius: 8px;
|
||||
font-weight: 600;
|
||||
font-size: 15px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
text-transform: uppercase;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
.btn-shine:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 5px 15px rgba(255,106,0,0.28);
|
||||
}
|
||||
|
||||
.btn-shine::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.2), transparent);
|
||||
transition: all 0.5s ease;
|
||||
}
|
||||
|
||||
.btn-shine:hover::after {
|
||||
left: 100%;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.main-content {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.login-container {
|
||||
padding: 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="main-content">
|
||||
<!-- Flash Messages Data (Hidden) -->
|
||||
<% if(typeof success_msg !== 'undefined' || typeof error_msg !== 'undefined' || typeof error !== 'undefined') { %>
|
||||
<div id="flash-messages-data" style="display: none;"><%- JSON.stringify({
|
||||
success_msg: typeof success_msg !== 'undefined' && success_msg ? success_msg : null,
|
||||
error_msg: typeof error_msg !== 'undefined' && error_msg ? error_msg : null,
|
||||
error: typeof error !== 'undefined' && error ? error : null
|
||||
}) %></div>
|
||||
<% } %>
|
||||
|
||||
<div class="login-container">
|
||||
<div class="logo-container img-shine">
|
||||
<img src="/images/Logo_round.jpg" alt="Logo">
|
||||
</div>
|
||||
|
||||
<div style="text-align: center; margin-bottom: 30px;">
|
||||
<h4 style="color: #b8b76a; font-weight: 600; margin-bottom: 10px;">CMS Management System</h4>
|
||||
<p style="color: #666; font-size: 14px;">Welcome to Content Management System</p>
|
||||
</div>
|
||||
|
||||
<form action="/auth/login" method="POST" class="login-form">
|
||||
<div class="form-group" style="margin-bottom: 20px;">
|
||||
<label for="username" class="form-label">Username</label>
|
||||
<input type="text" class="form-control" id="username" name="username" required autocomplete="username" autofocus>
|
||||
</div>
|
||||
|
||||
<div class="form-group" style="margin-bottom: 20px;">
|
||||
<label for="password" class="form-label">Password</label>
|
||||
<input type="password" class="form-control" id="password" name="password" required autocomplete="current-password">
|
||||
|
||||
<a href="/auth/forgot-password" style="display: block; text-align: right; margin-top: 8px; color: #b8b76a; text-decoration: none; font-size: 13px;">
|
||||
Forgot Your Password?
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div style="text-align: center; margin-top: 30px;">
|
||||
<button type="submit" class="btn-shine">
|
||||
Login
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 20px; text-align: center;">
|
||||
<p style="font-size: 12px; color: #fff;">© 2024 Swiss Institute of Management and Sciences. All rights reserved.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bootstrap JS -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
<!-- Toast JS -->
|
||||
<script src="/js/toast.js"></script>
|
||||
|
||||
<!-- Flash Handler JS -->
|
||||
<script src="/js/flash-handler.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
191
views/index.ejs
Normal file
191
views/index.ejs
Normal file
@@ -0,0 +1,191 @@
|
||||
<!-- Hero Section -->
|
||||
<section class="container">
|
||||
<div class="hero"
|
||||
style="background: linear-gradient(135deg, var(--primary-color), var(--primary-dark)); color: white; border-radius: 20px; box-shadow: 0 15px 30px rgba(0,0,0,0.1); margin-top: 2rem; overflow: hidden; position: relative;">
|
||||
<div class="position-absolute top-0 end-0 p-3 d-none d-lg-block">
|
||||
<div class="d-flex gap-2">
|
||||
<div class="rounded-circle bg-white bg-opacity-10" style="width: 12px; height: 12px;"></div>
|
||||
<div class="rounded-circle bg-white bg-opacity-10" style="width: 12px; height: 12px;"></div>
|
||||
<div class="rounded-circle bg-white bg-opacity-10" style="width: 12px; height: 12px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row align-items-center">
|
||||
<div class="col-lg-6">
|
||||
<h1 class="fw-bold mb-4 text-white">API Management</h1>
|
||||
<p class="lead mb-4 text-white-50">Simple dashboard to control your APIs</p>
|
||||
<div class="d-flex gap-3">
|
||||
<% if (locals.user) { %>
|
||||
<a href="/admin/dashboard" class="btn btn-lg"
|
||||
style="background-color: white; color: var(--primary-color); font-weight: 600;">
|
||||
<i class="fas fa-tachometer-alt me-2"></i>Go to Dashboard
|
||||
</a>
|
||||
<% } else { %>
|
||||
<a href="/auth/login" class="btn btn-lg"
|
||||
style="background-color: white; color: var(--primary-color); font-weight: 600;">
|
||||
<i class="fas fa-sign-in-alt me-2"></i>Login
|
||||
</a>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-6 d-none d-lg-block text-center">
|
||||
<div style="position: relative; z-index: 1;">
|
||||
<div class="position-absolute"
|
||||
style="top: -20px; right: -20px; width: 100px; height: 100px; background: rgba(255,255,255,0.1); border-radius: 50%; z-index: -1;">
|
||||
</div>
|
||||
<div class="position-absolute"
|
||||
style="bottom: -30px; left: -20px; width: 150px; height: 150px; background: rgba(255,255,255,0.1); border-radius: 50%; z-index: -1;">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<% if (locals.user) { %>
|
||||
<!-- Features Section -->
|
||||
<section class="container py-5">
|
||||
<div class="card border-0 shadow-sm" style="border-radius: 20px; overflow: hidden;">
|
||||
<div class="card-body p-4 p-lg-5">
|
||||
<div class="text-center mb-5">
|
||||
<span class="badge rounded-pill px-3 py-2 mb-2"
|
||||
style="background-color: rgba(184, 183, 106, 0.1); color: var(--primary-color);">Features</span>
|
||||
<h2 class="fw-bold" style="color: var(--primary-color);">Key Features</h2>
|
||||
</div>
|
||||
|
||||
<div class="row g-4">
|
||||
<div class="col-md-4">
|
||||
<div class="card h-100 border-0 shadow-sm" style="border-radius: 15px; transition: transform 0.3s;">
|
||||
<div class="card-body p-4">
|
||||
<div class="rounded-circle bg-light d-inline-flex align-items-center justify-content-center mb-4"
|
||||
style="width: 60px; height: 60px;">
|
||||
<i class="fas fa-lock fa-lg" style="color: var(--primary-color);"></i>
|
||||
</div>
|
||||
<h4 class="card-title fw-bold" style="color: var(--primary-color);">High Security</h4>
|
||||
<p class="card-text text-muted">Secure user authentication system with password encryption.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<div class="card h-100 border-0 shadow-sm" style="border-radius: 15px; transition: transform 0.3s;">
|
||||
<div class="card-body p-4">
|
||||
<div class="rounded-circle bg-light d-inline-flex align-items-center justify-content-center mb-4"
|
||||
style="width: 60px; height: 60px;">
|
||||
<i class="fas fa-chart-line fa-lg" style="color: var(--primary-color);"></i>
|
||||
</div>
|
||||
<h4 class="card-title fw-bold" style="color: var(--primary-color);">Analytics Dashboard</h4>
|
||||
<p class="card-text text-muted">Monitor and analyze data through visual charts.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<div class="card h-100 border-0 shadow-sm" style="border-radius: 15px; transition: transform 0.3s;">
|
||||
<div class="card-body p-4">
|
||||
<div class="rounded-circle bg-light d-inline-flex align-items-center justify-content-center mb-4"
|
||||
style="width: 60px; height: 60px;">
|
||||
<i class="fas fa-cogs fa-lg" style="color: var(--primary-color);"></i>
|
||||
</div>
|
||||
<h4 class="card-title fw-bold" style="color: var(--primary-color);">API Integration</h4>
|
||||
<p class="card-text text-muted">Provides API endpoints for easy data integration into other
|
||||
applications.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- CTA Section -->
|
||||
<section class="container py-5">
|
||||
<div class="card border-0 shadow-sm"
|
||||
style="border-radius: 20px; overflow: hidden; background: linear-gradient(135deg, var(--primary-color), var(--primary-dark)); color: white;">
|
||||
<div class="card-body p-4 p-lg-5">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-lg-8 mx-auto text-center">
|
||||
<h2 class="fw-bold mb-4 text-white">Start Using CMS-GGCamp Today</h2>
|
||||
<p class="lead mb-4 text-white-50">Experience simple and effective API management system</p>
|
||||
<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;">
|
||||
<i class="fas fa-tachometer-alt me-2"></i>Go to Dashboard
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
<% } else { %>
|
||||
<!-- Info Section for non-logged in users -->
|
||||
<section class="container py-5">
|
||||
<div class="row g-4">
|
||||
<div class="col-md-6">
|
||||
<div class="card border-0 shadow-sm h-100" style="border-radius: 20px; overflow: hidden;">
|
||||
<div class="card-body p-4">
|
||||
<div class="d-flex align-items-center mb-4">
|
||||
<div class="rounded-circle d-flex align-items-center justify-content-center me-3"
|
||||
style="width: 50px; height: 50px; background-color: rgba(184, 183, 106, 0.1);">
|
||||
<i class="fas fa-shield-alt fa-lg" style="color: var(--primary-color);"></i>
|
||||
</div>
|
||||
<h3 class="fw-bold mb-0" style="color: var(--primary-color);">Secure Access</h3>
|
||||
</div>
|
||||
<p class="text-muted mb-4">Our CMS provides secure access to manage your content with advanced
|
||||
authentication and authorization features.</p>
|
||||
<ul class="list-unstyled mb-0">
|
||||
<li class="d-flex align-items-center mb-3">
|
||||
<i class="fas fa-check-circle me-2" style="color: var(--primary-color);"></i>
|
||||
<span class="text-muted">Role-based access control</span>
|
||||
</li>
|
||||
<li class="d-flex align-items-center mb-3">
|
||||
<i class="fas fa-check-circle me-2" style="color: var(--primary-color);"></i>
|
||||
<span class="text-muted">Encrypted data transmission</span>
|
||||
</li>
|
||||
<li class="d-flex align-items-center">
|
||||
<i class="fas fa-check-circle me-2" style="color: var(--primary-color);"></i>
|
||||
<span class="text-muted">Secure session management</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<div class="card border-0 shadow-sm h-100" style="border-radius: 20px; overflow: hidden;">
|
||||
<div class="card-body p-4">
|
||||
<div class="d-flex align-items-center mb-4">
|
||||
<div class="rounded-circle d-flex align-items-center justify-content-center me-3"
|
||||
style="width: 50px; height: 50px; background-color: rgba(184, 183, 106, 0.1);">
|
||||
<i class="fas fa-code fa-lg" style="color: var(--primary-color);"></i>
|
||||
</div>
|
||||
<h3 class="fw-bold mb-0" style="color: var(--primary-color);">API Integration</h3>
|
||||
</div>
|
||||
<p class="text-muted mb-4">Access our comprehensive API documentation and integrate your applications
|
||||
seamlessly.</p>
|
||||
<ul class="list-unstyled mb-4">
|
||||
<li class="d-flex align-items-center mb-3">
|
||||
<i class="fas fa-check-circle me-2" style="color: var(--primary-color);"></i>
|
||||
<span class="text-muted">RESTful API endpoints</span>
|
||||
</li>
|
||||
<li class="d-flex align-items-center mb-3">
|
||||
<i class="fas fa-check-circle me-2" style="color: var(--primary-color);"></i>
|
||||
<span class="text-muted">JSON data format</span>
|
||||
</li>
|
||||
<li class="d-flex align-items-center">
|
||||
<i class="fas fa-check-circle me-2" style="color: var(--primary-color);"></i>
|
||||
<span class="text-muted">Detailed documentation</span>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="text-center mt-auto">
|
||||
<a href="/auth/login" class="btn btn-primary px-4 py-2" style="border-radius: 10px;">
|
||||
<i class="fas fa-sign-in-alt me-2"></i>Login to Access
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<% } %>
|
||||
122
views/layouts/admin.ejs
Normal file
122
views/layouts/admin.ejs
Normal file
@@ -0,0 +1,122 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<link rel="icon" type="image/png" href="/uploads/layout/favicon.png" />
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>
|
||||
<%= typeof title !=='undefined' ? title + ' | ' : '' %>CMS-GGCamp
|
||||
</title>
|
||||
<!-- Bootstrap CSS -->
|
||||
<link
|
||||
href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<!-- Font Awesome -->
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"
|
||||
/>
|
||||
<style>
|
||||
:root {
|
||||
--primary-color: #b8b76a;
|
||||
--primary-light: #c9c88a;
|
||||
--primary-dark: #9a994a;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: var(--primary-color);
|
||||
border-color: var(--primary-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background-color: var(--primary-dark);
|
||||
border-color: var(--primary-dark);
|
||||
}
|
||||
|
||||
.nav-link:hover {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.nav-link.active {
|
||||
color: var(--primary-color);
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
<%- style %>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<nav class="col-md-2 d-none d-md-block bg-light sidebar">
|
||||
<div class="position-sticky pt-3">
|
||||
<ul class="nav flex-column">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/admin/dashboard">Dashboard</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/admin/home">Home</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/admin/header">Header & Menu</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/admin/footer">Footer</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/admin/about">About</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/admin/about-us">About Us</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/admin/contact">Contact</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/admin/faq">FAQ</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/admin/terms-conditions">Terms & Conditions</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/admin/safety">Safety</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/admin/camp-location">Camp Location</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/admin/form">Form</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/admin/upload">Upload</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main class="col-md-9 ms-sm-auto col-lg-10 px-md-4">
|
||||
<!-- Flash Messages Data (Hidden) -->
|
||||
<% if(typeof success_msg !=='undefined' || typeof error_msg
|
||||
!=='undefined' || typeof error !=='undefined' ) { %>
|
||||
<div id="flash-messages-data" style="display: none">
|
||||
<%- JSON.stringify({ success_msg: typeof success_msg !=='undefined'
|
||||
&& success_msg ? success_msg : null, error_msg: typeof error_msg
|
||||
!=='undefined' && error_msg ? error_msg : null, error: typeof error
|
||||
!=='undefined' && error ? error : null }) %>
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
<div class="py-3"><%- body %></div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bootstrap JS -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<%- script %>
|
||||
</body>
|
||||
</html>
|
||||
1025
views/layouts/main.ejs
Normal file
1025
views/layouts/main.ejs
Normal file
File diff suppressed because it is too large
Load Diff
83
views/page/404.ejs
Normal file
83
views/page/404.ejs
Normal file
@@ -0,0 +1,83 @@
|
||||
<div class="error-page">
|
||||
<div class="error-container">
|
||||
<h1 class="error-title">404</h1>
|
||||
<h2 class="error-subtitle">Oops! Page Not Found</h2>
|
||||
<p class="error-text">Sorry, the page you are looking for does not exist or has been moved.</p>
|
||||
<div class="error-actions">
|
||||
<a href="/" class="btn btn-primary">Back to Home</a>
|
||||
<a href="/contact" class="btn btn-outline-primary">Contact Support</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.error-page {
|
||||
min-height: calc(100vh - 200px); /* Trừ đi header và footer */
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.error-container {
|
||||
text-align: center;
|
||||
max-width: 600px;
|
||||
padding: 40px 20px;
|
||||
}
|
||||
|
||||
.error-title {
|
||||
font-size: 120px;
|
||||
color: linear-gradient(90deg, #067f3f, #056633, #044e27);
|
||||
background: -webkit-linear-gradient(90deg, #067f3f, #056633, #044e27);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
margin: 0;
|
||||
line-height: 1;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.error-subtitle {
|
||||
font-size: 28px;
|
||||
color: #333;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.error-text {
|
||||
font-size: 18px;
|
||||
color: #666;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.error-actions {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.btn-outline-primary:hover {
|
||||
border-color: white;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.error-title {
|
||||
font-size: 80px;
|
||||
}
|
||||
|
||||
.error-subtitle {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.error-text {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.error-actions {
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.error-actions .btn {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
14
views/page/error.ejs
Normal file
14
views/page/error.ejs
Normal file
@@ -0,0 +1,14 @@
|
||||
<div class="container py-5">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-8 text-center">
|
||||
<div class="mb-4">
|
||||
<i class="fas fa-exclamation-circle fa-5x" style="color: var(--primary-color);"></i>
|
||||
</div>
|
||||
<h1 class="display-4 mb-4">Đã xảy ra lỗi</h1>
|
||||
<p class="lead mb-4"><%= message %></p>
|
||||
<a href="/" class="btn btn-primary">
|
||||
<i class="fas fa-home me-2"></i>Quay lại trang chủ
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
Reference in New Issue
Block a user