AdminLTE3

This commit is contained in:
2025-10-26 22:14:47 +09:00
parent 291fc63a4c
commit 9974811a3e
226 changed files with 88284 additions and 3406 deletions

View File

@@ -0,0 +1,362 @@
<!-- Content Header (Page header) -->
<div class="content-header">
<div class="container-fluid">
<div class="row mb-2">
<div class="col-sm-6">
<h1><i class="fas fa-briefcase mr-2"></i>Управление портфолио</h1>
</div>
<div class="col-sm-6">
<ol class="breadcrumb float-sm-right">
<li class="breadcrumb-item"><a href="/admin/dashboard">Админ</a></li>
<li class="breadcrumb-item active">Портфолио</li>
</ol>
</div>
</div>
</div>
</div>
<!-- Main content -->
<section class="content">
<div class="container-fluid">
<!-- Search and Filter Bar -->
<div class="row mb-3">
<div class="col-12">
<div class="card">
<div class="card-body">
<div class="row">
<div class="col-md-4">
<div class="input-group">
<input type="text" id="searchInput" class="form-control" placeholder="Поиск проектов...">
<div class="input-group-append">
<button type="button" class="btn btn-outline-secondary" onclick="searchProjects()">
<i class="fas fa-search"></i>
</button>
</div>
</div>
</div>
<div class="col-md-3">
<select id="categoryFilter" class="form-control" onchange="filterByCategory()">
<option value="">Все категории</option>
<option value="web-development">Веб-разработка</option>
<option value="mobile-app">Мобильное приложение</option>
<option value="ui-ux-design">UI/UX дизайн</option>
<option value="e-commerce">E-commerce</option>
<option value="enterprise">Корпоративное</option>
<option value="other">Другое</option>
</select>
</div>
<div class="col-md-2">
<select id="statusFilter" class="form-control" onchange="filterByStatus()">
<option value="">Все статусы</option>
<option value="published">Опубликовано</option>
<option value="draft">Черновик</option>
<option value="featured">Рекомендуемое</option>
</select>
</div>
<div class="col-md-3 text-right">
<a href="/admin/portfolio/add" class="btn btn-primary">
<i class="fas fa-plus mr-1"></i> Добавить проект
</a>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Portfolio Table -->
<div class="card">
<div class="card-header">
<h3 class="card-title">Список проектов</h3>
<div class="card-tools">
<span class="badge badge-secondary" id="projectCount">
Всего: <%= portfolio ? portfolio.length : 0 %>
</span>
</div>
</div>
<div class="card-body table-responsive p-0">
<table class="table table-hover text-nowrap">
<thead>
<tr>
<th style="width: 60px;">Фото</th>
<th>Название</th>
<th>Категория</th>
<th>Статус</th>
<th>Технологии</th>
<th>Создано</th>
<th style="width: 150px;">Действия</th>
</tr>
</thead>
<tbody>
<% if (portfolio && portfolio.length > 0) { %>
<% portfolio.forEach(item => { %>
<tr class="portfolio-item" data-category="<%= item.category %>" data-title="<%= item.title.toLowerCase() %>" data-status="<%= item.isPublished ? 'published' : 'draft' %><%= item.featured ? ' featured' : '' %>">
<td>
<% if (item.images && item.images.length > 0) { %>
<img src="<%= item.images[0].url %>" alt="<%= item.title %>" class="img-circle img-size-32">
<% } else { %>
<div class="img-circle bg-secondary d-flex align-items-center justify-content-center" style="width: 32px; height: 32px;">
<i class="fas fa-image text-white"></i>
</div>
<% } %>
</td>
<td>
<div>
<strong><%= item.title %></strong>
<% if (item.featured) { %>
<span class="badge badge-warning ml-1">
<i class="fas fa-star"></i>
</span>
<% } %>
</div>
<small class="text-muted"><%= item.shortDescription || 'Описание не указано' %></small>
</td>
<td>
<span class="badge badge-info">
<%= item.category.replace('-', ' ') %>
</span>
</td>
<td>
<% if (item.isPublished) { %>
<span class="badge badge-success">
<i class="fas fa-check-circle mr-1"></i>Опубликовано
</span>
<% } else { %>
<span class="badge badge-secondary">
<i class="fas fa-clock mr-1"></i>Черновик
</span>
<% } %>
</td>
<td>
<% if (item.technologies && item.technologies.length > 0) { %>
<% item.technologies.slice(0, 2).forEach(tech => { %>
<span class="badge badge-light mr-1"><%= tech %></span>
<% }) %>
<% if (item.technologies.length > 2) { %>
<span class="text-muted">+<%= item.technologies.length - 2 %></span>
<% } %>
<% } else { %>
<span class="text-muted">—</span>
<% } %>
</td>
<td>
<small class="text-muted">
<%= new Date(item.createdAt).toLocaleDateString('ru-RU') %>
</small>
</td>
<td>
<div class="btn-group btn-group-sm" role="group">
<% if (item.isPublished) { %>
<a href="/portfolio/<%= item.id %>" target="_blank" class="btn btn-info btn-sm" title="Просмотр">
<i class="fas fa-external-link-alt"></i>
</a>
<% } %>
<button type="button" class="btn btn-<%= item.isPublished ? 'warning' : 'success' %> btn-sm"
onclick="togglePublish('<%= item.id %>', '<%= item.isPublished %>')"
title="<%= item.isPublished ? 'Скрыть' : 'Опубликовать' %>">
<i class="fas <%= item.isPublished ? 'fa-eye-slash' : 'fa-eye' %>"></i>
</button>
<a href="/admin/portfolio/edit/<%= item.id %>" class="btn btn-primary btn-sm" title="Редактировать">
<i class="fas fa-edit"></i>
</a>
<button type="button" class="btn btn-danger btn-sm"
onclick="deletePortfolio('<%= item.id %>', '<%= item.title %>')"
title="Удалить">
<i class="fas fa-trash"></i>
</button>
</div>
</td>
</tr>
<% }) %>
<% } else { %>
<tr>
<td colspan="7" class="text-center py-4">
<div class="text-muted">
<i class="fas fa-briefcase fa-3x mb-3"></i>
<p>Проекты не найдены</p>
<a href="/admin/portfolio/add" class="btn btn-primary">
Добавить первый проект
</a>
</div>
</td>
</tr>
<% } %>
</tbody>
</table>
</div>
<!-- Pagination -->
<% if (pagination && pagination.total > 1) { %>
<div class="card-footer clearfix">
<ul class="pagination pagination-sm m-0 float-right">
<% if (pagination.hasPrev) { %>
<li class="page-item">
<a class="page-link" href="?page=<%= pagination.current - 1 %>">«</a>
</li>
<% } %>
<% for (let i = 1; i <= pagination.total; i++) { %>
<li class="page-item <%= pagination.current === i ? 'active' : '' %>">
<a class="page-link" href="?page=<%= i %>"><%= i %></a>
</li>
<% } %>
<% if (pagination.hasNext) { %>
<li class="page-item">
<a class="page-link" href="?page=<%= pagination.current + 1 %>">»</a>
</li>
<% } %>
</ul>
</div>
<% } %>
</div>
</div>
</section>
<script>
function deletePortfolio(id, title) {
Swal.fire({
title: 'Удалить проект?',
text: `Вы уверены, что хотите удалить проект "${title}"?`,
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#d33',
cancelButtonColor: '#3085d6',
confirmButtonText: 'Да, удалить!',
cancelButtonText: 'Отмена'
}).then((result) => {
if (result.isConfirmed) {
fetch(`/api/admin/portfolio/${id}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
Swal.fire('Удалено!', 'Проект был удален.', 'success').then(() => {
location.reload();
});
} else {
Swal.fire('Ошибка!', data.message || 'Ошибка при удалении проекта', 'error');
}
})
.catch(error => {
console.error('Error:', error);
Swal.fire('Ошибка!', 'Произошла ошибка при удалении проекта', 'error');
});
}
});
}
function togglePublish(id, currentStatus) {
const isPublished = currentStatus === 'true';
fetch(`/api/admin/portfolio/${id}`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
isPublished: !isPublished
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
toastr.success(`Проект ${!isPublished ? 'опубликован' : 'скрыт'}`);
setTimeout(() => {
location.reload();
}, 1000);
} else {
toastr.error(data.message || 'Ошибка при изменении статуса');
}
})
.catch(error => {
console.error('Error:', error);
toastr.error('Произошла ошибка при изменении статуса');
});
}
function searchProjects() {
const searchTerm = document.getElementById('searchInput').value.toLowerCase();
const items = document.querySelectorAll('.portfolio-item');
let visibleCount = 0;
items.forEach(item => {
const title = item.dataset.title;
if (title.includes(searchTerm)) {
item.style.display = 'table-row';
visibleCount++;
} else {
item.style.display = 'none';
}
});
updateProjectCount(visibleCount, items.length);
}
function filterByCategory() {
const selectedCategory = document.getElementById('categoryFilter').value;
const selectedStatus = document.getElementById('statusFilter').value;
filterPortfolio(selectedCategory, selectedStatus);
}
function filterByStatus() {
const selectedCategory = document.getElementById('categoryFilter').value;
const selectedStatus = document.getElementById('statusFilter').value;
filterPortfolio(selectedCategory, selectedStatus);
}
function filterPortfolio(category, status) {
const items = document.querySelectorAll('.portfolio-item');
let visibleCount = 0;
items.forEach(item => {
const itemCategory = item.dataset.category;
const itemStatus = item.dataset.status;
let showItem = true;
if (category && itemCategory !== category) {
showItem = false;
}
if (status && !itemStatus.includes(status)) {
showItem = false;
}
if (showItem) {
item.style.display = 'table-row';
visibleCount++;
} else {
item.style.display = 'none';
}
});
updateProjectCount(visibleCount, items.length);
}
function updateProjectCount(visible, total) {
const countElement = document.getElementById('projectCount');
if (countElement) {
if (visible !== undefined) {
countElement.textContent = `Показано: ${visible} из ${total}`;
} else {
countElement.textContent = `Всего: ${total}`;
}
}
}
// Search on Enter key
document.getElementById('searchInput').addEventListener('keyup', function(event) {
if (event.key === 'Enter') {
searchProjects();
}
});
// Auto search on input
document.getElementById('searchInput').addEventListener('input', function() {
searchProjects();
});
</script>

View File

@@ -0,0 +1,362 @@
<!-- Content Header (Page header) -->
<div class="content-header">
<div class="container-fluid">
<div class="row mb-2">
<div class="col-sm-6">
<h1><i class="fas fa-briefcase mr-2"></i>Управление портфолио</h1>
</div>
<div class="col-sm-6">
<ol class="breadcrumb float-sm-right">
<li class="breadcrumb-item"><a href="/admin/dashboard">Админ</a></li>
<li class="breadcrumb-item active">Портфолио</li>
</ol>
</div>
</div>
</div>
</div>
<!-- Main content -->
<section class="content">
<div class="container-fluid">
<!-- Search and Filter Bar -->
<div class="row mb-3">
<div class="col-12">
<div class="card">
<div class="card-body">
<div class="row">
<div class="col-md-4">
<div class="input-group">
<input type="text" id="searchInput" class="form-control" placeholder="Поиск проектов...">
<div class="input-group-append">
<button type="button" class="btn btn-outline-secondary" onclick="searchProjects()">
<i class="fas fa-search"></i>
</button>
</div>
</div>
</div>
<div class="col-md-3">
<select id="categoryFilter" class="form-control" onchange="filterByCategory()">
<option value="">Все категории</option>
<option value="web-development">Веб-разработка</option>
<option value="mobile-app">Мобильное приложение</option>
<option value="ui-ux-design">UI/UX дизайн</option>
<option value="e-commerce">E-commerce</option>
<option value="enterprise">Корпоративное</option>
<option value="other">Другое</option>
</select>
</div>
<div class="col-md-2">
<select id="statusFilter" class="form-control" onchange="filterByStatus()">
<option value="">Все статусы</option>
<option value="published">Опубликовано</option>
<option value="draft">Черновик</option>
<option value="featured">Рекомендуемое</option>
</select>
</div>
<div class="col-md-3 text-right">
<a href="/admin/portfolio/add" class="btn btn-primary">
<i class="fas fa-plus mr-1"></i> Добавить проект
</a>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Portfolio Table -->
<div class="card">
<div class="card-header">
<h3 class="card-title">Список проектов</h3>
<div class="card-tools">
<span class="badge badge-secondary" id="projectCount">
Всего: <%= portfolio ? portfolio.length : 0 %>
</span>
</div>
</div>
<div class="card-body table-responsive p-0">
<table class="table table-hover text-nowrap">
<thead>
<tr>
<th style="width: 60px;">Фото</th>
<th>Название</th>
<th>Категория</th>
<th>Статус</th>
<th>Технологии</th>
<th>Создано</th>
<th style="width: 150px;">Действия</th>
</tr>
</thead>
<tbody>
<% if (portfolio && portfolio.length > 0) { %>
<% portfolio.forEach(item => { %>
<tr class="portfolio-item" data-category="<%= item.category %>" data-title="<%= item.title.toLowerCase() %>" data-status="<%= item.isPublished ? 'published' : 'draft' %><%= item.featured ? ' featured' : '' %>">
<td>
<% if (item.images && item.images.length > 0) { %>
<img src="<%= item.images[0].url %>" alt="<%= item.title %>" class="img-circle img-size-32">
<% } else { %>
<div class="img-circle bg-secondary d-flex align-items-center justify-content-center" style="width: 32px; height: 32px;">
<i class="fas fa-image text-white"></i>
</div>
<% } %>
</td>
<td>
<div>
<strong><%= item.title %></strong>
<% if (item.featured) { %>
<span class="badge badge-warning ml-1">
<i class="fas fa-star"></i>
</span>
<% } %>
</div>
<small class="text-muted"><%= item.shortDescription || 'Описание не указано' %></small>
</td>
<td>
<span class="badge badge-info">
<%= item.category.replace('-', ' ') %>
</span>
</td>
<td>
<% if (item.isPublished) { %>
<span class="badge badge-success">
<i class="fas fa-check-circle mr-1"></i>Опубликовано
</span>
<% } else { %>
<span class="badge badge-secondary">
<i class="fas fa-clock mr-1"></i>Черновик
</span>
<% } %>
</td>
<td>
<% if (item.technologies && item.technologies.length > 0) { %>
<% item.technologies.slice(0, 2).forEach(tech => { %>
<span class="badge badge-light mr-1"><%= tech %></span>
<% }) %>
<% if (item.technologies.length > 2) { %>
<span class="text-muted">+<%= item.technologies.length - 2 %></span>
<% } %>
<% } else { %>
<span class="text-muted">—</span>
<% } %>
</td>
<td>
<small class="text-muted">
<%= new Date(item.createdAt).toLocaleDateString('ru-RU') %>
</small>
</td>
<td>
<div class="btn-group btn-group-sm" role="group">
<% if (item.isPublished) { %>
<a href="/portfolio/<%= item.id %>" target="_blank" class="btn btn-info btn-sm" title="Просмотр">
<i class="fas fa-external-link-alt"></i>
</a>
<% } %>
<button type="button" class="btn btn-<%= item.isPublished ? 'warning' : 'success' %> btn-sm"
onclick="togglePublish('<%= item.id %>', '<%= item.isPublished %>')"
title="<%= item.isPublished ? 'Скрыть' : 'Опубликовать' %>">
<i class="fas <%= item.isPublished ? 'fa-eye-slash' : 'fa-eye' %>"></i>
</button>
<a href="/admin/portfolio/edit/<%= item.id %>" class="btn btn-primary btn-sm" title="Редактировать">
<i class="fas fa-edit"></i>
</a>
<button type="button" class="btn btn-danger btn-sm"
onclick="deletePortfolio('<%= item.id %>', '<%= item.title %>')"
title="Удалить">
<i class="fas fa-trash"></i>
</button>
</div>
</td>
</tr>
<% }) %>
<% } else { %>
<tr>
<td colspan="7" class="text-center py-4">
<div class="text-muted">
<i class="fas fa-briefcase fa-3x mb-3"></i>
<p>Проекты не найдены</p>
<a href="/admin/portfolio/add" class="btn btn-primary">
Добавить первый проект
</a>
</div>
</td>
</tr>
<% } %>
</tbody>
</table>
</div>
<!-- Pagination -->
<% if (pagination && pagination.total > 1) { %>
<div class="card-footer clearfix">
<ul class="pagination pagination-sm m-0 float-right">
<% if (pagination.hasPrev) { %>
<li class="page-item">
<a class="page-link" href="?page=<%= pagination.current - 1 %>">«</a>
</li>
<% } %>
<% for (let i = 1; i <= pagination.total; i++) { %>
<li class="page-item <%= pagination.current === i ? 'active' : '' %>">
<a class="page-link" href="?page=<%= i %>"><%= i %></a>
</li>
<% } %>
<% if (pagination.hasNext) { %>
<li class="page-item">
<a class="page-link" href="?page=<%= pagination.current + 1 %>">»</a>
</li>
<% } %>
</ul>
</div>
<% } %>
</div>
</div>
</section>
<script>
function deletePortfolio(id, title) {
Swal.fire({
title: 'Удалить проект?',
text: `Вы уверены, что хотите удалить проект "${title}"?`,
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#d33',
cancelButtonColor: '#3085d6',
confirmButtonText: 'Да, удалить!',
cancelButtonText: 'Отмена'
}).then((result) => {
if (result.isConfirmed) {
fetch(`/api/admin/portfolio/${id}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
Swal.fire('Удалено!', 'Проект был удален.', 'success').then(() => {
location.reload();
});
} else {
Swal.fire('Ошибка!', data.message || 'Ошибка при удалении проекта', 'error');
}
})
.catch(error => {
console.error('Error:', error);
Swal.fire('Ошибка!', 'Произошла ошибка при удалении проекта', 'error');
});
}
});
}
function togglePublish(id, currentStatus) {
const isPublished = currentStatus === 'true';
fetch(`/api/admin/portfolio/${id}`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
isPublished: !isPublished
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
toastr.success(`Проект ${!isPublished ? 'опубликован' : 'скрыт'}`);
setTimeout(() => {
location.reload();
}, 1000);
} else {
toastr.error(data.message || 'Ошибка при изменении статуса');
}
})
.catch(error => {
console.error('Error:', error);
toastr.error('Произошла ошибка при изменении статуса');
});
}
function searchProjects() {
const searchTerm = document.getElementById('searchInput').value.toLowerCase();
const items = document.querySelectorAll('.portfolio-item');
let visibleCount = 0;
items.forEach(item => {
const title = item.dataset.title;
if (title.includes(searchTerm)) {
item.style.display = 'table-row';
visibleCount++;
} else {
item.style.display = 'none';
}
});
updateProjectCount(visibleCount, items.length);
}
function filterByCategory() {
const selectedCategory = document.getElementById('categoryFilter').value;
const selectedStatus = document.getElementById('statusFilter').value;
filterPortfolio(selectedCategory, selectedStatus);
}
function filterByStatus() {
const selectedCategory = document.getElementById('categoryFilter').value;
const selectedStatus = document.getElementById('statusFilter').value;
filterPortfolio(selectedCategory, selectedStatus);
}
function filterPortfolio(category, status) {
const items = document.querySelectorAll('.portfolio-item');
let visibleCount = 0;
items.forEach(item => {
const itemCategory = item.dataset.category;
const itemStatus = item.dataset.status;
let showItem = true;
if (category && itemCategory !== category) {
showItem = false;
}
if (status && !itemStatus.includes(status)) {
showItem = false;
}
if (showItem) {
item.style.display = 'table-row';
visibleCount++;
} else {
item.style.display = 'none';
}
});
updateProjectCount(visibleCount, items.length);
}
function updateProjectCount(visible, total) {
const countElement = document.getElementById('projectCount');
if (countElement) {
if (visible !== undefined) {
countElement.textContent = `Показано: ${visible} из ${total}`;
} else {
countElement.textContent = `Всего: ${total}`;
}
}
}
// Search on Enter key
document.getElementById('searchInput').addEventListener('keyup', function(event) {
if (event.key === 'Enter') {
searchProjects();
}
});
// Auto search on input
document.getElementById('searchInput').addEventListener('input', function() {
searchProjects();
});
</script>