362 lines
17 KiB
Plaintext
362 lines
17 KiB
Plaintext
<!-- 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> |