forked from UKSOURCE/cms.hailearning.edu.vn
feat: add service management module with CRUD operations
This commit is contained in:
@@ -4,16 +4,25 @@
|
||||
<!-- 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>
|
||||
<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
|
||||
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>
|
||||
@@ -29,9 +38,18 @@
|
||||
<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
|
||||
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>
|
||||
@@ -47,9 +65,18 @@
|
||||
<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
|
||||
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>
|
||||
@@ -62,14 +89,21 @@
|
||||
</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
|
||||
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>
|
||||
@@ -85,9 +119,18 @@
|
||||
<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
|
||||
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>
|
||||
@@ -103,9 +146,18 @@
|
||||
<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
|
||||
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>
|
||||
@@ -121,16 +173,28 @@
|
||||
<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
|
||||
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">
|
||||
<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>
|
||||
@@ -139,9 +203,18 @@
|
||||
<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
|
||||
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>
|
||||
@@ -157,9 +230,18 @@
|
||||
<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
|
||||
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>
|
||||
@@ -175,16 +257,28 @@
|
||||
<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
|
||||
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">
|
||||
<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>
|
||||
@@ -193,9 +287,18 @@
|
||||
<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
|
||||
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>
|
||||
@@ -207,7 +310,32 @@
|
||||
</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">Services</h5>
|
||||
<p class="text-muted mb-0 small">Manage services</p>
|
||||
</div>
|
||||
</div>
|
||||
<a href="/admin/service" 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>
|
||||
@@ -234,18 +362,37 @@
|
||||
<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
|
||||
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>
|
||||
<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">
|
||||
<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>
|
||||
@@ -253,18 +400,37 @@
|
||||
<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
|
||||
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>
|
||||
<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">
|
||||
<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>
|
||||
@@ -272,18 +438,37 @@
|
||||
<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
|
||||
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>
|
||||
<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">
|
||||
<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>
|
||||
@@ -291,18 +476,37 @@
|
||||
<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
|
||||
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>
|
||||
<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">
|
||||
<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>
|
||||
@@ -310,18 +514,37 @@
|
||||
<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
|
||||
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>
|
||||
<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">
|
||||
<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>
|
||||
@@ -329,18 +552,37 @@
|
||||
<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
|
||||
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>
|
||||
<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">
|
||||
<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>
|
||||
@@ -348,18 +590,37 @@
|
||||
<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
|
||||
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>
|
||||
<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">
|
||||
<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>
|
||||
@@ -367,18 +628,37 @@
|
||||
<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
|
||||
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>
|
||||
<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">
|
||||
<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>
|
||||
@@ -386,38 +666,76 @@
|
||||
<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
|
||||
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>
|
||||
<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">
|
||||
<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
|
||||
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>
|
||||
<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">
|
||||
<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>
|
||||
@@ -425,18 +743,37 @@
|
||||
<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
|
||||
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>
|
||||
<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">
|
||||
<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>
|
||||
@@ -444,18 +781,75 @@
|
||||
<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
|
||||
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>Service API</span>
|
||||
</div>
|
||||
</td>
|
||||
<td><code>/api/service</code></td>
|
||||
<td>
|
||||
<span
|
||||
class="badge"
|
||||
style="background-color: var(--primary-color)"
|
||||
>GET</span
|
||||
>
|
||||
</td>
|
||||
<td>API to get service data</td>
|
||||
<td>
|
||||
<a
|
||||
href="/api/service"
|
||||
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>
|
||||
<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">
|
||||
<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>
|
||||
@@ -468,46 +862,83 @@
|
||||
|
||||
<!-- System Info -->
|
||||
<div class="card">
|
||||
<div class="card-header" style="background: linear-gradient(135deg, #b8b76a, #9a994a); color: white;">
|
||||
<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="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
|
||||
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 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
|
||||
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 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="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>
|
||||
<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>
|
||||
<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>
|
||||
@@ -520,34 +951,34 @@
|
||||
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;
|
||||
|
||||
723
views/admin/service/details.ejs
Normal file
723
views/admin/service/details.ejs
Normal file
@@ -0,0 +1,723 @@
|
||||
<div class="container">
|
||||
<div class="d-flex justify-content-between align-items-center mt-4 mb-4">
|
||||
<div>
|
||||
<h1 class="h3 mb-0" style="color: var(--primary-dark);">Service Details: <%= service.name %></h1>
|
||||
<p class="text-muted mb-0">Edit detailed content for <%= service.name %> service</p>
|
||||
</div>
|
||||
<div>
|
||||
<a href="/admin/service" class="btn btn-outline-primary">
|
||||
<i class="fas fa-arrow-left me-2"></i>Back to Services
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<form action="/admin/service/<%= service.slug %>/details/update" method="POST" class="content-with-fixed-buttons" id="serviceDetailsForm">
|
||||
<!-- Hidden inputs for JSON data -->
|
||||
<input type="hidden" name="details" id="detailsJson">
|
||||
<input type="hidden" name="features" id="featuresJson">
|
||||
<input type="hidden" name="faq" id="faqJson">
|
||||
|
||||
<!-- Navigation Tabs -->
|
||||
<div class="card shadow-sm border-0 mb-4">
|
||||
<div class="card-header bg-white border-bottom">
|
||||
<ul class="nav nav-tabs card-header-tabs" role="tablist">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" data-bs-toggle="tab" href="#basic-info" role="tab">
|
||||
<i class="fas fa-info-circle me-2"></i>Basic Information
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-bs-toggle="tab" href="#key-features" role="tab">
|
||||
<i class="fas fa-star me-2"></i>Key Features
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-bs-toggle="tab" href="#faq-section" role="tab">
|
||||
<i class="fas fa-question-circle me-2"></i>FAQ Section
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<div class="tab-content">
|
||||
<!-- Basic Information Tab -->
|
||||
<div class="tab-pane fade show active" id="basic-info" role="tabpanel">
|
||||
<div class="card border shadow-sm">
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-5">
|
||||
<label class="form-label fw-medium">Main Image</label>
|
||||
<div class="input-group mb-2">
|
||||
<input type="text" class="form-control" id="mainImage" name="mainImage"
|
||||
value="<%= service.details?.mainImage || '' %>">
|
||||
<button type="button" class="btn btn-outline-primary btn-upload-image"
|
||||
data-target-input="mainImage" data-image-type="service">
|
||||
<i class="fas fa-upload me-1"></i>Upload
|
||||
</button>
|
||||
</div>
|
||||
<small class="form-text text-muted">Recommended size: 800x600px</small>
|
||||
</div>
|
||||
<div class="col-md-7">
|
||||
<div id="mainImagePreview">
|
||||
<% if (service.details?.mainImage) { %>
|
||||
<img src="<%= getFullImageUrl(service.details.mainImage) %>" class="img-thumbnail" style="max-height: 250px; width: auto; max-width: 100%; object-fit: contain;" alt="Main image preview">
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-3">
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-medium">Title</label>
|
||||
<input type="text" class="form-control" id="detailsTitle" name="title"
|
||||
value="<%= service.details?.title || service.name %>">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-3">
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-medium">Description</label>
|
||||
<textarea class="form-control" id="detailsDescription" name="description" rows="3"><%= service.details?.description || service.description %></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Overview Section -->
|
||||
<div class="row mt-4">
|
||||
<div class="col-12">
|
||||
<div class="card border">
|
||||
<div class="card-header bg-light">
|
||||
<h6 class="mb-0">Overview Section</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-12">
|
||||
<label class="form-label">Overview Title</label>
|
||||
<input type="text" class="form-control" id="overviewTitle" name="overviewTitle"
|
||||
value="<%= service.details?.overviewTitle || 'Service Overview' %>">
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label">Overview Description</label>
|
||||
<textarea class="form-control" id="overviewDescription" name="overviewDescription" rows="4"><%= service.details?.overviewDescription || '' %></textarea>
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label">Additional Description</label>
|
||||
<textarea class="form-control" id="additionalDescription" name="additionalDescription" rows="3"><%= service.details?.additionalDescription || '' %></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Key Features Tab -->
|
||||
<div class="tab-pane fade" id="key-features" role="tabpanel">
|
||||
<div class="card border shadow-sm">
|
||||
<div class="card-body">
|
||||
<!-- Features Header -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Features Title</label>
|
||||
<input type="text" class="form-control" id="keyFeaturesTitle" name="keyFeaturesTitle"
|
||||
value="<%= service.details?.keyFeaturesTitle || 'Key Features' %>">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Features Image</label>
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" id="keyFeaturesImage" name="keyFeaturesImage"
|
||||
value="<%= service.details?.keyFeaturesImage || '' %>">
|
||||
<button type="button" class="btn btn-outline-primary btn-upload-image"
|
||||
data-target-input="keyFeaturesImage" data-image-type="service">
|
||||
<i class="fas fa-upload me-1"></i>Upload
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Key Features Image Preview -->
|
||||
<div class="row mt-3">
|
||||
<div class="col-12">
|
||||
<div id="keyFeaturesImagePreview">
|
||||
<% if (service.details?.keyFeaturesImage) { %>
|
||||
<img src="<%= getFullImageUrl(service.details.keyFeaturesImage) %>" class="img-thumbnail" style="max-height: 180px; width: auto; max-width: 100%; object-fit: contain;" alt="Key Features image preview">
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Features List -->
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h6 class="fw-medium mb-0">Features</h6>
|
||||
<button type="button" class="btn btn-primary btn-sm" onclick="addFeature()">
|
||||
<i class="fas fa-plus"></i> Add Feature
|
||||
</button>
|
||||
</div>
|
||||
<div id="featuresContainer">
|
||||
<% if (service.details?.features && service.details.features.length > 0) { %>
|
||||
<% service.details.features.forEach((feature, index) => { %>
|
||||
<div class="card mb-3 feature-item">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h6 class="mb-0 text-decoration-underline">Feature <%= index + 1 %></h6>
|
||||
<button type="button" class="btn btn-danger btn-sm" onclick="removeFeature(this)">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-8">
|
||||
<label class="form-label">Title</label>
|
||||
<input type="text" class="form-control feature-title"
|
||||
value="<%= feature.title || '' %>" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row g-3 mt-2">
|
||||
<div class="col-md-12">
|
||||
<label class="form-label">Description</label>
|
||||
<textarea class="form-control feature-description" rows="2" required><%= feature.description || '' %></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% }) %>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- FAQ Section Tab -->
|
||||
<div class="tab-pane fade" id="faq-section" role="tabpanel">
|
||||
<div class="card border shadow-sm">
|
||||
<div class="card-body">
|
||||
<!-- FAQ Header -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">FAQ Title</label>
|
||||
<input type="text" class="form-control" id="faqTitle" name="faqTitle"
|
||||
value="<%= service.details?.faqTitle || 'Frequently Asked Questions' %>">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">FAQ Image</label>
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" id="faqImage" name="faqImage"
|
||||
value="<%= service.details?.faqImage || '' %>">
|
||||
<button type="button" class="btn btn-outline-primary btn-upload-image"
|
||||
data-target-input="faqImage" data-image-type="service">
|
||||
<i class="fas fa-upload me-1"></i>Upload
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- FAQ Image Preview -->
|
||||
<div class="row mt-3">
|
||||
<div class="col-12">
|
||||
<div id="faqImagePreview">
|
||||
<% if (service.details?.faqImage) { %>
|
||||
<img src="<%= getFullImageUrl(service.details.faqImage) %>" class="img-thumbnail" style="max-height: 180px; width: auto; max-width: 100%; object-fit: contain;" alt="FAQ image preview">
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- FAQ List -->
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h6 class="fw-medium mb-0">FAQ Items</h6>
|
||||
<button type="button" class="btn btn-primary btn-sm" onclick="addFAQ()">
|
||||
<i class="fas fa-plus"></i> Add FAQ
|
||||
</button>
|
||||
</div>
|
||||
<div id="faqContainer">
|
||||
<% if (service.details?.faq && service.details.faq.length > 0) { %>
|
||||
<% service.details.faq.forEach((faq, index) => { %>
|
||||
<div class="card mb-3 faq-item">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h6 class="mb-0 text-decoration-underline">FAQ <%= index + 1 %></h6>
|
||||
<button type="button" class="btn btn-danger btn-sm" onclick="removeFAQ(this)">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">ID</label>
|
||||
<input type="text" class="form-control faq-id"
|
||||
value="<%= faq.id || 'faq-' + (index + 1) %>" required>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Expanded by Default</label>
|
||||
<select class="form-control faq-expanded">
|
||||
<option value="false" <%= !faq.isExpanded ? 'selected' : '' %>>No</option>
|
||||
<option value="true" <%= faq.isExpanded ? 'selected' : '' %>>Yes</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row g-3 mt-2">
|
||||
<div class="col-md-12">
|
||||
<label class="form-label">Question</label>
|
||||
<input type="text" class="form-control faq-question"
|
||||
value="<%= faq.question || '' %>" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row g-3 mt-2">
|
||||
<div class="col-md-12">
|
||||
<label class="form-label">Answer</label>
|
||||
<textarea class="form-control faq-answer" rows="3" required><%= faq.answer || '' %></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% }) %>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bottom Buttons -->
|
||||
<div class="bottom-buttons">
|
||||
<button type="reset" class="btn btn-secondary">
|
||||
<i class="fas fa-undo me-2"></i>Reset
|
||||
</button>
|
||||
<button type="submit" class="btn btn-primary" id="submitBtn">
|
||||
<i class="fas fa-save me-2"></i>Save Changes
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Scripts -->
|
||||
<script>
|
||||
let originalFormData = null;
|
||||
let featureIndex = <%= service.details?.features?.length || 0 %>;
|
||||
let faqIndex = <%= service.details?.faq?.length || 0 %>;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Initialize form data
|
||||
originalFormData = <%- JSON.stringify(service) %>;
|
||||
|
||||
// Set initial JSON values
|
||||
updateAllJsonInputs(originalFormData);
|
||||
|
||||
// Initialize form handlers
|
||||
initializeFormHandlers();
|
||||
});
|
||||
|
||||
function initializeFormHandlers() {
|
||||
// Form submission
|
||||
const form = document.getElementById('serviceDetailsForm');
|
||||
form.addEventListener('submit', async function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const submitBtn = document.getElementById('submitBtn');
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i>Saving...';
|
||||
|
||||
try {
|
||||
updateJsonData();
|
||||
this.submit();
|
||||
} catch (error) {
|
||||
console.error('Error updating data:', error);
|
||||
showError('Failed to process form data. Please try again.');
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.innerHTML = '<i class="fas fa-save me-2"></i>Save Changes';
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize image upload buttons
|
||||
document.querySelectorAll('.btn-upload-image').forEach(button => {
|
||||
button.addEventListener('click', function() {
|
||||
const targetInput = this.dataset.targetInput;
|
||||
const imageType = this.dataset.imageType;
|
||||
openImageUploader(targetInput, imageType);
|
||||
});
|
||||
});
|
||||
|
||||
// Initialize image input change listeners for manual URL input
|
||||
const imageInputs = ['mainImage', 'keyFeaturesImage', 'faqImage'];
|
||||
imageInputs.forEach(inputId => {
|
||||
const input = document.getElementById(inputId);
|
||||
if (input) {
|
||||
input.addEventListener('input', function() {
|
||||
updateImagePreviewAfterUpload(inputId, this.value);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function addFeature() {
|
||||
const container = document.getElementById('featuresContainer');
|
||||
const featureHtml = `
|
||||
<div class="card mb-3 feature-item">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h6 class="mb-0 text-decoration-underline">Feature ${featureIndex + 1}</h6>
|
||||
<button type="button" class="btn btn-danger btn-sm" onclick="removeFeature(this)">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-8">
|
||||
<label class="form-label">Title</label>
|
||||
<input type="text" class="form-control feature-title" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row g-3 mt-2">
|
||||
<div class="col-md-12">
|
||||
<label class="form-label">Description</label>
|
||||
<textarea class="form-control feature-description" rows="2" required></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
container.insertAdjacentHTML('beforeend', featureHtml);
|
||||
featureIndex++;
|
||||
}
|
||||
|
||||
function removeFeature(button) {
|
||||
const featureItem = button.closest('.feature-item');
|
||||
if (featureItem) {
|
||||
featureItem.remove();
|
||||
}
|
||||
}
|
||||
|
||||
function addFAQ() {
|
||||
const container = document.getElementById('faqContainer');
|
||||
const faqHtml = `
|
||||
<div class="card mb-3 faq-item">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h6 class="mb-0 text-decoration-underline">FAQ ${faqIndex + 1}</h6>
|
||||
<button type="button" class="btn btn-danger btn-sm" onclick="removeFAQ(this)">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">ID</label>
|
||||
<input type="text" class="form-control faq-id"
|
||||
value="faq-${faqIndex + 1}" required>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Expanded by Default</label>
|
||||
<select class="form-control faq-expanded">
|
||||
<option value="false">No</option>
|
||||
<option value="true">Yes</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row g-3 mt-2">
|
||||
<div class="col-md-12">
|
||||
<label class="form-label">Question</label>
|
||||
<input type="text" class="form-control faq-question" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row g-3 mt-2">
|
||||
<div class="col-md-12">
|
||||
<label class="form-label">Answer</label>
|
||||
<textarea class="form-control faq-answer" rows="3" required></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
container.insertAdjacentHTML('beforeend', faqHtml);
|
||||
faqIndex++;
|
||||
}
|
||||
|
||||
function removeFAQ(button) {
|
||||
const faqItem = button.closest('.faq-item');
|
||||
if (faqItem) {
|
||||
faqItem.remove();
|
||||
}
|
||||
}
|
||||
|
||||
function updateAllJsonInputs(data) {
|
||||
// Collect basic details data
|
||||
const details = {
|
||||
title: data.details?.title || data.name,
|
||||
description: data.details?.description || data.description,
|
||||
mainImage: data.details?.mainImage || '',
|
||||
overviewTitle: data.details?.overviewTitle || 'Service Overview',
|
||||
overviewDescription: data.details?.overviewDescription || '',
|
||||
additionalDescription: data.details?.additionalDescription || '',
|
||||
keyFeaturesTitle: data.details?.keyFeaturesTitle || 'Key Features',
|
||||
keyFeaturesImage: data.details?.keyFeaturesImage || '',
|
||||
faqTitle: data.details?.faqTitle || 'Frequently Asked Questions',
|
||||
faqImage: data.details?.faqImage || ''
|
||||
};
|
||||
|
||||
document.getElementById('detailsJson').value = JSON.stringify(details);
|
||||
document.getElementById('featuresJson').value = JSON.stringify(data.details?.features || []);
|
||||
document.getElementById('faqJson').value = JSON.stringify(data.details?.faq || []);
|
||||
}
|
||||
|
||||
function updateJsonData() {
|
||||
// Collect basic details data
|
||||
const details = {
|
||||
title: document.getElementById('detailsTitle').value,
|
||||
description: document.getElementById('detailsDescription').value,
|
||||
mainImage: document.getElementById('mainImage').value,
|
||||
overviewTitle: document.getElementById('overviewTitle').value,
|
||||
overviewDescription: document.getElementById('overviewDescription').value,
|
||||
additionalDescription: document.getElementById('additionalDescription').value,
|
||||
keyFeaturesTitle: document.getElementById('keyFeaturesTitle').value,
|
||||
keyFeaturesImage: document.getElementById('keyFeaturesImage').value,
|
||||
faqTitle: document.getElementById('faqTitle').value,
|
||||
faqImage: document.getElementById('faqImage').value
|
||||
};
|
||||
|
||||
// Collect features data
|
||||
const features = [];
|
||||
document.querySelectorAll('.feature-item').forEach(item => {
|
||||
features.push({
|
||||
title: item.querySelector('.feature-title').value,
|
||||
description: item.querySelector('.feature-description').value
|
||||
});
|
||||
});
|
||||
|
||||
// Collect FAQ data
|
||||
const faq = [];
|
||||
document.querySelectorAll('.faq-item').forEach(item => {
|
||||
faq.push({
|
||||
id: item.querySelector('.faq-id').value,
|
||||
question: item.querySelector('.faq-question').value,
|
||||
answer: item.querySelector('.faq-answer').value,
|
||||
isExpanded: item.querySelector('.faq-expanded').value === 'true'
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById('detailsJson').value = JSON.stringify(details);
|
||||
document.getElementById('featuresJson').value = JSON.stringify(features);
|
||||
document.getElementById('faqJson').value = JSON.stringify(faq);
|
||||
}
|
||||
|
||||
// Helper function để tạo full URL cho ảnh - tương tự như server-side helper
|
||||
function getFullImageUrlJS(imagePath) {
|
||||
if (!imagePath) return '';
|
||||
|
||||
// Nếu đã là full URL thì return luôn
|
||||
if (imagePath.startsWith('http')) {
|
||||
return imagePath;
|
||||
}
|
||||
|
||||
// Lấy backend URL
|
||||
const backendUrl = '<%= (process.env.BACKEND_URL || "http://localhost:3001").replace(/\/$/, "") %>';
|
||||
|
||||
// Xử lý đường dẫn
|
||||
let imgSrc = imagePath;
|
||||
if (!imgSrc.startsWith('/')) {
|
||||
imgSrc = '/uploads/' + imgSrc;
|
||||
}
|
||||
|
||||
return backendUrl + imgSrc;
|
||||
}
|
||||
|
||||
// Function để cập nhật image preview sau khi upload
|
||||
function updateImagePreviewAfterUpload(targetInput, imagePath) {
|
||||
const fullImageUrl = getFullImageUrlJS(imagePath);
|
||||
|
||||
switch(targetInput) {
|
||||
case 'mainImage':
|
||||
const mainPreview = document.getElementById('mainImagePreview');
|
||||
if (mainPreview) {
|
||||
mainPreview.innerHTML = `<img src="${fullImageUrl}" class="img-thumbnail" style="max-height: 250px; width: auto; max-width: 100%; object-fit: contain;" alt="Main image preview">`;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'keyFeaturesImage':
|
||||
const featuresPreview = document.getElementById('keyFeaturesImagePreview');
|
||||
if (featuresPreview) {
|
||||
featuresPreview.innerHTML = `<img src="${fullImageUrl}" class="img-thumbnail" style="max-height: 180px; width: auto; max-width: 100%; object-fit: contain;" alt="Key Features image preview">`;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'faqImage':
|
||||
const faqPreview = document.getElementById('faqImagePreview');
|
||||
if (faqPreview) {
|
||||
faqPreview.innerHTML = `<img src="${fullImageUrl}" class="img-thumbnail" style="max-height: 180px; width: auto; max-width: 100%; object-fit: contain;" alt="FAQ image preview">`;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
async function openImageUploader(targetInput, imageType) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const fileInput = document.createElement('input');
|
||||
fileInput.type = 'file';
|
||||
fileInput.accept = 'image/*';
|
||||
fileInput.style.display = 'none';
|
||||
document.body.appendChild(fileInput);
|
||||
|
||||
fileInput.onchange = async function (e) {
|
||||
const file = e.target.files[0];
|
||||
if (!file) return reject(new Error('No file selected'));
|
||||
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('image', file);
|
||||
|
||||
const uploadBtn = document.querySelector(`[data-target-input="${targetInput}"]`);
|
||||
const originalBtnHtml = uploadBtn ? uploadBtn.innerHTML : null;
|
||||
if (uploadBtn) {
|
||||
uploadBtn.disabled = true;
|
||||
uploadBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Uploading...';
|
||||
}
|
||||
|
||||
const response = await fetch(`/admin/upload/image?imageType=${encodeURIComponent(imageType)}`, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Upload failed');
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Upload failed');
|
||||
}
|
||||
|
||||
const input = document.getElementById(targetInput) || document.querySelector(`[name="${targetInput}"]`);
|
||||
if (!input) throw new Error('Target input not found');
|
||||
|
||||
input.value = result.path;
|
||||
|
||||
// Update image preview based on target input
|
||||
updateImagePreviewAfterUpload(targetInput, result.path);
|
||||
|
||||
if (uploadBtn) {
|
||||
uploadBtn.disabled = false;
|
||||
uploadBtn.innerHTML = originalBtnHtml;
|
||||
}
|
||||
|
||||
resolve(result);
|
||||
} catch (err) {
|
||||
if (uploadBtn) {
|
||||
uploadBtn.disabled = false;
|
||||
uploadBtn.innerHTML = originalBtnHtml;
|
||||
}
|
||||
console.error('Upload error:', err);
|
||||
showError('Upload failed: ' + (err.message || 'Unknown error'));
|
||||
reject(err);
|
||||
} finally {
|
||||
fileInput.remove();
|
||||
}
|
||||
};
|
||||
|
||||
fileInput.click();
|
||||
});
|
||||
}
|
||||
|
||||
function showSuccess(message) {
|
||||
// Create and show success alert
|
||||
const alert = document.createElement('div');
|
||||
alert.className = 'alert alert-success alert-dismissible fade show position-fixed';
|
||||
alert.style.cssText = 'top: 20px; right: 20px; z-index: 9999; min-width: 300px;';
|
||||
alert.innerHTML = `
|
||||
${message}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
`;
|
||||
document.body.appendChild(alert);
|
||||
|
||||
setTimeout(() => {
|
||||
if (alert.parentNode) {
|
||||
alert.parentNode.removeChild(alert);
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
function showError(message) {
|
||||
// Create and show error alert
|
||||
const alert = document.createElement('div');
|
||||
alert.className = 'alert alert-danger alert-dismissible fade show position-fixed';
|
||||
alert.style.cssText = 'top: 20px; right: 20px; z-index: 9999; min-width: 300px;';
|
||||
alert.innerHTML = `
|
||||
${message}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
`;
|
||||
document.body.appendChild(alert);
|
||||
|
||||
setTimeout(() => {
|
||||
if (alert.parentNode) {
|
||||
alert.parentNode.removeChild(alert);
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.bottom-buttons {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
margin-top: 20px;
|
||||
padding: 15px 0;
|
||||
border-top: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
.content-with-fixed-buttons {
|
||||
/* Remove bottom padding since buttons are no longer fixed */
|
||||
}
|
||||
|
||||
.btn-group .btn {
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
.btn-group .btn:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.card-header h6 {
|
||||
color: var(--primary-dark);
|
||||
}
|
||||
|
||||
.text-decoration-underline {
|
||||
text-decoration: underline;
|
||||
color: var(--primary-dark);
|
||||
}
|
||||
|
||||
/* Image Preview Styles */
|
||||
#mainImagePreview, #keyFeaturesImagePreview, #faqImagePreview {
|
||||
min-height: 60px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #f8f9fa;
|
||||
border: 2px dashed #dee2e6;
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
#mainImagePreview:empty::before,
|
||||
#keyFeaturesImagePreview:empty::before,
|
||||
#faqImagePreview:empty::before {
|
||||
content: "No image selected";
|
||||
color: #6c757d;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
#mainImagePreview img,
|
||||
#keyFeaturesImagePreview img,
|
||||
#faqImagePreview img {
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
}
|
||||
</style>
|
||||
440
views/admin/service/edit.ejs
Normal file
440
views/admin/service/edit.ejs
Normal file
@@ -0,0 +1,440 @@
|
||||
<div class="container">
|
||||
<div class="d-flex justify-content-between align-items-center mt-4 mb-4">
|
||||
<div>
|
||||
<h1 class="h3 mb-0" style="color: var(--primary-dark);">Edit Service: <%= service.name %></h1>
|
||||
<p class="text-muted mb-0">Update service information and settings</p>
|
||||
</div>
|
||||
<div>
|
||||
<a href="/admin/service" class="btn btn-outline-primary">
|
||||
<i class="fas fa-arrow-left me-2"></i>Back to Services
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<form action="/admin/service/<%= service.slug %>/edit" method="POST" id="editServiceForm">
|
||||
<div class="card shadow-sm border-0 mb-4">
|
||||
<div class="card-header bg-white border-bottom">
|
||||
<h5 class="mb-0" style="color: var(--primary-dark);">
|
||||
<i class="fas fa-edit me-2"></i>Service Information
|
||||
</h5>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-medium">Service Name</label>
|
||||
<input type="text" class="form-control" id="serviceName" name="name"
|
||||
value="<%= service.name %>" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-3 mt-2">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">
|
||||
Slug
|
||||
<small class="text-muted">(generated from name)</small>
|
||||
<span id="slugAutoIndicator" class="badge bg-info ms-1" style="font-size: 0.7em;">EXISTING</span>
|
||||
</label>
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" id="serviceSlug" name="slug"
|
||||
value="<%= service.slug %>" readonly>
|
||||
<button type="button" class="btn btn-primary" id="generateSlugBtn" title="Generate slug from name">
|
||||
<i class="fas fa-magic me-1"></i>Generate
|
||||
</button>
|
||||
</div>
|
||||
<small class="form-text text-muted">URL-friendly version of the service name.</small>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Layout</label>
|
||||
<select class="form-control" id="serviceLayout" name="layout">
|
||||
<option value="left" <%= service.layout === 'left' ? 'selected' : '' %>>Left</option>
|
||||
<option value="right" <%= service.layout === 'right' ? 'selected' : '' %>>Right</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-3 mt-2">
|
||||
<div class="col-md-8">
|
||||
<label class="form-label fw-medium">Image</label>
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" id="serviceImage" name="image"
|
||||
value="<%= service.image || '' %>">
|
||||
<button type="button" class="btn btn-outline-primary btn-upload-image"
|
||||
data-target-input="serviceImage" data-image-type="service">
|
||||
<i class="fas fa-upload me-1"></i>Upload
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div id="serviceImagePreview">
|
||||
<% if (service.image) { %>
|
||||
<img src="<%= getFullImageUrl(service.image) %>" class="img-thumbnail"
|
||||
style="height: 80px; width: 100%; object-fit: cover;" alt="Preview">
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-3 mt-2">
|
||||
<div class="col-12">
|
||||
<label class="form-label fw-medium">Description</label>
|
||||
<textarea class="form-control" id="serviceDescription" name="description"
|
||||
rows="3" required><%= service.description %></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bottom Buttons -->
|
||||
<div class="bottom-buttons">
|
||||
<a href="/admin/service" class="btn btn-secondary">
|
||||
<i class="fas fa-times me-2"></i>Cancel
|
||||
</a>
|
||||
<button type="submit" class="btn btn-primary" id="submitBtn">
|
||||
<i class="fas fa-save me-2"></i>Update Service
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Scripts -->
|
||||
<script>
|
||||
let servicesData = []; // Will be populated for duplicate checking
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Load existing services data for duplicate checking
|
||||
loadServicesData();
|
||||
|
||||
// Initialize form handlers
|
||||
initializeFormHandlers();
|
||||
});
|
||||
|
||||
async function loadServicesData() {
|
||||
try {
|
||||
const response = await fetch('/api/service');
|
||||
const data = await response.json();
|
||||
servicesData = data.services?.items || [];
|
||||
} catch (error) {
|
||||
console.error('Error loading services data:', error);
|
||||
servicesData = [];
|
||||
}
|
||||
}
|
||||
|
||||
function initializeFormHandlers() {
|
||||
// Form submission
|
||||
const form = document.getElementById('editServiceForm');
|
||||
form.addEventListener('submit', async function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const submitBtn = document.getElementById('submitBtn');
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i>Updating...';
|
||||
|
||||
try {
|
||||
// Check for duplicate slug before submitting
|
||||
const slug = document.getElementById('serviceSlug').value.trim();
|
||||
const currentSlug = '<%= service.slug %>';
|
||||
|
||||
if (slug !== currentSlug && isSlugDuplicate(slug, -1)) {
|
||||
showError('Service with this slug already exists. Please generate a new slug.');
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.innerHTML = '<i class="fas fa-save me-2"></i>Update Service';
|
||||
return;
|
||||
}
|
||||
|
||||
this.submit();
|
||||
} catch (error) {
|
||||
console.error('Error updating service:', error);
|
||||
showError('Failed to update service. Please try again.');
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.innerHTML = '<i class="fas fa-save me-2"></i>Update Service';
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize image upload buttons
|
||||
document.querySelectorAll('.btn-upload-image').forEach(button => {
|
||||
button.addEventListener('click', function() {
|
||||
const targetInput = this.dataset.targetInput;
|
||||
const imageType = this.dataset.imageType;
|
||||
openImageUploader(targetInput, imageType);
|
||||
});
|
||||
});
|
||||
|
||||
// Image preview for service image
|
||||
const serviceImageInput = document.getElementById('serviceImage');
|
||||
if (serviceImageInput) {
|
||||
serviceImageInput.addEventListener('input', function() {
|
||||
updateImagePreview('serviceImagePreview', this.value);
|
||||
});
|
||||
}
|
||||
|
||||
// Generate slug from service name
|
||||
const serviceNameInput = document.getElementById('serviceName');
|
||||
const serviceSlugInput = document.getElementById('serviceSlug');
|
||||
const slugAutoIndicator = document.getElementById('slugAutoIndicator');
|
||||
const generateSlugBtn = document.getElementById('generateSlugBtn');
|
||||
|
||||
if (serviceNameInput && serviceSlugInput && generateSlugBtn) {
|
||||
// Generate slug button
|
||||
generateSlugBtn.addEventListener('click', async function() {
|
||||
const serviceName = serviceNameInput.value.trim();
|
||||
if (serviceName) {
|
||||
// Show loading state
|
||||
const originalBtnHtml = this.innerHTML;
|
||||
this.disabled = true;
|
||||
this.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Generating...';
|
||||
|
||||
try {
|
||||
const slug = await generateSlugFromText(serviceName);
|
||||
const currentSlug = '<%= service.slug %>';
|
||||
|
||||
// Check for duplicate slug (excluding current service)
|
||||
if (slug !== currentSlug && isSlugDuplicate(slug, -1)) {
|
||||
const uniqueSlug = generateUniqueSlug(slug);
|
||||
serviceSlugInput.value = uniqueSlug;
|
||||
showWarning(`Slug "${slug}" already exists. Generated unique slug: "${uniqueSlug}"`);
|
||||
} else {
|
||||
serviceSlugInput.value = slug;
|
||||
showSuccess('Slug generated successfully!');
|
||||
}
|
||||
|
||||
if (slugAutoIndicator) {
|
||||
slugAutoIndicator.textContent = 'GENERATED';
|
||||
slugAutoIndicator.className = 'badge bg-success ms-1';
|
||||
slugAutoIndicator.style.fontSize = '0.7em';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error generating slug:', error);
|
||||
showError('Failed to generate slug. Please try again.');
|
||||
} finally {
|
||||
// Restore button state
|
||||
this.disabled = false;
|
||||
this.innerHTML = originalBtnHtml;
|
||||
}
|
||||
} else {
|
||||
showError('Please enter a service name first.');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Generate slug using backend API
|
||||
async function generateSlugFromText(text) {
|
||||
try {
|
||||
const response = await fetch('/admin/service/generate-slug', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ text: text })
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
return result.slug;
|
||||
} else {
|
||||
throw new Error(result.message || 'Failed to generate slug');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error generating slug:', error);
|
||||
// Fallback to simple slug generation if API fails
|
||||
return text
|
||||
.toString()
|
||||
.toLowerCase()
|
||||
.trim()
|
||||
.replace(/\s+/g, '-')
|
||||
.replace(/[^\w\-]+/g, '')
|
||||
.replace(/\-\-+/g, '-')
|
||||
.replace(/^-+/, '')
|
||||
.replace(/-+$/, '');
|
||||
}
|
||||
}
|
||||
|
||||
// Check if slug already exists
|
||||
function isSlugDuplicate(slug, excludeIndex = -1) {
|
||||
return servicesData.some((service, index) => {
|
||||
return service && service.slug === slug && index !== excludeIndex;
|
||||
});
|
||||
}
|
||||
|
||||
// Generate unique slug by appending number
|
||||
function generateUniqueSlug(baseSlug) {
|
||||
let counter = 1;
|
||||
let uniqueSlug = baseSlug;
|
||||
|
||||
while (isSlugDuplicate(uniqueSlug, -1)) {
|
||||
uniqueSlug = baseSlug + '-' + counter;
|
||||
counter++;
|
||||
}
|
||||
|
||||
return uniqueSlug;
|
||||
}
|
||||
|
||||
function updateImagePreview(previewId, imagePath) {
|
||||
const preview = document.getElementById(previewId);
|
||||
if (imagePath) {
|
||||
const fullImageUrl = getFullImageUrlJS(imagePath);
|
||||
preview.innerHTML = `<img src="${fullImageUrl}" class="img-thumbnail" style="height: 80px; width: 100%; object-fit: cover;" alt="Preview">`;
|
||||
} else {
|
||||
preview.innerHTML = '';
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function để tạo full URL cho ảnh
|
||||
function getFullImageUrlJS(imagePath) {
|
||||
if (!imagePath) return '';
|
||||
|
||||
if (imagePath.startsWith('http')) {
|
||||
return imagePath;
|
||||
}
|
||||
|
||||
const backendUrl = '<%= (process.env.BACKEND_URL || "http://localhost:3001").replace(/\/$/, "") %>';
|
||||
|
||||
let imgSrc = imagePath;
|
||||
if (!imgSrc.startsWith('/')) {
|
||||
imgSrc = '/uploads/' + imgSrc;
|
||||
}
|
||||
|
||||
return backendUrl + imgSrc;
|
||||
}
|
||||
|
||||
async function openImageUploader(targetInput, imageType) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const fileInput = document.createElement('input');
|
||||
fileInput.type = 'file';
|
||||
fileInput.accept = 'image/*';
|
||||
fileInput.style.display = 'none';
|
||||
document.body.appendChild(fileInput);
|
||||
|
||||
fileInput.onchange = async function (e) {
|
||||
const file = e.target.files[0];
|
||||
if (!file) return reject(new Error('No file selected'));
|
||||
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('image', file);
|
||||
|
||||
const uploadBtn = document.querySelector(`[data-target-input="${targetInput}"]`);
|
||||
const originalBtnHtml = uploadBtn ? uploadBtn.innerHTML : null;
|
||||
if (uploadBtn) {
|
||||
uploadBtn.disabled = true;
|
||||
uploadBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Uploading...';
|
||||
}
|
||||
|
||||
const response = await fetch(`/admin/upload/image?imageType=service`, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Upload failed');
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Upload failed');
|
||||
}
|
||||
|
||||
const input = document.getElementById(targetInput);
|
||||
if (!input) throw new Error('Target input not found');
|
||||
|
||||
input.value = result.path;
|
||||
|
||||
// Update preview
|
||||
if (targetInput === 'serviceImage') {
|
||||
updateImagePreview('serviceImagePreview', result.path);
|
||||
}
|
||||
|
||||
if (uploadBtn) {
|
||||
uploadBtn.disabled = false;
|
||||
uploadBtn.innerHTML = originalBtnHtml;
|
||||
}
|
||||
|
||||
resolve(result);
|
||||
} catch (err) {
|
||||
if (uploadBtn) {
|
||||
uploadBtn.disabled = false;
|
||||
uploadBtn.innerHTML = originalBtnHtml;
|
||||
}
|
||||
console.error('Upload error:', err);
|
||||
showError('Upload failed: ' + (err.message || 'Unknown error'));
|
||||
reject(err);
|
||||
} finally {
|
||||
fileInput.remove();
|
||||
}
|
||||
};
|
||||
|
||||
fileInput.click();
|
||||
});
|
||||
}
|
||||
|
||||
function showSuccess(message) {
|
||||
const alert = document.createElement('div');
|
||||
alert.className = 'alert alert-success alert-dismissible fade show position-fixed';
|
||||
alert.style.cssText = 'top: 20px; right: 20px; z-index: 9999; min-width: 300px;';
|
||||
alert.innerHTML = `
|
||||
${message}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
`;
|
||||
document.body.appendChild(alert);
|
||||
|
||||
setTimeout(() => {
|
||||
if (alert.parentNode) {
|
||||
alert.parentNode.removeChild(alert);
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
function showWarning(message) {
|
||||
const alert = document.createElement('div');
|
||||
alert.className = 'alert alert-warning alert-dismissible fade show position-fixed';
|
||||
alert.style.cssText = 'top: 20px; right: 20px; z-index: 9999; min-width: 300px;';
|
||||
alert.innerHTML = `
|
||||
${message}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
`;
|
||||
document.body.appendChild(alert);
|
||||
|
||||
setTimeout(() => {
|
||||
if (alert.parentNode) {
|
||||
alert.parentNode.removeChild(alert);
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
function showError(message) {
|
||||
const alert = document.createElement('div');
|
||||
alert.className = 'alert alert-danger alert-dismissible fade show position-fixed';
|
||||
alert.style.cssText = 'top: 20px; right: 20px; z-index: 9999; min-width: 300px;';
|
||||
alert.innerHTML = `
|
||||
${message}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
`;
|
||||
document.body.appendChild(alert);
|
||||
|
||||
setTimeout(() => {
|
||||
if (alert.parentNode) {
|
||||
alert.parentNode.removeChild(alert);
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.bottom-buttons {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
margin-top: 20px;
|
||||
padding: 15px 0;
|
||||
border-top: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
.card-header h5 {
|
||||
color: var(--primary-dark);
|
||||
}
|
||||
</style>
|
||||
782
views/admin/service/index.ejs
Normal file
782
views/admin/service/index.ejs
Normal file
@@ -0,0 +1,782 @@
|
||||
<div class="container">
|
||||
<div class="d-flex justify-content-between align-items-center mt-4 mb-4">
|
||||
<div>
|
||||
<h1 class="h3 mb-0" style="color: var(--primary-dark);"><%= title %></h1>
|
||||
<p class="text-muted mb-0">Manage services and their detailed content</p>
|
||||
</div>
|
||||
<div>
|
||||
<a href="<%= frontendUrl %>/admin/service/" class="btn btn-outline-primary" target="_blank">
|
||||
<i class="fas fa-external-link-alt me-2"></i>View Services Page
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<form action="/admin/service/update" method="POST" class="content-with-fixed-buttons" id="serviceForm">
|
||||
<!-- Hidden inputs for JSON data -->
|
||||
<input type="hidden" name="services" id="servicesJson">
|
||||
|
||||
<!-- 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="#services-list" role="tab">
|
||||
<i class="fas fa-list me-2"></i>Services List
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-bs-toggle="tab" href="#section-settings" role="tab">
|
||||
<i class="fas fa-cog me-2"></i>Section Settings
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-bs-toggle="tab" href="#add-service" role="tab">
|
||||
<i class="fas fa-plus me-2"></i>Add Service
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<div class="tab-content">
|
||||
<!-- Services List Tab -->
|
||||
<div class="tab-pane fade show active" id="services-list" role="tabpanel">
|
||||
<div class="card border shadow-sm">
|
||||
<div class="card-body">
|
||||
<!-- Statistics Cards -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-4">
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="bg-primary bg-opacity-10 rounded-circle p-3">
|
||||
<i class="fas fa-cogs text-primary"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-grow-1 ms-3">
|
||||
<h6 class="text-muted mb-0">Total Services</h6>
|
||||
<h3 class="mb-0" id="totalCount">
|
||||
<%= (data.services && data.services.items) ? data.services.items.length : 0 %>
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="bg-success bg-opacity-10 rounded-circle p-3">
|
||||
<i class="fas fa-align-left text-success"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-grow-1 ms-3">
|
||||
<h6 class="text-muted mb-0">Left Layout</h6>
|
||||
<h3 class="mb-0" id="leftCount">
|
||||
<%= (data.services && data.services.items) ? data.services.items.filter(s => s && s.layout === 'left').length : 0 %>
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="bg-info bg-opacity-10 rounded-circle p-3">
|
||||
<i class="fas fa-align-right text-info"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-grow-1 ms-3">
|
||||
<h6 class="text-muted mb-0">Right Layout</h6>
|
||||
<h3 class="mb-0" id="rightCount">
|
||||
<%= (data.services && data.services.items) ? data.services.items.filter(s => s && s.layout === 'right').length : 0 %>
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Services Table -->
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover mb-0" id="servicesTable">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th style="width: 40px;">#</th>
|
||||
<th style="width: 80px;">Image</th>
|
||||
<th>Name</th>
|
||||
<th>Slug</th>
|
||||
<th style="width: 100px;">Layout</th>
|
||||
<th style="width: 200px;">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="servicesTableBody">
|
||||
<% if (data.services && data.services.items) { %>
|
||||
<% data.services.items.forEach((service, index) => { %>
|
||||
<% if (service) { %>
|
||||
<tr data-index="<%= index %>">
|
||||
<td class="text-muted"><%= index + 1 %></td>
|
||||
<td>
|
||||
<% if (service.image) { %>
|
||||
<img src="<%= getFullImageUrl(service.image) %>" class="rounded" style="width: 60px; height: 45px; object-fit: cover;" alt="<%= service.name %>" >
|
||||
<% } else { %>
|
||||
<div class="bg-light rounded d-flex align-items-center justify-content-center" style="width: 60px; height: 45px;">
|
||||
<i class="fas fa-image text-muted"></i>
|
||||
</div>
|
||||
<% } %>
|
||||
</td>
|
||||
<td>
|
||||
<strong><%= service.name %></strong>
|
||||
<% if (service.description) { %>
|
||||
<br><small class="text-muted"><%= service.description.substring(0, 50) %>...</small>
|
||||
<% } %>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge bg-light text-dark"><%= service.slug %></span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge bg-<%= service.layout === 'left' ? 'primary' : 'success' %>">
|
||||
<i class="fas fa-align-<%= service.layout %>"></i> <%= service.layout %>
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<div class="btn-group" role="group">
|
||||
<a href="/admin/service/<%= service.slug %>/edit" class="btn btn-sm btn-outline-primary" title="Edit Service">
|
||||
<i class="fas fa-edit"></i>
|
||||
</a>
|
||||
<a href="/admin/service/<%= service.slug %>/details" class="btn btn-sm btn-outline-warning" title="Edit Details">
|
||||
<i class="fas fa-cog"></i>
|
||||
</a>
|
||||
<button type="button" class="btn btn-sm btn-outline-danger" onclick="deleteService(<%= index %>)" title="Delete Service">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<% } %>
|
||||
<% }) %>
|
||||
<% } %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<% if (!data.services || !data.services.items || data.services.items.length === 0) { %>
|
||||
<div class="text-center py-5">
|
||||
<i class="fas fa-inbox fa-3x text-muted mb-3"></i>
|
||||
<h5 class="text-muted">No services found</h5>
|
||||
<p class="text-muted">Click "Add Service" tab to create your first service.</p>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Section Settings Tab -->
|
||||
<div class="tab-pane fade" id="section-settings" role="tabpanel">
|
||||
<div class="card border shadow-sm">
|
||||
<div class="card-body">
|
||||
<h6 class="fw-medium mb-3">Services Section Title</h6>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Sub Title</label>
|
||||
<input type="text" class="form-control" id="servicesSubTitle"
|
||||
value="<%= data.services?.title?.subTitle || '' %>"
|
||||
placeholder="Enter subtitle for services section">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Main Title</label>
|
||||
<input type="text" class="form-control" id="servicesMainTitle"
|
||||
value="<%= data.services?.title?.mainTitle || '' %>"
|
||||
placeholder="Enter main title for services section">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add Service Tab -->
|
||||
<div class="tab-pane fade" id="add-service" role="tabpanel">
|
||||
<div class="card border shadow-sm">
|
||||
<div class="card-body">
|
||||
<h6 class="fw-medium mb-3">Add New Service</h6>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-medium">Service Name</label>
|
||||
<input type="text" class="form-control" id="newServiceName" placeholder="Enter service name">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row g-3 mt-2">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">
|
||||
Slug
|
||||
<small class="text-muted">(generated from name)</small>
|
||||
<span id="slugAutoIndicator" class="badge bg-success ms-1" style="font-size: 0.7em;">AUTO</span>
|
||||
</label>
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" id="newServiceSlug" placeholder="Click generate to create slug" readonly>
|
||||
<button type="button" class="btn btn-primary" id="generateSlugBtn" title="Generate slug from name">
|
||||
<i class="fas fa-magic me-1"></i>Generate
|
||||
</button>
|
||||
</div>
|
||||
<small class="form-text text-muted">URL-friendly version of the service name. Generated automatically from the service name.</small>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Layout</label>
|
||||
<select class="form-control" id="newServiceLayout">
|
||||
<option value="left">Left</option>
|
||||
<option value="right">Right</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row g-3 mt-2">
|
||||
<div class="col-md-8">
|
||||
<label class="form-label fw-medium">Image</label>
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" id="newServiceImage" placeholder="Enter image path">
|
||||
<button type="button" class="btn btn-outline-primary btn-upload-image" data-target-input="newServiceImage" data-image-type="service">
|
||||
<i class="fas fa-upload me-1"></i>Upload
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div id="newServiceImagePreview"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row g-3 mt-2">
|
||||
<div class="col-12">
|
||||
<label class="form-label fw-medium">Description</label>
|
||||
<textarea class="form-control" id="newServiceDescription" rows="3" placeholder="Enter service description"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-3">
|
||||
<button type="button" class="btn btn-primary" onclick="addService()">
|
||||
<i class="fas fa-plus me-2"></i>Add Service
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bottom Buttons -->
|
||||
<div class="bottom-buttons">
|
||||
<button type="reset" class="btn btn-secondary">
|
||||
<i class="fas fa-undo me-2"></i>Reset
|
||||
</button>
|
||||
<button type="submit" class="btn btn-primary" id="submitBtn">
|
||||
<i class="fas fa-save me-2"></i>Save Changes
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Scripts -->
|
||||
<script>
|
||||
let originalFormData = null;
|
||||
let servicesData = <%- JSON.stringify(data.services?.items || []) %>;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Initialize form data
|
||||
originalFormData = <%- JSON.stringify(data) %>;
|
||||
|
||||
// Set initial JSON values
|
||||
updateAllJsonInputs(originalFormData);
|
||||
|
||||
// Initialize form handlers
|
||||
initializeFormHandlers();
|
||||
});
|
||||
|
||||
function initializeFormHandlers() {
|
||||
// Form submission
|
||||
const form = document.getElementById('serviceForm');
|
||||
form.addEventListener('submit', async function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const submitBtn = document.getElementById('submitBtn');
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i>Saving...';
|
||||
|
||||
try {
|
||||
updateJsonData();
|
||||
this.submit();
|
||||
} catch (error) {
|
||||
console.error('Error updating data:', error);
|
||||
showError('Failed to process form data. Please try again.');
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.innerHTML = '<i class="fas fa-save me-2"></i>Save Changes';
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize image upload buttons
|
||||
document.querySelectorAll('.btn-upload-image').forEach(button => {
|
||||
button.addEventListener('click', function() {
|
||||
const targetInput = this.dataset.targetInput;
|
||||
const imageType = this.dataset.imageType;
|
||||
openImageUploader(targetInput, imageType);
|
||||
});
|
||||
});
|
||||
|
||||
// Image preview for new service
|
||||
const newServiceImageInput = document.getElementById('newServiceImage');
|
||||
if (newServiceImageInput) {
|
||||
newServiceImageInput.addEventListener('input', function() {
|
||||
updateImagePreview('newServiceImagePreview', this.value);
|
||||
});
|
||||
}
|
||||
|
||||
// Generate slug from service name
|
||||
const newServiceNameInput = document.getElementById('newServiceName');
|
||||
const newServiceSlugInput = document.getElementById('newServiceSlug');
|
||||
const slugAutoIndicator = document.getElementById('slugAutoIndicator');
|
||||
const generateSlugBtn = document.getElementById('generateSlugBtn');
|
||||
|
||||
if (newServiceNameInput && newServiceSlugInput && generateSlugBtn) {
|
||||
// Generate slug button
|
||||
generateSlugBtn.addEventListener('click', async function() {
|
||||
const serviceName = newServiceNameInput.value.trim();
|
||||
if (serviceName) {
|
||||
// Show loading state
|
||||
const originalBtnHtml = this.innerHTML;
|
||||
this.disabled = true;
|
||||
this.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Generating...';
|
||||
|
||||
try {
|
||||
const slug = await generateSlugFromText(serviceName);
|
||||
|
||||
// Check if we're in edit mode
|
||||
const addBtn = document.querySelector('#add-service .btn-primary');
|
||||
const isEditMode = addBtn && addBtn.textContent.includes('Update');
|
||||
|
||||
// Get current service index if editing
|
||||
let currentIndex = -1;
|
||||
if (isEditMode) {
|
||||
const onclickAttr = addBtn.getAttribute('onclick');
|
||||
const match = onclickAttr.match(/updateService\((\d+)\)/);
|
||||
if (match) {
|
||||
currentIndex = parseInt(match[1]);
|
||||
}
|
||||
}
|
||||
|
||||
// Check for duplicate slug
|
||||
if (isSlugDuplicate(slug, currentIndex)) {
|
||||
const uniqueSlug = generateUniqueSlug(slug);
|
||||
newServiceSlugInput.value = uniqueSlug;
|
||||
showWarning(`Slug "${slug}" already exists. Generated unique slug: "${uniqueSlug}"`);
|
||||
} else {
|
||||
newServiceSlugInput.value = slug;
|
||||
showSuccess('Slug generated successfully!');
|
||||
}
|
||||
|
||||
if (slugAutoIndicator) {
|
||||
slugAutoIndicator.textContent = 'GENERATED';
|
||||
slugAutoIndicator.className = 'badge bg-success ms-1';
|
||||
slugAutoIndicator.style.fontSize = '0.7em';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error generating slug:', error);
|
||||
showError('Failed to generate slug. Please try again.');
|
||||
} finally {
|
||||
// Restore button state
|
||||
this.disabled = false;
|
||||
this.innerHTML = originalBtnHtml;
|
||||
}
|
||||
} else {
|
||||
showError('Please enter a service name first.');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function để tạo full URL cho ảnh - tương tự như server-side helper
|
||||
function getFullImageUrlJS(imagePath) {
|
||||
if (!imagePath) return '';
|
||||
|
||||
// Nếu đã là full URL thì return luôn
|
||||
if (imagePath.startsWith('http')) {
|
||||
return imagePath;
|
||||
}
|
||||
|
||||
// Lấy backend URL
|
||||
const backendUrl = '<%= (process.env.BACKEND_URL || "http://localhost:3001").replace(/\/$/, "") %>';
|
||||
|
||||
// Xử lý đường dẫn
|
||||
let imgSrc = imagePath;
|
||||
if (!imgSrc.startsWith('/')) {
|
||||
imgSrc = '/uploads/' + imgSrc;
|
||||
}
|
||||
|
||||
return backendUrl + imgSrc;
|
||||
}
|
||||
|
||||
// Generate slug using backend API
|
||||
async function generateSlugFromText(text) {
|
||||
try {
|
||||
const response = await fetch('/admin/service/generate-slug', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ text: text })
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
return result.slug;
|
||||
} else {
|
||||
throw new Error(result.message || 'Failed to generate slug');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error generating slug:', error);
|
||||
// Fallback to simple slug generation if API fails
|
||||
return text
|
||||
.toString()
|
||||
.toLowerCase()
|
||||
.trim()
|
||||
.replace(/\s+/g, '-')
|
||||
.replace(/[^\w\-]+/g, '')
|
||||
.replace(/\-\-+/g, '-')
|
||||
.replace(/^-+/, '')
|
||||
.replace(/-+$/, '');
|
||||
}
|
||||
}
|
||||
|
||||
// Check if slug already exists (excluding current service when editing)
|
||||
function isSlugDuplicate(slug, excludeIndex = -1) {
|
||||
return servicesData.some((service, index) => {
|
||||
return service && service.slug === slug && index !== excludeIndex;
|
||||
});
|
||||
}
|
||||
|
||||
// Generate unique slug by appending number
|
||||
function generateUniqueSlug(baseSlug) {
|
||||
let counter = 1;
|
||||
let uniqueSlug = baseSlug;
|
||||
|
||||
while (isSlugDuplicate(uniqueSlug, -1)) {
|
||||
uniqueSlug = baseSlug + '-' + counter;
|
||||
counter++;
|
||||
}
|
||||
|
||||
return uniqueSlug;
|
||||
}
|
||||
|
||||
function updateImagePreview(previewId, imagePath) {
|
||||
const preview = document.getElementById(previewId);
|
||||
if (imagePath) {
|
||||
const fullImageUrl = getFullImageUrlJS(imagePath);
|
||||
preview.innerHTML = `<img src="${fullImageUrl}" class="img-thumbnail" style="height: 80px; width: 100%; object-fit: cover;" alt="Preview">`;
|
||||
} else {
|
||||
preview.innerHTML = '';
|
||||
}
|
||||
}
|
||||
|
||||
function addService() {
|
||||
const name = document.getElementById('newServiceName').value.trim();
|
||||
const slug = document.getElementById('newServiceSlug').value.trim();
|
||||
const description = document.getElementById('newServiceDescription').value.trim();
|
||||
const image = document.getElementById('newServiceImage').value.trim();
|
||||
const layout = document.getElementById('newServiceLayout').value;
|
||||
|
||||
if (!name || !slug || !description) {
|
||||
showError('Please fill in all required fields and generate a slug.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if service with same slug already exists
|
||||
if (isSlugDuplicate(slug, -1)) {
|
||||
showError('Service with this slug already exists. Please generate a new slug.');
|
||||
return;
|
||||
}
|
||||
|
||||
const newService = {
|
||||
slug: slug,
|
||||
name: name,
|
||||
description: description,
|
||||
image: image,
|
||||
layout: layout,
|
||||
details: {
|
||||
title: name,
|
||||
description: description,
|
||||
mainImage: "img/inner-page/service-details/details-1.jpg",
|
||||
overviewTitle: "Service Overview",
|
||||
overviewDescription: "Service overview description...",
|
||||
additionalDescription: "Additional description...",
|
||||
keyFeaturesTitle: "Key Features",
|
||||
keyFeaturesImage: "img/inner-page/service-details/details-2.jpg",
|
||||
features: [],
|
||||
faqTitle: "Frequently Asked Question",
|
||||
faqImage: "img/inner-page/service-details/details-3.jpg",
|
||||
faq: []
|
||||
}
|
||||
};
|
||||
|
||||
servicesData.push(newService);
|
||||
updateServicesTable();
|
||||
updateStatistics();
|
||||
|
||||
// Clear form
|
||||
clearAddServiceForm();
|
||||
|
||||
showSuccess('Service added successfully!');
|
||||
}
|
||||
|
||||
function deleteService(index) {
|
||||
if (confirm('Are you sure you want to delete this service? This action cannot be undone.')) {
|
||||
servicesData.splice(index, 1);
|
||||
updateServicesTable();
|
||||
updateStatistics();
|
||||
showSuccess('Service deleted successfully!');
|
||||
}
|
||||
}
|
||||
|
||||
function clearAddServiceForm() {
|
||||
document.getElementById('newServiceName').value = '';
|
||||
document.getElementById('newServiceSlug').value = '';
|
||||
document.getElementById('newServiceDescription').value = '';
|
||||
document.getElementById('newServiceImage').value = '';
|
||||
document.getElementById('newServiceLayout').value = 'left';
|
||||
document.getElementById('newServiceImagePreview').innerHTML = '';
|
||||
|
||||
// Reset slug indicator
|
||||
const slugAutoIndicator = document.getElementById('slugAutoIndicator');
|
||||
if (slugAutoIndicator) {
|
||||
slugAutoIndicator.textContent = 'AUTO';
|
||||
slugAutoIndicator.className = 'badge bg-success ms-1';
|
||||
slugAutoIndicator.style.fontSize = '0.7em';
|
||||
}
|
||||
}
|
||||
|
||||
function updateServicesTable() {
|
||||
const tbody = document.getElementById('servicesTableBody');
|
||||
tbody.innerHTML = '';
|
||||
|
||||
servicesData.forEach((service, index) => {
|
||||
if (!service) return; // Skip undefined services
|
||||
|
||||
let imageHtml = '';
|
||||
if (service.image) {
|
||||
const fullImageUrl = getFullImageUrlJS(service.image);
|
||||
imageHtml = `<img src="${fullImageUrl}" class="rounded" style="width: 60px; height: 45px; object-fit: cover;" alt="${service.name || 'Service'}" onerror="this.onerror=null; this.src='/images/placeholder.png'; this.classList.add('bg-light');">`;
|
||||
} else {
|
||||
imageHtml = `<div class="bg-light rounded d-flex align-items-center justify-content-center" style="width: 60px; height: 45px;"><i class="fas fa-image text-muted"></i></div>`;
|
||||
}
|
||||
|
||||
const row = `
|
||||
<tr data-index="${index}">
|
||||
<td class="text-muted">${index + 1}</td>
|
||||
<td>${imageHtml}</td>
|
||||
<td>
|
||||
<strong>${service.name || 'Unnamed Service'}</strong>
|
||||
${service.description ? `<br><small class="text-muted">${service.description.substring(0, 50)}...</small>` : ''}
|
||||
</td>
|
||||
<td><span class="badge bg-light text-dark">${service.slug || 'no-slug'}</span></td>
|
||||
<td><span class="badge bg-${(service.layout === 'left') ? 'primary' : 'success'}"><i class="fas fa-align-${service.layout || 'left'}"></i> ${service.layout || 'left'}</span></td>
|
||||
<td>
|
||||
<div class="btn-group" role="group">
|
||||
<a href="/admin/service/${service.slug || 'unknown'}/edit" class="btn btn-sm btn-outline-primary" title="Edit Service">
|
||||
<i class="fas fa-edit"></i>
|
||||
</a>
|
||||
<a href="/admin/service/${service.slug || 'unknown'}/details" class="btn btn-sm btn-outline-warning" title="Edit Details">
|
||||
<i class="fas fa-cog"></i>
|
||||
</a>
|
||||
<button type="button" class="btn btn-sm btn-outline-danger" onclick="deleteService(${index})" title="Delete Service">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
tbody.innerHTML += row;
|
||||
});
|
||||
}
|
||||
|
||||
function updateStatistics() {
|
||||
const validServices = servicesData.filter(s => s && s.layout);
|
||||
document.getElementById('totalCount').textContent = servicesData.length;
|
||||
document.getElementById('leftCount').textContent = validServices.filter(s => s.layout === 'left').length;
|
||||
document.getElementById('rightCount').textContent = validServices.filter(s => s.layout === 'right').length;
|
||||
}
|
||||
|
||||
function updateAllJsonInputs(data) {
|
||||
const servicesSection = {
|
||||
title: data.services?.title || { subTitle: '', mainTitle: '' },
|
||||
items: data.services?.items || []
|
||||
};
|
||||
|
||||
document.getElementById('servicesJson').value = JSON.stringify(servicesSection);
|
||||
}
|
||||
|
||||
function updateJsonData() {
|
||||
const servicesSection = {
|
||||
title: {
|
||||
subTitle: document.getElementById('servicesSubTitle').value,
|
||||
mainTitle: document.getElementById('servicesMainTitle').value,
|
||||
},
|
||||
items: servicesData
|
||||
};
|
||||
|
||||
// Gửi toàn bộ services section (bao gồm cả title và items)
|
||||
document.getElementById('servicesJson').value = JSON.stringify(servicesSection);
|
||||
}
|
||||
|
||||
async function openImageUploader(targetInput, imageType) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const fileInput = document.createElement('input');
|
||||
fileInput.type = 'file';
|
||||
fileInput.accept = 'image/*';
|
||||
fileInput.style.display = 'none';
|
||||
document.body.appendChild(fileInput);
|
||||
|
||||
fileInput.onchange = async function (e) {
|
||||
const file = e.target.files[0];
|
||||
if (!file) return reject(new Error('No file selected'));
|
||||
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('image', file);
|
||||
|
||||
const uploadBtn = document.querySelector(`[data-target-input="${targetInput}"]`);
|
||||
const originalBtnHtml = uploadBtn ? uploadBtn.innerHTML : null;
|
||||
if (uploadBtn) {
|
||||
uploadBtn.disabled = true;
|
||||
uploadBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Uploading...';
|
||||
}
|
||||
|
||||
// Sử dụng imageType=service để upload vào folder service
|
||||
const response = await fetch(`/admin/upload/image?imageType=service`, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Upload failed');
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Upload failed');
|
||||
}
|
||||
|
||||
const input = document.getElementById(targetInput) || document.querySelector(`[name="${targetInput}"]`);
|
||||
if (!input) throw new Error('Target input not found');
|
||||
|
||||
input.value = result.path;
|
||||
|
||||
// Update preview if it's the new service image
|
||||
if (targetInput === 'newServiceImage') {
|
||||
updateImagePreview('newServiceImagePreview', result.path);
|
||||
}
|
||||
|
||||
if (uploadBtn) {
|
||||
uploadBtn.disabled = false;
|
||||
uploadBtn.innerHTML = originalBtnHtml;
|
||||
}
|
||||
|
||||
resolve(result);
|
||||
} catch (err) {
|
||||
if (uploadBtn) {
|
||||
uploadBtn.disabled = false;
|
||||
uploadBtn.innerHTML = originalBtnHtml;
|
||||
}
|
||||
console.error('Upload error:', err);
|
||||
showError('Upload failed: ' + (err.message || 'Unknown error'));
|
||||
reject(err);
|
||||
} finally {
|
||||
fileInput.remove();
|
||||
}
|
||||
};
|
||||
|
||||
fileInput.click();
|
||||
});
|
||||
}
|
||||
|
||||
function showSuccess(message) {
|
||||
// Create and show success alert
|
||||
const alert = document.createElement('div');
|
||||
alert.className = 'alert alert-success alert-dismissible fade show position-fixed';
|
||||
alert.style.cssText = 'top: 20px; right: 20px; z-index: 9999; min-width: 300px;';
|
||||
alert.innerHTML = `
|
||||
${message}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
`;
|
||||
document.body.appendChild(alert);
|
||||
|
||||
setTimeout(() => {
|
||||
if (alert.parentNode) {
|
||||
alert.parentNode.removeChild(alert);
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
function showWarning(message) {
|
||||
// Create and show warning alert
|
||||
const alert = document.createElement('div');
|
||||
alert.className = 'alert alert-warning alert-dismissible fade show position-fixed';
|
||||
alert.style.cssText = 'top: 20px; right: 20px; z-index: 9999; min-width: 300px;';
|
||||
alert.innerHTML = `
|
||||
${message}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
`;
|
||||
document.body.appendChild(alert);
|
||||
|
||||
setTimeout(() => {
|
||||
if (alert.parentNode) {
|
||||
alert.parentNode.removeChild(alert);
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
function showError(message) {
|
||||
// Create and show error alert
|
||||
const alert = document.createElement('div');
|
||||
alert.className = 'alert alert-danger alert-dismissible fade show position-fixed';
|
||||
alert.style.cssText = 'top: 20px; right: 20px; z-index: 9999; min-width: 300px;';
|
||||
alert.innerHTML = `
|
||||
${message}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
`;
|
||||
document.body.appendChild(alert);
|
||||
|
||||
setTimeout(() => {
|
||||
if (alert.parentNode) {
|
||||
alert.parentNode.removeChild(alert);
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.bottom-buttons {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
margin-top: 20px;
|
||||
padding: 15px 0;
|
||||
border-top: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
.content-with-fixed-buttons {
|
||||
/* Remove bottom padding since buttons are no longer fixed */
|
||||
}
|
||||
|
||||
.btn-group .btn {
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
.btn-group .btn:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -771,6 +771,12 @@
|
||||
>Terms & Conditions</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
class="dropdown-item <%= currentPath === '/admin/service' ? 'active' : '' %>"
|
||||
href="/admin/service"
|
||||
>Service</a>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
Reference in New Issue
Block a user