first commit

This commit is contained in:
r2xrzh9q2z-lab
2026-02-02 11:07:09 +07:00
commit d1b931d547
286 changed files with 53992 additions and 0 deletions

1024
views/admin/about/index.ejs Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

555
views/admin/dashboard.ejs Normal file
View 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>

View 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">&times;</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>

View 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">&times;</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>

View 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">&times;</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

File diff suppressed because it is too large Load Diff

View 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">&nbsp;</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">&nbsp;</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">&nbsp;</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">&nbsp;</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
View 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>

View 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">&nbsp;</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">&nbsp;</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

File diff suppressed because it is too large Load Diff

View 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>

View 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">&times;</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
View 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">&times;</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

File diff suppressed because it is too large Load Diff

1210
views/admin/terms/index.ejs Normal file

File diff suppressed because it is too large Load Diff

View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

83
views/page/404.ejs Normal file
View 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
View 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>