forked from UKSOURCE/cms.hailearning.edu.vn
feat: Implement admin management for FAQ, Testimonial, and Video Gallery sections with new controllers, views, and routing.
This commit is contained in:
@@ -116,7 +116,7 @@
|
||||
<p class="text-muted mb-0 small">Manage FAQ</p>
|
||||
</div>
|
||||
</div>
|
||||
<a href="/admin/faq" class="btn btn-sm btn-primary w-100 mt-2">
|
||||
<a href="/admin/home/faq" class="btn btn-sm btn-primary w-100 mt-2">
|
||||
<i class="fas fa-edit me-2"></i>Edit
|
||||
</a>
|
||||
</div>
|
||||
@@ -327,19 +327,11 @@
|
||||
</td>
|
||||
<td><code>/api/header</code></td>
|
||||
<td>
|
||||
<span
|
||||
class="badge"
|
||||
style="background-color: var(--primary-color)"
|
||||
>GET</span
|
||||
>
|
||||
<span class="badge" style="background-color: var(--primary-color)">GET</span>
|
||||
</td>
|
||||
<td>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>
|
||||
@@ -356,19 +348,11 @@
|
||||
</td>
|
||||
<td><code>/api/home</code></td>
|
||||
<td>
|
||||
<span
|
||||
class="badge"
|
||||
style="background-color: var(--primary-color)"
|
||||
>GET</span
|
||||
>
|
||||
<span class="badge" style="background-color: var(--primary-color)">GET</span>
|
||||
</td>
|
||||
<td>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>
|
||||
@@ -385,19 +369,11 @@
|
||||
</td>
|
||||
<td><code>/api/about</code></td>
|
||||
<td>
|
||||
<span
|
||||
class="badge"
|
||||
style="background-color: var(--primary-color)"
|
||||
>GET</span
|
||||
>
|
||||
<span class="badge" style="background-color: var(--primary-color)">GET</span>
|
||||
</td>
|
||||
<td>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>
|
||||
@@ -414,19 +390,11 @@
|
||||
</td>
|
||||
<td><code>/api/about-us</code></td>
|
||||
<td>
|
||||
<span
|
||||
class="badge"
|
||||
style="background-color: var(--primary-color)"
|
||||
>GET</span
|
||||
>
|
||||
<span class="badge" style="background-color: var(--primary-color)">GET</span>
|
||||
</td>
|
||||
<td>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>
|
||||
@@ -443,19 +411,11 @@
|
||||
</td>
|
||||
<td><code>/api/faq</code></td>
|
||||
<td>
|
||||
<span
|
||||
class="badge"
|
||||
style="background-color: var(--primary-color)"
|
||||
>GET</span
|
||||
>
|
||||
<span class="badge" style="background-color: var(--primary-color)">GET</span>
|
||||
</td>
|
||||
<td>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>
|
||||
@@ -472,19 +432,11 @@
|
||||
</td>
|
||||
<td><code>/api/terms</code></td>
|
||||
<td>
|
||||
<span
|
||||
class="badge"
|
||||
style="background-color: var(--primary-color)"
|
||||
>GET</span
|
||||
>
|
||||
<span class="badge" style="background-color: var(--primary-color)">GET</span>
|
||||
</td>
|
||||
<td>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>
|
||||
@@ -501,19 +453,11 @@
|
||||
</td>
|
||||
<td><code>/api/travel</code></td>
|
||||
<td>
|
||||
<span
|
||||
class="badge"
|
||||
style="background-color: var(--primary-color)"
|
||||
>GET</span
|
||||
>
|
||||
<span class="badge" style="background-color: var(--primary-color)">GET</span>
|
||||
</td>
|
||||
<td>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>
|
||||
@@ -530,19 +474,11 @@
|
||||
</td>
|
||||
<td><code>/api/safety</code></td>
|
||||
<td>
|
||||
<span
|
||||
class="badge"
|
||||
style="background-color: var(--primary-color)"
|
||||
>GET</span
|
||||
>
|
||||
<span class="badge" style="background-color: var(--primary-color)">GET</span>
|
||||
</td>
|
||||
<td>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>
|
||||
@@ -559,19 +495,11 @@
|
||||
</td>
|
||||
<td><code>/api/camp-location</code></td>
|
||||
<td>
|
||||
<span
|
||||
class="badge"
|
||||
style="background-color: var(--primary-color)"
|
||||
>GET</span
|
||||
>
|
||||
<span class="badge" style="background-color: var(--primary-color)">GET</span>
|
||||
</td>
|
||||
<td>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>
|
||||
@@ -589,19 +517,11 @@
|
||||
</td>
|
||||
<td><code>/api/menu-tree</code></td>
|
||||
<td>
|
||||
<span
|
||||
class="badge"
|
||||
style="background-color: var(--primary-color)"
|
||||
>GET</span
|
||||
>
|
||||
<span class="badge" style="background-color: var(--primary-color)">GET</span>
|
||||
</td>
|
||||
<td>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>
|
||||
@@ -618,19 +538,11 @@
|
||||
</td>
|
||||
<td><code>/api/contact</code></td>
|
||||
<td>
|
||||
<span
|
||||
class="badge"
|
||||
style="background-color: var(--primary-color)"
|
||||
>GET</span
|
||||
>
|
||||
<span class="badge" style="background-color: var(--primary-color)">GET</span>
|
||||
</td>
|
||||
<td>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>
|
||||
@@ -647,19 +559,11 @@
|
||||
</td>
|
||||
<td><code>/api/blog</code></td>
|
||||
<td>
|
||||
<span
|
||||
class="badge"
|
||||
style="background-color: var(--primary-color)"
|
||||
>GET</span
|
||||
>
|
||||
<span class="badge" style="background-color: var(--primary-color)">GET</span>
|
||||
</td>
|
||||
<td>API to get blog posts</td>
|
||||
<td>
|
||||
<a
|
||||
href="/api/blog"
|
||||
class="btn btn-sm btn-outline-primary"
|
||||
target="_blank"
|
||||
>
|
||||
<a href="/api/blog" class="btn btn-sm btn-outline-primary" target="_blank">
|
||||
<i class="fas fa-external-link-alt me-1"></i>View
|
||||
</a>
|
||||
</td>
|
||||
@@ -672,13 +576,10 @@
|
||||
|
||||
<!-- System Info -->
|
||||
<div class="card">
|
||||
<div
|
||||
class="card-header"
|
||||
style="
|
||||
<div class="card-header" style="
|
||||
background: linear-gradient(135deg, #0048b4, #0028b4);
|
||||
color: white;
|
||||
"
|
||||
>
|
||||
">
|
||||
<h5 class="mb-0">System Information</h5>
|
||||
</div>
|
||||
<div class="card-body" style="background-color: #f8faf8">
|
||||
@@ -689,52 +590,49 @@
|
||||
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>
|
||||
<div class="text-muted small">Version</div>
|
||||
<div class="fw-bold" style="color: var(--primary-color)">
|
||||
CMS.HAILearning v1.0.0
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
<div class="rounded-circle d-flex align-items-center justify-content-center me-3"
|
||||
style="width: 40px; height: 40px; background-color: rgba(184, 183, 106, 0.1);">
|
||||
<i class="fas fa-user" style="color: var(--primary-color);"></i>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-muted small">Logged in as</div>
|
||||
<div class="fw-bold" style="color: var(--primary-color);">
|
||||
<%= user.username %>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-muted small">Version</div>
|
||||
<div class="fw-bold" style="color: var(--primary-color)">
|
||||
CMS.HAILearning v1.0.0
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="alert mt-3 mb-0"
|
||||
style="background-color: rgba(184, 183, 106, 0.05); border-left: 4px solid var(--primary-color);" role="alert">
|
||||
<div class="d-flex">
|
||||
<div class="me-3">
|
||||
<i
|
||||
class="fas fa-lightbulb fa-lg"
|
||||
style="color: var(--primary-color)"
|
||||
></i>
|
||||
<div class="col-md-6">
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
<div class="rounded-circle d-flex align-items-center justify-content-center me-3"
|
||||
style="width: 40px; height: 40px; background-color: rgba(184, 183, 106, 0.1);">
|
||||
<i class="fas fa-user" style="color: var(--primary-color);"></i>
|
||||
</div>
|
||||
<div>
|
||||
<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 class="text-muted small">Logged in as</div>
|
||||
<div class="fw-bold" style="color: var(--primary-color);">
|
||||
<%= user.username %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="alert mt-3 mb-0"
|
||||
style="background-color: rgba(184, 183, 106, 0.05); border-left: 4px solid var(--primary-color);" role="alert">
|
||||
<div class="d-flex">
|
||||
<div class="me-3">
|
||||
<i class="fas fa-lightbulb fa-lg" style="color: var(--primary-color)"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h6 class="mb-1" style="color: var(--primary-color)">Quick Tip</h6>
|
||||
<p class="mb-0 text-muted">
|
||||
Click the Edit button to make changes to your data.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.page-title {
|
||||
@@ -773,4 +671,4 @@
|
||||
background-color: var(--primary-color) !important;
|
||||
color: white;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
216
views/admin/home/faq/index.ejs
Normal file
216
views/admin/home/faq/index.ejs
Normal file
@@ -0,0 +1,216 @@
|
||||
<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 FAQ section content</p>
|
||||
</div>
|
||||
<div>
|
||||
<a href="<%= frontendUrl %>/" class="btn btn-outline-primary" target="_blank">
|
||||
<i class="fas fa-external-link-alt me-2"></i>View Homepage
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<form method="POST" class="content-with-fixed-buttons" id="faqForm" action="/admin/faq/update">
|
||||
<!-- Hidden input for items JSON data -->
|
||||
<input type="hidden" name="items" id="itemsJson">
|
||||
|
||||
<div class="card shadow-sm border-0 mb-4">
|
||||
<div class="card-header bg-white border-bottom">
|
||||
<h5 class="mb-0"><i class="fas fa-question-circle me-2"></i>FAQ Settings</h5>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<!-- Header Section -->
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Heading</label>
|
||||
<input type="text" class="form-control" name="heading" id="heading"
|
||||
value="<%= data.heading || '' %>">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Subheading</label>
|
||||
<input type="text" class="form-control" name="subheading" id="subheading"
|
||||
value="<%= data.subheading || '' %>">
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-medium">Description</label>
|
||||
<textarea class="form-control" name="description" id="description"
|
||||
rows="2"><%= data.description || '' %></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- CTA Button Section -->
|
||||
<h6 class="fw-medium mb-3">CTA Button</h6>
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Button Label</label>
|
||||
<input type="text" class="form-control" name="ctaLabel"
|
||||
value="<%= data.ctaButton && data.ctaButton.label ? data.ctaButton.label : '' %>">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Button Link</label>
|
||||
<input type="text" class="form-control" name="ctaHref"
|
||||
value="<%= data.ctaButton && data.ctaButton.href ? data.ctaButton.href : '' %>">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="my-4">
|
||||
|
||||
<!-- FAQ Items -->
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h6 class="fw-medium mb-0">Frequently Asked Questions</h6>
|
||||
<button type="button" class="btn btn-primary btn-sm" onclick="addFaqItem()">
|
||||
<i class="fas fa-plus"></i> Add Question
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="faqItemsContainer">
|
||||
<% if (data.items && data.items.length> 0) { %>
|
||||
<% data.items.forEach((item, index)=> { %>
|
||||
<div class="card mb-3 faq-item">
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-12">
|
||||
<label class="form-label">Question</label>
|
||||
<input type="text" class="form-control"
|
||||
name="itemQuestion_<%= index %>"
|
||||
value="<%= item.question || '' %>">
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label">Answer</label>
|
||||
<textarea class="form-control" name="itemAnswer_<%= index %>"
|
||||
rows="3"><%= item.answer || '' %></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="btn btn-outline-danger btn-sm mt-3"
|
||||
onclick="removeFaqItem(this)">
|
||||
<i class="fas fa-trash me-2"></i>Remove
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<% }); %>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Fixed Bottom Buttons -->
|
||||
<div class="fixed-bottom-buttons">
|
||||
<button type="reset" class="btn btn-secondary" onclick="resetForm()">
|
||||
<i class="fas fa-undo me-2"></i>Reset
|
||||
</button>
|
||||
<button type="submit" class="btn btn-primary" id="submitBtn">
|
||||
<i class="fas fa-save me-2"></i>Save Changes
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="application/json" id="faqDataJson"><%- JSON.stringify(data) %></script>
|
||||
|
||||
<script>
|
||||
let originalFormData = null;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
try {
|
||||
var jsonScript = document.getElementById('faqDataJson');
|
||||
originalFormData = JSON.parse(jsonScript.textContent);
|
||||
} catch (e) {
|
||||
console.error('Error parsing originalFormData:', e);
|
||||
originalFormData = { items: [] };
|
||||
}
|
||||
|
||||
initializeFormHandlers();
|
||||
});
|
||||
|
||||
function initializeFormHandlers() {
|
||||
const form = document.getElementById('faqForm');
|
||||
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 {
|
||||
updateItemsJson();
|
||||
this.submit();
|
||||
} catch (error) {
|
||||
console.error('Error updating data:', error);
|
||||
alert('Failed to process form data. Please try again.');
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.innerHTML = '<i class="fas fa-save me-2"></i>Save Changes';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function updateItemsJson() {
|
||||
const items = [];
|
||||
document.querySelectorAll('.faq-item').forEach((item, index) => {
|
||||
const question = item.querySelector(`[name="itemQuestion_${index}"]`)?.value ||
|
||||
item.querySelector('[name^="itemQuestion_"]')?.value || '';
|
||||
const answer = item.querySelector(`[name="itemAnswer_${index}"]`)?.value ||
|
||||
item.querySelector('[name^="itemAnswer_"]')?.value || '';
|
||||
|
||||
items.push({ question, answer });
|
||||
});
|
||||
|
||||
document.getElementById('itemsJson').value = JSON.stringify(items);
|
||||
}
|
||||
|
||||
function addFaqItem() {
|
||||
const container = document.getElementById('faqItemsContainer');
|
||||
const index = container.querySelectorAll('.faq-item').length;
|
||||
|
||||
const html = `
|
||||
<div class="card mb-3 faq-item">
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-12">
|
||||
<label class="form-label">Question</label>
|
||||
<input type="text" class="form-control" name="itemQuestion_${index}" value="">
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label class="form-label">Answer</label>
|
||||
<textarea class="form-control" name="itemAnswer_${index}" rows="3"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="btn btn-outline-danger btn-sm mt-3"
|
||||
onclick="removeFaqItem(this)">
|
||||
<i class="fas fa-trash me-2"></i>Remove
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
container.insertAdjacentHTML('beforeend', html);
|
||||
}
|
||||
|
||||
function removeFaqItem(button) {
|
||||
if (confirm('Are you sure you want to remove this question?')) {
|
||||
button.closest('.faq-item').remove();
|
||||
reindexItems();
|
||||
}
|
||||
}
|
||||
|
||||
function reindexItems() {
|
||||
document.querySelectorAll('.faq-item').forEach((item, index) => {
|
||||
item.querySelectorAll('[name^="item"]').forEach(input => {
|
||||
const baseName = input.name.replace(/_\d+$/, '');
|
||||
input.name = `${baseName}_${index}`;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
if (confirm('Are you sure you want to reset all changes?')) {
|
||||
location.reload();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
438
views/admin/home/testimonial/index.ejs
Normal file
438
views/admin/home/testimonial/index.ejs
Normal file
@@ -0,0 +1,438 @@
|
||||
<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 testimonials section</p>
|
||||
</div>
|
||||
<div>
|
||||
<a href="<%= frontendUrl %>/" class="btn btn-outline-primary" target="_blank">
|
||||
<i class="fas fa-external-link-alt me-2"></i>View Homepage
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<form method="POST" class="content-with-fixed-buttons" id="testimonialForm"
|
||||
action=" /update">
|
||||
<!-- Hidden input for items JSON data -->
|
||||
<input type="hidden" name="items" id="itemsJson">
|
||||
|
||||
<div class="card shadow-sm border-0 mb-4">
|
||||
<div class="card-header bg-white border-bottom">
|
||||
<h5 class="mb-0"><i class="fas fa-quote-left me-2"></i>Testimonials Settings</h5>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<!-- Header Section -->
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Heading</label>
|
||||
<input type="text" class="form-control" name="heading" id="heading"
|
||||
value="<%= data.heading || '' %>">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Subheading</label>
|
||||
<input type="text" class="form-control" name="subheading" id="subheading"
|
||||
value="<%= data.subheading || '' %>">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Video Section -->
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Video URL</label>
|
||||
<input type="text" class="form-control" name="videoUrl" id="videoUrl"
|
||||
value="<%= data.videoUrl || '' %>"
|
||||
placeholder="https://www.youtube.com/watch?v=...">
|
||||
<small class="text-muted">YouTube video URL</small>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-medium">Video Thumbnail</label>
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" name="videoThumbnail" id="videoThumbnail"
|
||||
value="<%= data.videoThumbnail || '' %>">
|
||||
<button type="button" class="btn btn-outline-primary btn-upload-image"
|
||||
data-target-input="videoThumbnail" data-image-type="testimonial">
|
||||
<i class="fas fa-upload me-1"></i>Upload
|
||||
</button>
|
||||
</div>
|
||||
<% if (data.videoThumbnail) { %>
|
||||
<img src="<%= data.videoThumbnail %>" class="img-thumbnail mt-2"
|
||||
style="max-height: 100px;" alt="Thumbnail preview">
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="my-4">
|
||||
|
||||
<!-- Testimonial Items -->
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h6 class="fw-medium mb-0">Testimonial Items</h6>
|
||||
<button type="button" class="btn btn-primary btn-sm" onclick="addTestimonialItem()">
|
||||
<i class="fas fa-plus"></i> Add Testimonial
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="testimonialItemsContainer">
|
||||
<% if (data.items && data.items.length> 0) { %>
|
||||
<% data.items.forEach((item, index)=> { %>
|
||||
<div class="card mb-3 testimonial-item">
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Name</label>
|
||||
<input type="text" class="form-control" name="itemName_<%= index %>"
|
||||
value="<%= item.name || '' %>">
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Role</label>
|
||||
<input type="text" class="form-control" name="itemRole_<%= index %>"
|
||||
value="<%= item.role || '' %>">
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Country</label>
|
||||
<input type="text" class="form-control"
|
||||
name="itemCountry_<%= index %>"
|
||||
value="<%= item.country || '' %>">
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Rating (1-5)</label>
|
||||
<select class="form-select" name="itemRating_<%= index %>">
|
||||
<% for (let i=1; i <=5; i++) { %>
|
||||
<option value="<%= i %>" <%=item.rating===i ? 'selected'
|
||||
: '' %>>
|
||||
<%= i %> Star<%= i> 1 ? 's' : '' %>
|
||||
</option>
|
||||
<% } %>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<label class="form-label">Comment</label>
|
||||
<textarea class="form-control" name="itemComment_<%= index %>"
|
||||
rows="3"><%= item.comment || '' %></textarea>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Avatar</label>
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control"
|
||||
name="itemAvatar_<%= index %>"
|
||||
value="<%= item.avatar || '' %>">
|
||||
<button type="button"
|
||||
class="btn btn-outline-primary btn-upload-image"
|
||||
data-target-input="itemAvatar_<%= index %>"
|
||||
data-image-type="testimonial">
|
||||
<i class="fas fa-upload me-1"></i>Upload
|
||||
</button>
|
||||
</div>
|
||||
<% if (item.avatar) { %>
|
||||
<img src="<%= item.avatar %>"
|
||||
class="img-thumbnail mt-2 avatar-preview"
|
||||
style="max-height: 60px; max-width: 60px; border-radius: 50%;"
|
||||
alt="Avatar">
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="btn btn-outline-danger btn-sm mt-3"
|
||||
onclick="removeTestimonialItem(this)">
|
||||
<i class="fas fa-trash me-2"></i>Remove
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<% }); %>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Fixed Bottom Buttons -->
|
||||
<div class="fixed-bottom-buttons">
|
||||
<button type="reset" class="btn btn-secondary" onclick="resetForm()">
|
||||
<i class="fas fa-undo me-2"></i>Reset
|
||||
</button>
|
||||
<button type="submit" class="btn btn-primary" id="submitBtn">
|
||||
<i class="fas fa-save me-2"></i>Save Changes
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="application/json" id="testimonialDataJson"><%- JSON.stringify(data) %></script>
|
||||
|
||||
<script>
|
||||
let originalFormData = null;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
try {
|
||||
var jsonScript = document.getElementById('testimonialDataJson');
|
||||
originalFormData = JSON.parse(jsonScript.textContent);
|
||||
} catch (e) {
|
||||
console.error('Error parsing originalFormData:', e);
|
||||
originalFormData = { items: [] };
|
||||
}
|
||||
|
||||
initializeFormHandlers();
|
||||
});
|
||||
|
||||
function initializeFormHandlers() {
|
||||
const form = document.getElementById('testimonialForm');
|
||||
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 {
|
||||
updateItemsJson();
|
||||
this.submit();
|
||||
} catch (error) {
|
||||
console.error('Error updating data:', error);
|
||||
alert('Failed to process form data. Please try again.');
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.innerHTML = '<i class="fas fa-save me-2"></i>Save Changes';
|
||||
}
|
||||
});
|
||||
|
||||
document.querySelectorAll('.btn-upload-image').forEach(button => {
|
||||
button.addEventListener('click', function () {
|
||||
const targetInput = this.dataset.targetInput;
|
||||
const imageType = this.dataset.imageType;
|
||||
openImageUploader(targetInput, imageType);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function updateItemsJson() {
|
||||
const items = [];
|
||||
document.querySelectorAll('.testimonial-item').forEach((item, index) => {
|
||||
const name = item.querySelector(`[name="itemName_${index}"]`)?.value ||
|
||||
item.querySelector('[name^="itemName_"]')?.value || '';
|
||||
const role = item.querySelector(`[name="itemRole_${index}"]`)?.value ||
|
||||
item.querySelector('[name^="itemRole_"]')?.value || '';
|
||||
const country = item.querySelector(`[name="itemCountry_${index}"]`)?.value ||
|
||||
item.querySelector('[name^="itemCountry_"]')?.value || '';
|
||||
const rating = parseInt(item.querySelector(`[name="itemRating_${index}"]`)?.value ||
|
||||
item.querySelector('[name^="itemRating_"]')?.value || '5');
|
||||
const comment = item.querySelector(`[name="itemComment_${index}"]`)?.value ||
|
||||
item.querySelector('[name^="itemComment_"]')?.value || '';
|
||||
const avatar = item.querySelector(`[name="itemAvatar_${index}"]`)?.value ||
|
||||
item.querySelector('[name^="itemAvatar_"]')?.value || '';
|
||||
|
||||
items.push({ name, role, country, rating, comment, avatar });
|
||||
});
|
||||
|
||||
document.getElementById('itemsJson').value = JSON.stringify(items);
|
||||
}
|
||||
|
||||
function addTestimonialItem() {
|
||||
const container = document.getElementById('testimonialItemsContainer');
|
||||
const index = container.querySelectorAll('.testimonial-item').length;
|
||||
|
||||
const html = `
|
||||
<div class="card mb-3 testimonial-item">
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Name</label>
|
||||
<input type="text" class="form-control" name="itemName_${index}" value="">
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Role</label>
|
||||
<input type="text" class="form-control" name="itemRole_${index}" value="">
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Country</label>
|
||||
<input type="text" class="form-control" name="itemCountry_${index}" value="">
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label">Rating (1-5)</label>
|
||||
<select class="form-select" name="itemRating_${index}">
|
||||
<option value="1">1 Star</option>
|
||||
<option value="2">2 Stars</option>
|
||||
<option value="3">3 Stars</option>
|
||||
<option value="4">4 Stars</option>
|
||||
<option value="5" selected>5 Stars</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<label class="form-label">Comment</label>
|
||||
<textarea class="form-control" name="itemComment_${index}" rows="3"></textarea>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Avatar</label>
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" name="itemAvatar_${index}" value="">
|
||||
<button type="button" class="btn btn-outline-primary btn-upload-image"
|
||||
data-target-input="itemAvatar_${index}" data-image-type="testimonial">
|
||||
<i class="fas fa-upload me-1"></i>Upload
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="btn btn-outline-danger btn-sm mt-3"
|
||||
onclick="removeTestimonialItem(this)">
|
||||
<i class="fas fa-trash me-2"></i>Remove
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
container.insertAdjacentHTML('beforeend', html);
|
||||
|
||||
// Re-attach upload handlers for new item
|
||||
container.querySelector('.testimonial-item:last-child .btn-upload-image').addEventListener('click', function () {
|
||||
const targetInput = this.dataset.targetInput;
|
||||
const imageType = this.dataset.imageType;
|
||||
openImageUploader(targetInput, imageType);
|
||||
});
|
||||
}
|
||||
|
||||
function removeTestimonialItem(button) {
|
||||
if (confirm('Are you sure you want to remove this testimonial?')) {
|
||||
button.closest('.testimonial-item').remove();
|
||||
reindexItems();
|
||||
}
|
||||
}
|
||||
|
||||
function reindexItems() {
|
||||
document.querySelectorAll('.testimonial-item').forEach((item, index) => {
|
||||
item.querySelectorAll('[name^="item"]').forEach(input => {
|
||||
const baseName = input.name.replace(/_\d+$/, '');
|
||||
input.name = `${baseName}_${index}`;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
if (confirm('Are you sure you want to reset all changes?')) {
|
||||
location.reload();
|
||||
}
|
||||
}
|
||||
|
||||
function openImageUploader(targetInput, imageType) {
|
||||
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;
|
||||
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('image', file);
|
||||
|
||||
const uploadBtn = document.querySelector(`[data-target-input="${targetInput}"]`);
|
||||
const originalBtnHtml = uploadBtn ? uploadBtn.innerHTML : 'Upload';
|
||||
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=${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 preview if exists
|
||||
const previewUrl = (result.path && (result.path.startsWith('http://') || result.path.startsWith('https://')))
|
||||
? result.path
|
||||
: (window.location.origin + result.path);
|
||||
|
||||
const parent = input.closest('.col-md-4') || input.closest('div');
|
||||
let previewImg = parent.querySelector('.avatar-preview, .img-thumbnail');
|
||||
|
||||
if (previewImg) {
|
||||
previewImg.src = previewUrl;
|
||||
} else {
|
||||
const img = document.createElement('img');
|
||||
img.src = previewUrl;
|
||||
img.className = 'img-thumbnail mt-2';
|
||||
img.style.maxHeight = '60px';
|
||||
img.alt = 'Preview';
|
||||
input.closest('.input-group').after(img);
|
||||
}
|
||||
|
||||
if (uploadBtn) {
|
||||
uploadBtn.disabled = false;
|
||||
uploadBtn.innerHTML = originalBtnHtml;
|
||||
}
|
||||
|
||||
showToast('Success', 'Image uploaded successfully', 'success');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Upload error:', error);
|
||||
showToast('Error', 'Failed to upload image: ' + error.message, 'error');
|
||||
|
||||
const uploadBtn = document.querySelector(`[data-target-input="${targetInput}"]`);
|
||||
if (uploadBtn) {
|
||||
uploadBtn.disabled = false;
|
||||
uploadBtn.innerHTML = '<i class="fas fa-upload me-1"></i>Upload';
|
||||
}
|
||||
} finally {
|
||||
document.body.removeChild(fileInput);
|
||||
}
|
||||
};
|
||||
|
||||
fileInput.click();
|
||||
}
|
||||
|
||||
function showToast(title, message, type = 'info') {
|
||||
const toast = document.createElement('div');
|
||||
toast.className = `toast align-items-center text-white bg-${type === 'error' ? 'danger' : type} border-0`;
|
||||
toast.setAttribute('role', 'alert');
|
||||
toast.setAttribute('aria-live', 'assertive');
|
||||
toast.setAttribute('aria-atomic', 'true');
|
||||
|
||||
toast.innerHTML = `
|
||||
<div class="d-flex">
|
||||
<div class="toast-body">
|
||||
<strong>${title}:</strong> ${message}
|
||||
</div>
|
||||
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
let container = document.querySelector('.toast-container');
|
||||
if (!container) {
|
||||
container = document.createElement('div');
|
||||
container.className = 'toast-container position-fixed top-0 end-0 p-3';
|
||||
document.body.appendChild(container);
|
||||
}
|
||||
container.appendChild(toast);
|
||||
|
||||
const bsToast = new bootstrap.Toast(toast, {
|
||||
animation: true,
|
||||
autohide: true,
|
||||
delay: 3000
|
||||
});
|
||||
bsToast.show();
|
||||
|
||||
toast.addEventListener('hidden.bs.toast', () => {
|
||||
toast.remove();
|
||||
});
|
||||
}
|
||||
</script>
|
||||
206
views/admin/home/videoGallery/index.ejs
Normal file
206
views/admin/home/videoGallery/index.ejs
Normal file
@@ -0,0 +1,206 @@
|
||||
<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 video gallery section on homepage</p>
|
||||
</div>
|
||||
<div>
|
||||
<a href="<%= frontendUrl %>/" class="btn btn-outline-primary" target="_blank">
|
||||
<i class="fas fa-external-link-alt me-2"></i>View Homepage
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<form method="POST" class="content-with-fixed-buttons" id="videoGalleryForm"
|
||||
action="/admin/video-gallery/update">
|
||||
<div class="card shadow-sm border-0 mb-4">
|
||||
<div class="card-header bg-white border-bottom">
|
||||
<h5 class="mb-0"><i class="fas fa-video me-2"></i>Video Gallery Settings</h5>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-medium">Heading</label>
|
||||
<input type="text" class="form-control" name="heading" id="heading"
|
||||
value="<%= data.heading || '' %>" placeholder="e.g., Explore World">
|
||||
</div>
|
||||
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-medium">Video URL</label>
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" name="videoUrl" id="videoUrl"
|
||||
value="<%= data.videoUrl || '' %>" placeholder="/assets/video/sample.mp4">
|
||||
<button type="button" class="btn btn-outline-primary btn-upload-video"
|
||||
data-target-input="videoUrl">
|
||||
<i class="fas fa-upload me-1"></i>Upload Video
|
||||
</button>
|
||||
</div>
|
||||
<small class="text-muted">MP4 video file path or URL</small>
|
||||
<% if (data.videoUrl) { %>
|
||||
<div class="mt-2">
|
||||
<video width="100%" height="200" controls style="border-radius: 8px;">
|
||||
<source src="<%= data.videoUrl %>" type="video/mp4">
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Fixed Bottom Buttons -->
|
||||
<div class="fixed-bottom-buttons">
|
||||
<button type="reset" class="btn btn-secondary" onclick="resetForm()">
|
||||
<i class="fas fa-undo me-2"></i>Reset
|
||||
</button>
|
||||
<button type="submit" class="btn btn-primary" id="submitBtn">
|
||||
<i class="fas fa-save me-2"></i>Save Changes
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
initializeFormHandlers();
|
||||
});
|
||||
|
||||
function initializeFormHandlers() {
|
||||
document.querySelectorAll('.btn-upload-image').forEach(button => {
|
||||
button.addEventListener('click', function () {
|
||||
const targetInput = this.dataset.targetInput;
|
||||
const imageType = this.dataset.imageType;
|
||||
openImageUploader(targetInput, imageType);
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll('.btn-upload-video').forEach(button => {
|
||||
button.addEventListener('click', function () {
|
||||
const targetInput = this.dataset.targetInput;
|
||||
openVideoUploader(targetInput);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
if (confirm('Are you sure you want to reset all changes?')) {
|
||||
location.reload();
|
||||
}
|
||||
}
|
||||
|
||||
function openImageUploader(targetInput, imageType) {
|
||||
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;
|
||||
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('image', file);
|
||||
|
||||
const uploadBtn = document.querySelector(`[data-target-input="${targetInput}"]`);
|
||||
const originalBtnHtml = uploadBtn ? uploadBtn.innerHTML : 'Upload';
|
||||
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=${imageType}`, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Upload failed');
|
||||
}
|
||||
|
||||
document.getElementById(targetInput).value = result.path;
|
||||
|
||||
if (uploadBtn) {
|
||||
uploadBtn.disabled = false;
|
||||
uploadBtn.innerHTML = originalBtnHtml;
|
||||
}
|
||||
|
||||
alert('Image uploaded successfully!');
|
||||
location.reload();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Upload error:', error);
|
||||
alert('Failed to upload image: ' + error.message);
|
||||
} finally {
|
||||
document.body.removeChild(fileInput);
|
||||
}
|
||||
};
|
||||
|
||||
fileInput.click();
|
||||
}
|
||||
|
||||
function openVideoUploader(targetInput) {
|
||||
const fileInput = document.createElement('input');
|
||||
fileInput.type = 'file';
|
||||
fileInput.accept = 'video/mp4,video/webm,video/ogg';
|
||||
fileInput.style.display = 'none';
|
||||
document.body.appendChild(fileInput);
|
||||
|
||||
fileInput.onchange = async function (e) {
|
||||
const file = e.target.files[0];
|
||||
if (!file) return;
|
||||
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('video', file);
|
||||
|
||||
const uploadBtn = document.querySelector(`[data-target-input="${targetInput}"]`);
|
||||
const originalBtnHtml = uploadBtn ? uploadBtn.innerHTML : 'Upload';
|
||||
if (uploadBtn) {
|
||||
uploadBtn.disabled = true;
|
||||
uploadBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Uploading...';
|
||||
}
|
||||
|
||||
const response = await fetch('/admin/upload/video', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Upload failed');
|
||||
}
|
||||
|
||||
document.getElementById(targetInput).value = result.path;
|
||||
|
||||
if (uploadBtn) {
|
||||
uploadBtn.disabled = false;
|
||||
uploadBtn.innerHTML = originalBtnHtml;
|
||||
}
|
||||
|
||||
alert('Video uploaded successfully!');
|
||||
location.reload();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Upload error:', error);
|
||||
alert('Failed to upload video: ' + error.message);
|
||||
} finally {
|
||||
document.body.removeChild(fileInput);
|
||||
}
|
||||
};
|
||||
|
||||
fileInput.click();
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user