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,361 @@
<!-- Content Header (Page header) -->
<section class="content-header">
<div class="container-fluid">
<div class="row mb-2">
<div class="col-sm-6">
<h1><i class="fas fa-images mr-2"></i>Редактор Баннеров</h1>
</div>
<div class="col-sm-6">
<ol class="breadcrumb float-sm-right">
<li class="breadcrumb-item"><a href="/admin">Админ</a></li>
<li class="breadcrumb-item active">Баннеры</li>
</ol>
</div>
</div>
</div>
</section>
<!-- Main content -->
<section class="content">
<div class="container-fluid">
<!-- Banner Upload Card -->
<div class="card">
<div class="card-header">
<h3 class="card-title">Загрузить новый баннер</h3>
</div>
<div class="card-body">
<div class="upload-zone p-4 text-center border-2 border-dashed border-gray-300 rounded"
ondrop="handleDrop(event)" ondragover="handleDragOver(event)" ondragleave="handleDragLeave(event)">
<i class="fas fa-cloud-upload-alt fa-3x text-muted mb-3"></i>
<p class="h5 text-muted mb-2">Перетащите изображения сюда или нажмите для выбора</p>
<p class="text-muted">Поддерживаются: JPG, PNG, GIF (максимум 10MB)</p>
<input type="file" id="banner-upload" multiple accept="image/*" class="d-none">
<button type="button" class="btn btn-primary mt-3" onclick="document.getElementById('banner-upload').click()">
<i class="fas fa-plus mr-1"></i>Выбрать файлы
</button>
</div>
</div>
</div>
<!-- Current Banners Card -->
<div class="card">
<div class="card-header">
<h3 class="card-title">Текущие баннеры</h3>
<div class="card-tools">
<button class="btn btn-sm btn-default" onclick="loadBanners()">
<i class="fas fa-sync-alt"></i> Обновить
</button>
</div>
</div>
<div class="card-body">
<div id="banners-list" class="row">
<!-- Banners will be loaded here -->
</div>
<!-- Empty state -->
<div id="empty-state" class="text-center py-5" style="display: none;">
<i class="fas fa-images fa-4x text-muted mb-3"></i>
<h4 class="text-muted">Нет загруженных баннеров</h4>
<p class="text-muted">Загрузите ваши первые баннеры используя форму выше</p>
</div>
</div>
</div>
</div>
</section>
<!-- Banner Edit Modal -->
<div class="modal fade" id="edit-banner-modal" tabindex="-1" role="dialog">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">Редактировать баннер</h4>
<button type="button" class="close" data-dismiss="modal">
<span>&times;</span>
</button>
</div>
<form id="edit-banner-form">
<div class="modal-body">
<input type="hidden" id="edit-banner-id">
<div class="row">
<div class="col-md-6">
<img id="edit-banner-preview" src="" alt="Banner preview" class="img-fluid rounded">
</div>
<div class="col-md-6">
<div class="form-group">
<label for="edit-banner-title">Заголовок</label>
<input type="text" class="form-control" id="edit-banner-title" name="title" required>
</div>
<div class="form-group">
<label for="edit-banner-subtitle">Подзаголовок</label>
<input type="text" class="form-control" id="edit-banner-subtitle" name="subtitle">
</div>
<div class="form-group">
<label for="edit-banner-description">Описание</label>
<textarea class="form-control" id="edit-banner-description" name="description" rows="3"></textarea>
</div>
<div class="form-group">
<label for="edit-banner-button-text">Текст кнопки</label>
<input type="text" class="form-control" id="edit-banner-button-text" name="buttonText">
</div>
<div class="form-group">
<label for="edit-banner-button-url">Ссылка кнопки</label>
<input type="url" class="form-control" id="edit-banner-button-url" name="buttonUrl">
</div>
<div class="form-group">
<label for="edit-banner-order">Порядок отображения</label>
<input type="number" class="form-control" id="edit-banner-order" name="order" min="0">
</div>
<div class="form-group">
<div class="custom-control custom-switch">
<input type="checkbox" class="custom-control-input" id="edit-banner-active" name="active">
<label class="custom-control-label" for="edit-banner-active">Активен</label>
</div>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Отмена</button>
<button type="submit" class="btn btn-primary">
<i class="fas fa-save mr-1"></i>Сохранить
</button>
</div>
</form>
</div>
</div>
</div>
<style>
.upload-zone.dragover {
border-color: #007bff !important;
background-color: #f8f9fa !important;
}
.banner-item {
transition: transform 0.2s;
}
.banner-item:hover {
transform: translateY(-2px);
}
</style>
<script>
let banners = [];
document.addEventListener('DOMContentLoaded', function() {
loadBanners();
// Setup file upload
document.getElementById('banner-upload').addEventListener('change', handleFileSelect);
// Setup edit form
document.getElementById('edit-banner-form').addEventListener('submit', saveBanner);
});
async function loadBanners() {
try {
const response = await fetch('/api/admin/banners');
const data = await response.json();
if (data.success) {
banners = data.banners;
renderBanners();
} else {
throw new Error(data.message);
}
} catch (error) {
console.error('Error loading banners:', error);
alert('Ошибка загрузки баннеров: ' + error.message);
}
}
function renderBanners() {
const container = document.getElementById('banners-list');
const emptyState = document.getElementById('empty-state');
if (banners.length === 0) {
container.innerHTML = '';
emptyState.style.display = 'block';
return;
}
emptyState.style.display = 'none';
container.innerHTML = banners.map(banner => `
<div class="col-md-4 mb-4">
<div class="card banner-item">
<img src="${banner.imageUrl}" class="card-img-top" style="height: 200px; object-fit: cover;">
<div class="card-body">
<h5 class="card-title">${banner.title || 'Без названия'}</h5>
<p class="card-text text-muted small">${banner.subtitle || ''}</p>
<div class="d-flex justify-content-between align-items-center">
<div class="btn-group">
<button class="btn btn-sm btn-outline-primary" onclick="editBanner('${banner._id}')">
<i class="fas fa-edit"></i>
</button>
<button class="btn btn-sm btn-outline-danger" onclick="deleteBanner('${banner._id}')">
<i class="fas fa-trash"></i>
</button>
</div>
<small class="text-muted">
${banner.active ? '<span class="badge badge-success">Активен</span>' : '<span class="badge badge-secondary">Неактивен</span>'}
</small>
</div>
</div>
</div>
</div>
`).join('');
}
function handleDragOver(event) {
event.preventDefault();
event.currentTarget.classList.add('dragover');
}
function handleDragLeave(event) {
event.preventDefault();
event.currentTarget.classList.remove('dragover');
}
function handleDrop(event) {
event.preventDefault();
event.currentTarget.classList.remove('dragover');
const files = Array.from(event.dataTransfer.files);
uploadFiles(files);
}
function handleFileSelect(event) {
const files = Array.from(event.target.files);
uploadFiles(files);
event.target.value = ''; // Reset input
}
async function uploadFiles(files) {
for (const file of files) {
if (!file.type.startsWith('image/')) {
alert(`Файл ${file.name} не является изображением`);
continue;
}
if (file.size > 10 * 1024 * 1024) { // 10MB
alert(`Файл ${file.name} слишком большой (максимум 10MB)`);
continue;
}
await uploadFile(file);
}
loadBanners(); // Refresh the list
}
async function uploadFile(file) {
const formData = new FormData();
formData.append('banner', file);
try {
const response = await fetch('/api/admin/banners/upload', {
method: 'POST',
body: formData
});
const data = await response.json();
if (!data.success) {
throw new Error(data.message);
}
console.log(`Файл ${file.name} успешно загружен`);
} catch (error) {
console.error(`Error uploading ${file.name}:`, error);
alert(`Ошибка загрузки ${file.name}: ${error.message}`);
}
}
function editBanner(bannerId) {
const banner = banners.find(b => b._id === bannerId);
if (!banner) return;
// Fill the form
document.getElementById('edit-banner-id').value = banner._id;
document.getElementById('edit-banner-preview').src = banner.imageUrl;
document.getElementById('edit-banner-title').value = banner.title || '';
document.getElementById('edit-banner-subtitle').value = banner.subtitle || '';
document.getElementById('edit-banner-description').value = banner.description || '';
document.getElementById('edit-banner-button-text').value = banner.buttonText || '';
document.getElementById('edit-banner-button-url').value = banner.buttonUrl || '';
document.getElementById('edit-banner-order').value = banner.order || 0;
document.getElementById('edit-banner-active').checked = banner.active || false;
// Show modal
$('#edit-banner-modal').modal('show');
}
async function saveBanner(event) {
event.preventDefault();
const formData = new FormData(event.target);
const bannerId = document.getElementById('edit-banner-id').value;
const bannerData = {
title: formData.get('title'),
subtitle: formData.get('subtitle'),
description: formData.get('description'),
buttonText: formData.get('buttonText'),
buttonUrl: formData.get('buttonUrl'),
order: parseInt(formData.get('order')) || 0,
active: formData.has('active')
};
try {
const response = await fetch(`/api/admin/banners/${bannerId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(bannerData)
});
const data = await response.json();
if (data.success) {
$('#edit-banner-modal').modal('hide');
loadBanners();
alert('Баннер обновлен');
} else {
throw new Error(data.message);
}
} catch (error) {
console.error('Error saving banner:', error);
alert('Ошибка сохранения баннера: ' + error.message);
}
}
async function deleteBanner(bannerId) {
if (!confirm('Вы уверены, что хотите удалить этот баннер?')) {
return;
}
try {
const response = await fetch(`/api/admin/banners/${bannerId}`, {
method: 'DELETE'
});
const data = await response.json();
if (data.success) {
loadBanners();
alert('Баннер удален');
} else {
throw new Error(data.message);
}
} catch (error) {
console.error('Error deleting banner:', error);
alert('Ошибка удаления баннера: ' + error.message);
}
}
</script>

View File

@@ -0,0 +1,361 @@
<!-- Content Header (Page header) -->
<section class="content-header">
<div class="container-fluid">
<div class="row mb-2">
<div class="col-sm-6">
<h1><i class="fas fa-images mr-2"></i>Редактор Баннеров</h1>
</div>
<div class="col-sm-6">
<ol class="breadcrumb float-sm-right">
<li class="breadcrumb-item"><a href="/admin">Админ</a></li>
<li class="breadcrumb-item active">Баннеры</li>
</ol>
</div>
</div>
</div>
</section>
<!-- Main content -->
<section class="content">
<div class="container-fluid">
<!-- Banner Upload Card -->
<div class="card">
<div class="card-header">
<h3 class="card-title">Загрузить новый баннер</h3>
</div>
<div class="card-body">
<div class="upload-zone p-4 text-center border-2 border-dashed border-gray-300 rounded"
ondrop="handleDrop(event)" ondragover="handleDragOver(event)" ondragleave="handleDragLeave(event)">
<i class="fas fa-cloud-upload-alt fa-3x text-muted mb-3"></i>
<p class="h5 text-muted mb-2">Перетащите изображения сюда или нажмите для выбора</p>
<p class="text-muted">Поддерживаются: JPG, PNG, GIF (максимум 10MB)</p>
<input type="file" id="banner-upload" multiple accept="image/*" class="d-none">
<button type="button" class="btn btn-primary mt-3" onclick="document.getElementById('banner-upload').click()">
<i class="fas fa-plus mr-1"></i>Выбрать файлы
</button>
</div>
</div>
</div>
<!-- Current Banners Card -->
<div class="card">
<div class="card-header">
<h3 class="card-title">Текущие баннеры</h3>
<div class="card-tools">
<button class="btn btn-sm btn-default" onclick="loadBanners()">
<i class="fas fa-sync-alt"></i> Обновить
</button>
</div>
</div>
<div class="card-body">
<div id="banners-list" class="row">
<!-- Banners will be loaded here -->
</div>
<!-- Empty state -->
<div id="empty-state" class="text-center py-5" style="display: none;">
<i class="fas fa-images fa-4x text-muted mb-3"></i>
<h4 class="text-muted">Нет загруженных баннеров</h4>
<p class="text-muted">Загрузите ваши первые баннеры используя форму выше</p>
</div>
</div>
</div>
</div>
</section>
<!-- Banner Edit Modal -->
<div class="modal fade" id="edit-banner-modal" tabindex="-1" role="dialog">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">Редактировать баннер</h4>
<button type="button" class="close" data-dismiss="modal">
<span>&times;</span>
</button>
</div>
<form id="edit-banner-form">
<div class="modal-body">
<input type="hidden" id="edit-banner-id">
<div class="row">
<div class="col-md-6">
<img id="edit-banner-preview" src="" alt="Banner preview" class="img-fluid rounded">
</div>
<div class="col-md-6">
<div class="form-group">
<label for="edit-banner-title">Заголовок</label>
<input type="text" class="form-control" id="edit-banner-title" name="title" required>
</div>
<div class="form-group">
<label for="edit-banner-subtitle">Подзаголовок</label>
<input type="text" class="form-control" id="edit-banner-subtitle" name="subtitle">
</div>
<div class="form-group">
<label for="edit-banner-description">Описание</label>
<textarea class="form-control" id="edit-banner-description" name="description" rows="3"></textarea>
</div>
<div class="form-group">
<label for="edit-banner-button-text">Текст кнопки</label>
<input type="text" class="form-control" id="edit-banner-button-text" name="buttonText">
</div>
<div class="form-group">
<label for="edit-banner-button-url">Ссылка кнопки</label>
<input type="url" class="form-control" id="edit-banner-button-url" name="buttonUrl">
</div>
<div class="form-group">
<label for="edit-banner-order">Порядок отображения</label>
<input type="number" class="form-control" id="edit-banner-order" name="order" min="0">
</div>
<div class="form-group">
<div class="custom-control custom-switch">
<input type="checkbox" class="custom-control-input" id="edit-banner-active" name="active">
<label class="custom-control-label" for="edit-banner-active">Активен</label>
</div>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Отмена</button>
<button type="submit" class="btn btn-primary">
<i class="fas fa-save mr-1"></i>Сохранить
</button>
</div>
</form>
</div>
</div>
</div>
<style>
.upload-zone.dragover {
border-color: #007bff !important;
background-color: #f8f9fa !important;
}
.banner-item {
transition: transform 0.2s;
}
.banner-item:hover {
transform: translateY(-2px);
}
</style>
<script>
let banners = [];
document.addEventListener('DOMContentLoaded', function() {
loadBanners();
// Setup file upload
document.getElementById('banner-upload').addEventListener('change', handleFileSelect);
// Setup edit form
document.getElementById('edit-banner-form').addEventListener('submit', saveBanner);
});
async function loadBanners() {
try {
const response = await fetch('/api/admin/banners');
const data = await response.json();
if (data.success) {
banners = data.banners;
renderBanners();
} else {
throw new Error(data.message);
}
} catch (error) {
console.error('Error loading banners:', error);
alert('Ошибка загрузки баннеров: ' + error.message);
}
}
function renderBanners() {
const container = document.getElementById('banners-list');
const emptyState = document.getElementById('empty-state');
if (banners.length === 0) {
container.innerHTML = '';
emptyState.style.display = 'block';
return;
}
emptyState.style.display = 'none';
container.innerHTML = banners.map(banner => `
<div class="col-md-4 mb-4">
<div class="card banner-item">
<img src="${banner.imageUrl}" class="card-img-top" style="height: 200px; object-fit: cover;">
<div class="card-body">
<h5 class="card-title">${banner.title || 'Без названия'}</h5>
<p class="card-text text-muted small">${banner.subtitle || ''}</p>
<div class="d-flex justify-content-between align-items-center">
<div class="btn-group">
<button class="btn btn-sm btn-outline-primary" onclick="editBanner('${banner._id}')">
<i class="fas fa-edit"></i>
</button>
<button class="btn btn-sm btn-outline-danger" onclick="deleteBanner('${banner._id}')">
<i class="fas fa-trash"></i>
</button>
</div>
<small class="text-muted">
${banner.active ? '<span class="badge badge-success">Активен</span>' : '<span class="badge badge-secondary">Неактивен</span>'}
</small>
</div>
</div>
</div>
</div>
`).join('');
}
function handleDragOver(event) {
event.preventDefault();
event.currentTarget.classList.add('dragover');
}
function handleDragLeave(event) {
event.preventDefault();
event.currentTarget.classList.remove('dragover');
}
function handleDrop(event) {
event.preventDefault();
event.currentTarget.classList.remove('dragover');
const files = Array.from(event.dataTransfer.files);
uploadFiles(files);
}
function handleFileSelect(event) {
const files = Array.from(event.target.files);
uploadFiles(files);
event.target.value = ''; // Reset input
}
async function uploadFiles(files) {
for (const file of files) {
if (!file.type.startsWith('image/')) {
alert(`Файл ${file.name} не является изображением`);
continue;
}
if (file.size > 10 * 1024 * 1024) { // 10MB
alert(`Файл ${file.name} слишком большой (максимум 10MB)`);
continue;
}
await uploadFile(file);
}
loadBanners(); // Refresh the list
}
async function uploadFile(file) {
const formData = new FormData();
formData.append('banner', file);
try {
const response = await fetch('/api/admin/banners/upload', {
method: 'POST',
body: formData
});
const data = await response.json();
if (!data.success) {
throw new Error(data.message);
}
console.log(`Файл ${file.name} успешно загружен`);
} catch (error) {
console.error(`Error uploading ${file.name}:`, error);
alert(`Ошибка загрузки ${file.name}: ${error.message}`);
}
}
function editBanner(bannerId) {
const banner = banners.find(b => b._id === bannerId);
if (!banner) return;
// Fill the form
document.getElementById('edit-banner-id').value = banner._id;
document.getElementById('edit-banner-preview').src = banner.imageUrl;
document.getElementById('edit-banner-title').value = banner.title || '';
document.getElementById('edit-banner-subtitle').value = banner.subtitle || '';
document.getElementById('edit-banner-description').value = banner.description || '';
document.getElementById('edit-banner-button-text').value = banner.buttonText || '';
document.getElementById('edit-banner-button-url').value = banner.buttonUrl || '';
document.getElementById('edit-banner-order').value = banner.order || 0;
document.getElementById('edit-banner-active').checked = banner.active || false;
// Show modal
$('#edit-banner-modal').modal('show');
}
async function saveBanner(event) {
event.preventDefault();
const formData = new FormData(event.target);
const bannerId = document.getElementById('edit-banner-id').value;
const bannerData = {
title: formData.get('title'),
subtitle: formData.get('subtitle'),
description: formData.get('description'),
buttonText: formData.get('buttonText'),
buttonUrl: formData.get('buttonUrl'),
order: parseInt(formData.get('order')) || 0,
active: formData.has('active')
};
try {
const response = await fetch(`/api/admin/banners/${bannerId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(bannerData)
});
const data = await response.json();
if (data.success) {
$('#edit-banner-modal').modal('hide');
loadBanners();
alert('Баннер обновлен');
} else {
throw new Error(data.message);
}
} catch (error) {
console.error('Error saving banner:', error);
alert('Ошибка сохранения баннера: ' + error.message);
}
}
async function deleteBanner(bannerId) {
if (!confirm('Вы уверены, что хотите удалить этот баннер?')) {
return;
}
try {
const response = await fetch(`/api/admin/banners/${bannerId}`, {
method: 'DELETE'
});
const data = await response.json();
if (data.success) {
loadBanners();
alert('Баннер удален');
} else {
throw new Error(data.message);
}
} catch (error) {
console.error('Error deleting banner:', error);
alert('Ошибка удаления баннера: ' + error.message);
}
}
</script>

View File

@@ -0,0 +1,359 @@
<!-- Dashboard Content -->
<div class="row">
<!-- Stats Cards -->
<div class="col-lg-3 col-6">
<div class="info-box">
<span class="info-box-icon bg-info elevation-1">
<i class="fas fa-briefcase"></i>
</span>
<div class="info-box-content">
<span class="info-box-text">포트폴리오 프로젝트</span>
<span class="info-box-number"><%= stats.portfolioCount || 0 %></span>
<div class="progress">
<div class="progress-bar bg-info" style="width: 70%"></div>
</div>
<span class="progress-description">
70% 완료된 프로젝트
</span>
</div>
</div>
</div>
<div class="col-lg-3 col-6">
<div class="info-box">
<span class="info-box-icon bg-success elevation-1">
<i class="fas fa-cogs"></i>
</span>
<div class="info-box-content">
<span class="info-box-text">제공 서비스</span>
<span class="info-box-number"><%= stats.servicesCount || 0 %></span>
<div class="progress">
<div class="progress-bar bg-success" style="width: 100%"></div>
</div>
<span class="progress-description">
모든 서비스 활성화
</span>
</div>
</div>
</div>
<div class="col-lg-3 col-6">
<div class="info-box">
<span class="info-box-icon bg-warning elevation-1">
<i class="fas fa-envelope"></i>
</span>
<div class="info-box-content">
<span class="info-box-text">문의 메시지</span>
<span class="info-box-number"><%= stats.contactsCount || 0 %></span>
<div class="progress">
<div class="progress-bar bg-warning" style="width: 60%"></div>
</div>
<span class="progress-description">
60% 응답 완료
</span>
</div>
</div>
</div>
<div class="col-lg-3 col-6">
<div class="info-box">
<span class="info-box-icon bg-danger elevation-1">
<i class="fas fa-users"></i>
</span>
<div class="info-box-content">
<span class="info-box-text">관리자 계정</span>
<span class="info-box-number"><%= stats.usersCount || 0 %></span>
<div class="progress">
<div class="progress-bar bg-danger" style="width: 100%"></div>
</div>
<span class="progress-description">
모든 계정 활성화
</span>
</div>
</div>
</div>
</div>
<!-- Main Content Row -->
<div class="row">
<!-- Recent Portfolio Projects -->
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h3 class="card-title">
<i class="fas fa-briefcase mr-1"></i>
최근 포트폴리오 프로젝트
</h3>
<div class="card-tools">
<button type="button" class="btn btn-tool" data-card-widget="collapse">
<i class="fas fa-minus"></i>
</button>
</div>
</div>
<div class="card-body">
<% if (recentPortfolio && recentPortfolio.length > 0) { %>
<ul class="list-unstyled">
<% recentPortfolio.forEach(function(project, index) { %>
<li class="d-flex align-items-center <%= index < recentPortfolio.length - 1 ? 'border-bottom pb-3 mb-3' : '' %>">
<div class="flex-shrink-0">
<span class="badge badge-info badge-pill">
<i class="fas fa-code"></i>
</span>
</div>
<div class="flex-grow-1 ml-3">
<h6 class="mb-1 font-weight-bold"><%= project.title %></h6>
<p class="text-muted mb-1"><%= project.category %></p>
<small class="text-muted">
<i class="fas fa-calendar mr-1"></i>
<%= project.createdAt ? project.createdAt.toLocaleDateString('ko-KR') : '날짜 없음' %>
</small>
</div>
<div class="flex-shrink-0">
<span class="badge badge-<%= project.status === 'completed' ? 'success' : project.status === 'in-progress' ? 'warning' : 'secondary' %>">
<%= project.status === 'completed' ? '완료' : project.status === 'in-progress' ? '진행중' : '계획' %>
</span>
</div>
</li>
<% }); %>
</ul>
<div class="text-center mt-3">
<a href="/admin/portfolio" class="btn btn-primary btn-sm">
<i class="fas fa-eye mr-1"></i>
모든 프로젝트 보기
</a>
</div>
<% } else { %>
<div class="text-center py-4">
<i class="fas fa-briefcase fa-3x text-muted mb-3"></i>
<p class="text-muted">아직 포트폴리오 프로젝트가 없습니다.</p>
<a href="/admin/portfolio/add" class="btn btn-primary btn-sm">
<i class="fas fa-plus mr-1"></i>
첫 번째 프로젝트 추가
</a>
</div>
<% } %>
</div>
</div>
</div>
<!-- Recent Contact Messages -->
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h3 class="card-title">
<i class="fas fa-envelope mr-1"></i>
최근 문의 메시지
</h3>
<div class="card-tools">
<button type="button" class="btn btn-tool" data-card-widget="collapse">
<i class="fas fa-minus"></i>
</button>
</div>
</div>
<div class="card-body">
<% if (recentContacts && recentContacts.length > 0) { %>
<ul class="list-unstyled">
<% recentContacts.forEach(function(contact, index) { %>
<li class="d-flex align-items-center <%= index < recentContacts.length - 1 ? 'border-bottom pb-3 mb-3' : '' %>">
<div class="flex-shrink-0">
<div class="bg-primary text-white rounded-circle d-flex align-items-center justify-content-center" style="width: 40px; height: 40px;">
<%= contact.name ? contact.name.charAt(0).toUpperCase() : 'U' %>
</div>
</div>
<div class="flex-grow-1 ml-3">
<h6 class="mb-1 font-weight-bold"><%= contact.name %></h6>
<p class="text-muted mb-1"><%= contact.email %></p>
<small class="text-muted">
<i class="fas fa-clock mr-1"></i>
<%= contact.createdAt ? contact.createdAt.toLocaleDateString('ko-KR') : '날짜 없음' %>
</small>
</div>
<div class="flex-shrink-0">
<span class="badge badge-<%= contact.status === 'replied' ? 'success' : contact.status === 'pending' ? 'warning' : 'secondary' %>">
<%= contact.status === 'replied' ? '답변완료' : contact.status === 'pending' ? '대기중' : '신규' %>
</span>
</div>
</li>
<% }); %>
</ul>
<div class="text-center mt-3">
<a href="/admin/contacts" class="btn btn-warning btn-sm">
<i class="fas fa-envelope-open mr-1"></i>
모든 문의 보기
</a>
</div>
<% } else { %>
<div class="text-center py-4">
<i class="fas fa-envelope fa-3x text-muted mb-3"></i>
<p class="text-muted">새로운 문의가 없습니다.</p>
<small class="text-muted">고객 문의가 들어오면 여기에 표시됩니다.</small>
</div>
<% } %>
</div>
</div>
</div>
</div>
<!-- Quick Actions & Tools -->
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header">
<h3 class="card-title">
<i class="fas fa-bolt mr-1"></i>
빠른 작업
</h3>
</div>
<div class="card-body">
<div class="row">
<div class="col-lg-3 col-md-6">
<div class="info-box bg-gradient-info">
<span class="info-box-icon">
<i class="fas fa-plus"></i>
</span>
<div class="info-box-content">
<span class="info-box-text">새 프로젝트</span>
<span class="info-box-number">추가하기</span>
<div class="progress">
<div class="progress-bar" style="width: 100%"></div>
</div>
<a href="/admin/portfolio/add" class="progress-description text-white">
포트폴리오에 새 프로젝트 추가 →
</a>
</div>
</div>
</div>
<div class="col-lg-3 col-md-6">
<div class="info-box bg-gradient-success">
<span class="info-box-icon">
<i class="fas fa-cog"></i>
</span>
<div class="info-box-content">
<span class="info-box-text">서비스 관리</span>
<span class="info-box-number">설정</span>
<div class="progress">
<div class="progress-bar" style="width: 100%"></div>
</div>
<a href="/admin/services" class="progress-description text-white">
서비스 가격 및 내용 수정 →
</a>
</div>
</div>
</div>
<div class="col-lg-3 col-md-6">
<div class="info-box bg-gradient-warning">
<span class="info-box-icon">
<i class="fas fa-images"></i>
</span>
<div class="info-box-content">
<span class="info-box-text">미디어</span>
<span class="info-box-number">업로드</span>
<div class="progress">
<div class="progress-bar" style="width: 100%"></div>
</div>
<a href="/admin/media" class="progress-description text-white">
이미지 및 파일 관리 →
</a>
</div>
</div>
</div>
<div class="col-lg-3 col-md-6">
<div class="info-box bg-gradient-danger">
<span class="info-box-icon">
<i class="fas fa-wrench"></i>
</span>
<div class="info-box-content">
<span class="info-box-text">사이트 설정</span>
<span class="info-box-number">관리</span>
<div class="progress">
<div class="progress-bar" style="width: 100%"></div>
</div>
<a href="/admin/settings" class="progress-description text-white">
전체 사이트 설정 변경 →
</a>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- System Status -->
<div class="row">
<div class="col-md-6">
<div class="card card-primary">
<div class="card-header">
<h3 class="card-title">
<i class="fas fa-server mr-1"></i>
시스템 상태
</h3>
</div>
<div class="card-body">
<div class="row">
<div class="col-6">
<div class="description-block border-right">
<span class="description-percentage text-success">
<i class="fas fa-caret-up"></i> 99.2%
</span>
<h5 class="description-header">서버 업타임</h5>
<span class="description-text">지난 30일</span>
</div>
</div>
<div class="col-6">
<div class="description-block">
<span class="description-percentage text-info">
<i class="fas fa-caret-up"></i> 2.3초
</span>
<h5 class="description-header">평균 응답시간</h5>
<span class="description-text">페이지 로딩</span>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card card-success">
<div class="card-header">
<h3 class="card-title">
<i class="fab fa-telegram mr-1"></i>
텔레그램 봇 상태
</h3>
</div>
<div class="card-body">
<div class="row">
<div class="col-6">
<div class="description-block border-right">
<span class="description-percentage text-success">
<i class="fas fa-check-circle"></i> 연결됨
</span>
<h5 class="description-header">봇 상태</h5>
<span class="description-text">정상 작동</span>
</div>
</div>
<div class="col-6">
<div class="description-block">
<span class="description-percentage text-info">
<i class="fas fa-paper-plane"></i> 24개
</span>
<h5 class="description-header">전송된 알림</h5>
<span class="description-text">오늘</span>
</div>
</div>
</div>
<div class="text-center mt-2">
<a href="/admin/telegram" class="btn btn-success btn-sm">
<i class="fab fa-telegram mr-1"></i>
텔레그램 설정
</a>
</div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,359 @@
<!-- Dashboard Content -->
<div class="row">
<!-- Stats Cards -->
<div class="col-lg-3 col-6">
<div class="info-box">
<span class="info-box-icon bg-info elevation-1">
<i class="fas fa-briefcase"></i>
</span>
<div class="info-box-content">
<span class="info-box-text">포트폴리오 프로젝트</span>
<span class="info-box-number"><%= stats.portfolioCount || 0 %></span>
<div class="progress">
<div class="progress-bar bg-info" style="width: 70%"></div>
</div>
<span class="progress-description">
70% 완료된 프로젝트
</span>
</div>
</div>
</div>
<div class="col-lg-3 col-6">
<div class="info-box">
<span class="info-box-icon bg-success elevation-1">
<i class="fas fa-cogs"></i>
</span>
<div class="info-box-content">
<span class="info-box-text">제공 서비스</span>
<span class="info-box-number"><%= stats.servicesCount || 0 %></span>
<div class="progress">
<div class="progress-bar bg-success" style="width: 100%"></div>
</div>
<span class="progress-description">
모든 서비스 활성화
</span>
</div>
</div>
</div>
<div class="col-lg-3 col-6">
<div class="info-box">
<span class="info-box-icon bg-warning elevation-1">
<i class="fas fa-envelope"></i>
</span>
<div class="info-box-content">
<span class="info-box-text">문의 메시지</span>
<span class="info-box-number"><%= stats.contactsCount || 0 %></span>
<div class="progress">
<div class="progress-bar bg-warning" style="width: 60%"></div>
</div>
<span class="progress-description">
60% 응답 완료
</span>
</div>
</div>
</div>
<div class="col-lg-3 col-6">
<div class="info-box">
<span class="info-box-icon bg-danger elevation-1">
<i class="fas fa-users"></i>
</span>
<div class="info-box-content">
<span class="info-box-text">관리자 계정</span>
<span class="info-box-number"><%= stats.usersCount || 0 %></span>
<div class="progress">
<div class="progress-bar bg-danger" style="width: 100%"></div>
</div>
<span class="progress-description">
모든 계정 활성화
</span>
</div>
</div>
</div>
</div>
<!-- Main Content Row -->
<div class="row">
<!-- Recent Portfolio Projects -->
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h3 class="card-title">
<i class="fas fa-briefcase mr-1"></i>
최근 포트폴리오 프로젝트
</h3>
<div class="card-tools">
<button type="button" class="btn btn-tool" data-card-widget="collapse">
<i class="fas fa-minus"></i>
</button>
</div>
</div>
<div class="card-body">
<% if (recentPortfolio && recentPortfolio.length > 0) { %>
<ul class="list-unstyled">
<% recentPortfolio.forEach(function(project, index) { %>
<li class="d-flex align-items-center <%= index < recentPortfolio.length - 1 ? 'border-bottom pb-3 mb-3' : '' %>">
<div class="flex-shrink-0">
<span class="badge badge-info badge-pill">
<i class="fas fa-code"></i>
</span>
</div>
<div class="flex-grow-1 ml-3">
<h6 class="mb-1 font-weight-bold"><%= project.title %></h6>
<p class="text-muted mb-1"><%= project.category %></p>
<small class="text-muted">
<i class="fas fa-calendar mr-1"></i>
<%= project.createdAt ? project.createdAt.toLocaleDateString('ko-KR') : '날짜 없음' %>
</small>
</div>
<div class="flex-shrink-0">
<span class="badge badge-<%= project.status === 'completed' ? 'success' : project.status === 'in-progress' ? 'warning' : 'secondary' %>">
<%= project.status === 'completed' ? '완료' : project.status === 'in-progress' ? '진행중' : '계획' %>
</span>
</div>
</li>
<% }); %>
</ul>
<div class="text-center mt-3">
<a href="/admin/portfolio" class="btn btn-primary btn-sm">
<i class="fas fa-eye mr-1"></i>
모든 프로젝트 보기
</a>
</div>
<% } else { %>
<div class="text-center py-4">
<i class="fas fa-briefcase fa-3x text-muted mb-3"></i>
<p class="text-muted">아직 포트폴리오 프로젝트가 없습니다.</p>
<a href="/admin/portfolio/add" class="btn btn-primary btn-sm">
<i class="fas fa-plus mr-1"></i>
첫 번째 프로젝트 추가
</a>
</div>
<% } %>
</div>
</div>
</div>
<!-- Recent Contact Messages -->
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h3 class="card-title">
<i class="fas fa-envelope mr-1"></i>
최근 문의 메시지
</h3>
<div class="card-tools">
<button type="button" class="btn btn-tool" data-card-widget="collapse">
<i class="fas fa-minus"></i>
</button>
</div>
</div>
<div class="card-body">
<% if (recentContacts && recentContacts.length > 0) { %>
<ul class="list-unstyled">
<% recentContacts.forEach(function(contact, index) { %>
<li class="d-flex align-items-center <%= index < recentContacts.length - 1 ? 'border-bottom pb-3 mb-3' : '' %>">
<div class="flex-shrink-0">
<div class="bg-primary text-white rounded-circle d-flex align-items-center justify-content-center" style="width: 40px; height: 40px;">
<%= contact.name ? contact.name.charAt(0).toUpperCase() : 'U' %>
</div>
</div>
<div class="flex-grow-1 ml-3">
<h6 class="mb-1 font-weight-bold"><%= contact.name %></h6>
<p class="text-muted mb-1"><%= contact.email %></p>
<small class="text-muted">
<i class="fas fa-clock mr-1"></i>
<%= contact.createdAt ? contact.createdAt.toLocaleDateString('ko-KR') : '날짜 없음' %>
</small>
</div>
<div class="flex-shrink-0">
<span class="badge badge-<%= contact.status === 'replied' ? 'success' : contact.status === 'pending' ? 'warning' : 'secondary' %>">
<%= contact.status === 'replied' ? '답변완료' : contact.status === 'pending' ? '대기중' : '신규' %>
</span>
</div>
</li>
<% }); %>
</ul>
<div class="text-center mt-3">
<a href="/admin/contacts" class="btn btn-warning btn-sm">
<i class="fas fa-envelope-open mr-1"></i>
모든 문의 보기
</a>
</div>
<% } else { %>
<div class="text-center py-4">
<i class="fas fa-envelope fa-3x text-muted mb-3"></i>
<p class="text-muted">새로운 문의가 없습니다.</p>
<small class="text-muted">고객 문의가 들어오면 여기에 표시됩니다.</small>
</div>
<% } %>
</div>
</div>
</div>
</div>
<!-- Quick Actions & Tools -->
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header">
<h3 class="card-title">
<i class="fas fa-bolt mr-1"></i>
빠른 작업
</h3>
</div>
<div class="card-body">
<div class="row">
<div class="col-lg-3 col-md-6">
<div class="info-box bg-gradient-info">
<span class="info-box-icon">
<i class="fas fa-plus"></i>
</span>
<div class="info-box-content">
<span class="info-box-text">새 프로젝트</span>
<span class="info-box-number">추가하기</span>
<div class="progress">
<div class="progress-bar" style="width: 100%"></div>
</div>
<a href="/admin/portfolio/add" class="progress-description text-white">
포트폴리오에 새 프로젝트 추가 →
</a>
</div>
</div>
</div>
<div class="col-lg-3 col-md-6">
<div class="info-box bg-gradient-success">
<span class="info-box-icon">
<i class="fas fa-cog"></i>
</span>
<div class="info-box-content">
<span class="info-box-text">서비스 관리</span>
<span class="info-box-number">설정</span>
<div class="progress">
<div class="progress-bar" style="width: 100%"></div>
</div>
<a href="/admin/services" class="progress-description text-white">
서비스 가격 및 내용 수정 →
</a>
</div>
</div>
</div>
<div class="col-lg-3 col-md-6">
<div class="info-box bg-gradient-warning">
<span class="info-box-icon">
<i class="fas fa-images"></i>
</span>
<div class="info-box-content">
<span class="info-box-text">미디어</span>
<span class="info-box-number">업로드</span>
<div class="progress">
<div class="progress-bar" style="width: 100%"></div>
</div>
<a href="/admin/media" class="progress-description text-white">
이미지 및 파일 관리 →
</a>
</div>
</div>
</div>
<div class="col-lg-3 col-md-6">
<div class="info-box bg-gradient-danger">
<span class="info-box-icon">
<i class="fas fa-wrench"></i>
</span>
<div class="info-box-content">
<span class="info-box-text">사이트 설정</span>
<span class="info-box-number">관리</span>
<div class="progress">
<div class="progress-bar" style="width: 100%"></div>
</div>
<a href="/admin/settings" class="progress-description text-white">
전체 사이트 설정 변경 →
</a>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- System Status -->
<div class="row">
<div class="col-md-6">
<div class="card card-primary">
<div class="card-header">
<h3 class="card-title">
<i class="fas fa-server mr-1"></i>
시스템 상태
</h3>
</div>
<div class="card-body">
<div class="row">
<div class="col-6">
<div class="description-block border-right">
<span class="description-percentage text-success">
<i class="fas fa-caret-up"></i> 99.2%
</span>
<h5 class="description-header">서버 업타임</h5>
<span class="description-text">지난 30일</span>
</div>
</div>
<div class="col-6">
<div class="description-block">
<span class="description-percentage text-info">
<i class="fas fa-caret-up"></i> 2.3초
</span>
<h5 class="description-header">평균 응답시간</h5>
<span class="description-text">페이지 로딩</span>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card card-success">
<div class="card-header">
<h3 class="card-title">
<i class="fab fa-telegram mr-1"></i>
텔레그램 봇 상태
</h3>
</div>
<div class="card-body">
<div class="row">
<div class="col-6">
<div class="description-block border-right">
<span class="description-percentage text-success">
<i class="fas fa-check-circle"></i> 연결됨
</span>
<h5 class="description-header">봇 상태</h5>
<span class="description-text">정상 작동</span>
</div>
</div>
<div class="col-6">
<div class="description-block">
<span class="description-percentage text-info">
<i class="fas fa-paper-plane"></i> 24개
</span>
<h5 class="description-header">전송된 알림</h5>
<span class="description-text">오늘</span>
</div>
</div>
</div>
<div class="text-center mt-2">
<a href="/admin/telegram" class="btn btn-success btn-sm">
<i class="fab fa-telegram mr-1"></i>
텔레그램 설정
</a>
</div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,342 @@
<!-- Tabler Dashboard -->
<div class="row row-deck row-cards">
<!-- Stats Cards -->
<div class="col-12">
<div class="row row-cards">
<div class="col-sm-6 col-lg-3">
<div class="card card-sm">
<div class="card-body">
<div class="row align-items-center">
<div class="col-auto">
<span class="bg-primary text-white avatar">
<i class="fas fa-briefcase"></i>
</span>
</div>
<div class="col">
<div class="font-weight-medium">
포트폴리오 프로젝트
</div>
<div class="text-muted">
<%= stats.portfolioCount || 0 %>개 프로젝트
</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-sm-6 col-lg-3">
<div class="card card-sm">
<div class="card-body">
<div class="row align-items-center">
<div class="col-auto">
<span class="bg-green text-white avatar">
<i class="fas fa-cogs"></i>
</span>
</div>
<div class="col">
<div class="font-weight-medium">
제공 서비스
</div>
<div class="text-muted">
<%= stats.servicesCount || 0 %>개 서비스
</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-sm-6 col-lg-3">
<div class="card card-sm">
<div class="card-body">
<div class="row align-items-center">
<div class="col-auto">
<span class="bg-yellow text-white avatar">
<i class="fas fa-envelope"></i>
</span>
</div>
<div class="col">
<div class="font-weight-medium">
문의 메시지
</div>
<div class="text-muted">
<%= stats.contactsCount || 0 %>개 메시지
</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-sm-6 col-lg-3">
<div class="card card-sm">
<div class="card-body">
<div class="row align-items-center">
<div class="col-auto">
<span class="bg-red text-white avatar">
<i class="fas fa-users"></i>
</span>
</div>
<div class="col">
<div class="font-weight-medium">
관리자 계정
</div>
<div class="text-muted">
<%= stats.usersCount || 0 %>명 사용자
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Recent Portfolio Projects -->
<div class="col-lg-6">
<div class="card">
<div class="card-header">
<h3 class="card-title">최근 포트폴리오 프로젝트</h3>
</div>
<div class="card-body">
<% if (recentPortfolio && recentPortfolio.length > 0) { %>
<div class="divide-y">
<% recentPortfolio.forEach(function(project, index) { %>
<div class="row <%= index < recentPortfolio.length - 1 ? 'py-2' : 'pt-2' %>">
<div class="col-auto">
<span class="avatar avatar-sm bg-blue-lt">
<i class="fas fa-code"></i>
</span>
</div>
<div class="col">
<div class="text-truncate">
<strong><%= project.title %></strong>
</div>
<div class="text-muted">
<%= project.category %> •
<%= project.createdAt ? project.createdAt.toLocaleDateString('ko-KR') : '날짜 없음' %>
</div>
</div>
<div class="col-auto">
<span class="badge bg-<%= project.status === 'completed' ? 'green' : project.status === 'in-progress' ? 'yellow' : 'blue' %>">
<%= project.status === 'completed' ? '완료' : project.status === 'in-progress' ? '진행중' : '계획' %>
</span>
</div>
</div>
<% }); %>
</div>
<div class="mt-3">
<a href="/admin/portfolio" class="btn btn-primary btn-sm w-100">
모든 프로젝트 보기
</a>
</div>
<% } else { %>
<div class="empty">
<div class="empty-img">
<i class="fas fa-briefcase fa-3x text-muted"></i>
</div>
<p class="empty-title">포트폴리오가 없습니다</p>
<p class="empty-subtitle text-muted">
첫 번째 프로젝트를 추가해보세요
</p>
<div class="empty-action">
<a href="/admin/portfolio/add" class="btn btn-primary">
<i class="fas fa-plus"></i>
프로젝트 추가
</a>
</div>
</div>
<% } %>
</div>
</div>
</div>
<!-- Recent Contact Messages -->
<div class="col-lg-6">
<div class="card">
<div class="card-header">
<h3 class="card-title">최근 문의 메시지</h3>
</div>
<div class="card-body">
<% if (recentContacts && recentContacts.length > 0) { %>
<div class="divide-y">
<% recentContacts.forEach(function(contact, index) { %>
<div class="row <%= index < recentContacts.length - 1 ? 'py-2' : 'pt-2' %>">
<div class="col-auto">
<span class="avatar avatar-sm">
<%= contact.name ? contact.name.charAt(0).toUpperCase() : 'U' %>
</span>
</div>
<div class="col">
<div class="text-truncate">
<strong><%= contact.name %></strong>
</div>
<div class="text-muted">
<%= contact.email %> •
<%= contact.createdAt ? contact.createdAt.toLocaleDateString('ko-KR') : '날짜 없음' %>
</div>
</div>
<div class="col-auto">
<span class="badge bg-<%= contact.status === 'replied' ? 'green' : contact.status === 'pending' ? 'yellow' : 'blue' %>">
<%= contact.status === 'replied' ? '답변완료' : contact.status === 'pending' ? '대기중' : '신규' %>
</span>
</div>
</div>
<% }); %>
</div>
<div class="mt-3">
<a href="/admin/contacts" class="btn btn-warning btn-sm w-100">
모든 문의 보기
</a>
</div>
<% } else { %>
<div class="empty">
<div class="empty-img">
<i class="fas fa-envelope fa-3x text-muted"></i>
</div>
<p class="empty-title">새로운 문의가 없습니다</p>
<p class="empty-subtitle text-muted">
고객 문의가 들어오면 여기에 표시됩니다
</p>
</div>
<% } %>
</div>
</div>
</div>
<!-- Quick Actions -->
<div class="col-12">
<div class="card">
<div class="card-header">
<h3 class="card-title">빠른 작업</h3>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-3">
<a href="/admin/portfolio/add" class="card card-link bg-primary-lt">
<div class="card-body text-center">
<div class="text-primary mb-3">
<i class="fas fa-plus fa-2x"></i>
</div>
<div class="font-weight-medium">새 프로젝트</div>
<div class="text-muted">포트폴리오에 추가</div>
</div>
</a>
</div>
<div class="col-md-3">
<a href="/admin/services" class="card card-link bg-green-lt">
<div class="card-body text-center">
<div class="text-green mb-3">
<i class="fas fa-cog fa-2x"></i>
</div>
<div class="font-weight-medium">서비스 관리</div>
<div class="text-muted">가격 및 내용 수정</div>
</div>
</a>
</div>
<div class="col-md-3">
<a href="/admin/media" class="card card-link bg-yellow-lt">
<div class="card-body text-center">
<div class="text-yellow mb-3">
<i class="fas fa-images fa-2x"></i>
</div>
<div class="font-weight-medium">미디어 업로드</div>
<div class="text-muted">이미지 및 파일 관리</div>
</div>
</a>
</div>
<div class="col-md-3">
<a href="/admin/settings" class="card card-link bg-red-lt">
<div class="card-body text-center">
<div class="text-red mb-3">
<i class="fas fa-wrench fa-2x"></i>
</div>
<div class="font-weight-medium">사이트 설정</div>
<div class="text-muted">전체 설정 관리</div>
</div>
</a>
</div>
</div>
</div>
</div>
</div>
<!-- System Status & Analytics -->
<div class="col-lg-6">
<div class="card">
<div class="card-header">
<h3 class="card-title">시스템 상태</h3>
</div>
<div class="card-body">
<div class="row">
<div class="col-6">
<div class="d-flex align-items-center">
<div class="me-3">
<i class="fas fa-server fa-2x text-green"></i>
</div>
<div>
<div class="h4 mb-0">99.2%</div>
<div class="text-muted">서버 업타임</div>
</div>
</div>
</div>
<div class="col-6">
<div class="d-flex align-items-center">
<div class="me-3">
<i class="fas fa-clock fa-2x text-blue"></i>
</div>
<div>
<div class="h4 mb-0">2.3초</div>
<div class="text-muted">평균 응답시간</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Telegram Bot Status -->
<div class="col-lg-6">
<div class="card">
<div class="card-header">
<h3 class="card-title">텔레그램 봇</h3>
</div>
<div class="card-body">
<div class="row">
<div class="col-6">
<div class="d-flex align-items-center">
<div class="me-3">
<i class="fab fa-telegram fa-2x text-green"></i>
</div>
<div>
<div class="h4 mb-0 text-green">연결됨</div>
<div class="text-muted">봇 상태</div>
</div>
</div>
</div>
<div class="col-6">
<div class="d-flex align-items-center">
<div class="me-3">
<i class="fas fa-paper-plane fa-2x text-blue"></i>
</div>
<div>
<div class="h4 mb-0">24</div>
<div class="text-muted">오늘 전송</div>
</div>
</div>
</div>
</div>
<div class="mt-3">
<a href="/admin/telegram" class="btn btn-outline-primary w-100">
텔레그램 설정
</a>
</div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,342 @@
<!-- Tabler Dashboard -->
<div class="row row-deck row-cards">
<!-- Stats Cards -->
<div class="col-12">
<div class="row row-cards">
<div class="col-sm-6 col-lg-3">
<div class="card card-sm">
<div class="card-body">
<div class="row align-items-center">
<div class="col-auto">
<span class="bg-primary text-white avatar">
<i class="fas fa-briefcase"></i>
</span>
</div>
<div class="col">
<div class="font-weight-medium">
포트폴리오 프로젝트
</div>
<div class="text-muted">
<%= stats.portfolioCount || 0 %>개 프로젝트
</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-sm-6 col-lg-3">
<div class="card card-sm">
<div class="card-body">
<div class="row align-items-center">
<div class="col-auto">
<span class="bg-green text-white avatar">
<i class="fas fa-cogs"></i>
</span>
</div>
<div class="col">
<div class="font-weight-medium">
제공 서비스
</div>
<div class="text-muted">
<%= stats.servicesCount || 0 %>개 서비스
</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-sm-6 col-lg-3">
<div class="card card-sm">
<div class="card-body">
<div class="row align-items-center">
<div class="col-auto">
<span class="bg-yellow text-white avatar">
<i class="fas fa-envelope"></i>
</span>
</div>
<div class="col">
<div class="font-weight-medium">
문의 메시지
</div>
<div class="text-muted">
<%= stats.contactsCount || 0 %>개 메시지
</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-sm-6 col-lg-3">
<div class="card card-sm">
<div class="card-body">
<div class="row align-items-center">
<div class="col-auto">
<span class="bg-red text-white avatar">
<i class="fas fa-users"></i>
</span>
</div>
<div class="col">
<div class="font-weight-medium">
관리자 계정
</div>
<div class="text-muted">
<%= stats.usersCount || 0 %>명 사용자
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Recent Portfolio Projects -->
<div class="col-lg-6">
<div class="card">
<div class="card-header">
<h3 class="card-title">최근 포트폴리오 프로젝트</h3>
</div>
<div class="card-body">
<% if (recentPortfolio && recentPortfolio.length > 0) { %>
<div class="divide-y">
<% recentPortfolio.forEach(function(project, index) { %>
<div class="row <%= index < recentPortfolio.length - 1 ? 'py-2' : 'pt-2' %>">
<div class="col-auto">
<span class="avatar avatar-sm bg-blue-lt">
<i class="fas fa-code"></i>
</span>
</div>
<div class="col">
<div class="text-truncate">
<strong><%= project.title %></strong>
</div>
<div class="text-muted">
<%= project.category %> •
<%= project.createdAt ? project.createdAt.toLocaleDateString('ko-KR') : '날짜 없음' %>
</div>
</div>
<div class="col-auto">
<span class="badge bg-<%= project.status === 'completed' ? 'green' : project.status === 'in-progress' ? 'yellow' : 'blue' %>">
<%= project.status === 'completed' ? '완료' : project.status === 'in-progress' ? '진행중' : '계획' %>
</span>
</div>
</div>
<% }); %>
</div>
<div class="mt-3">
<a href="/admin/portfolio" class="btn btn-primary btn-sm w-100">
모든 프로젝트 보기
</a>
</div>
<% } else { %>
<div class="empty">
<div class="empty-img">
<i class="fas fa-briefcase fa-3x text-muted"></i>
</div>
<p class="empty-title">포트폴리오가 없습니다</p>
<p class="empty-subtitle text-muted">
첫 번째 프로젝트를 추가해보세요
</p>
<div class="empty-action">
<a href="/admin/portfolio/add" class="btn btn-primary">
<i class="fas fa-plus"></i>
프로젝트 추가
</a>
</div>
</div>
<% } %>
</div>
</div>
</div>
<!-- Recent Contact Messages -->
<div class="col-lg-6">
<div class="card">
<div class="card-header">
<h3 class="card-title">최근 문의 메시지</h3>
</div>
<div class="card-body">
<% if (recentContacts && recentContacts.length > 0) { %>
<div class="divide-y">
<% recentContacts.forEach(function(contact, index) { %>
<div class="row <%= index < recentContacts.length - 1 ? 'py-2' : 'pt-2' %>">
<div class="col-auto">
<span class="avatar avatar-sm">
<%= contact.name ? contact.name.charAt(0).toUpperCase() : 'U' %>
</span>
</div>
<div class="col">
<div class="text-truncate">
<strong><%= contact.name %></strong>
</div>
<div class="text-muted">
<%= contact.email %> •
<%= contact.createdAt ? contact.createdAt.toLocaleDateString('ko-KR') : '날짜 없음' %>
</div>
</div>
<div class="col-auto">
<span class="badge bg-<%= contact.status === 'replied' ? 'green' : contact.status === 'pending' ? 'yellow' : 'blue' %>">
<%= contact.status === 'replied' ? '답변완료' : contact.status === 'pending' ? '대기중' : '신규' %>
</span>
</div>
</div>
<% }); %>
</div>
<div class="mt-3">
<a href="/admin/contacts" class="btn btn-warning btn-sm w-100">
모든 문의 보기
</a>
</div>
<% } else { %>
<div class="empty">
<div class="empty-img">
<i class="fas fa-envelope fa-3x text-muted"></i>
</div>
<p class="empty-title">새로운 문의가 없습니다</p>
<p class="empty-subtitle text-muted">
고객 문의가 들어오면 여기에 표시됩니다
</p>
</div>
<% } %>
</div>
</div>
</div>
<!-- Quick Actions -->
<div class="col-12">
<div class="card">
<div class="card-header">
<h3 class="card-title">빠른 작업</h3>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-3">
<a href="/admin/portfolio/add" class="card card-link bg-primary-lt">
<div class="card-body text-center">
<div class="text-primary mb-3">
<i class="fas fa-plus fa-2x"></i>
</div>
<div class="font-weight-medium">새 프로젝트</div>
<div class="text-muted">포트폴리오에 추가</div>
</div>
</a>
</div>
<div class="col-md-3">
<a href="/admin/services" class="card card-link bg-green-lt">
<div class="card-body text-center">
<div class="text-green mb-3">
<i class="fas fa-cog fa-2x"></i>
</div>
<div class="font-weight-medium">서비스 관리</div>
<div class="text-muted">가격 및 내용 수정</div>
</div>
</a>
</div>
<div class="col-md-3">
<a href="/admin/media" class="card card-link bg-yellow-lt">
<div class="card-body text-center">
<div class="text-yellow mb-3">
<i class="fas fa-images fa-2x"></i>
</div>
<div class="font-weight-medium">미디어 업로드</div>
<div class="text-muted">이미지 및 파일 관리</div>
</div>
</a>
</div>
<div class="col-md-3">
<a href="/admin/settings" class="card card-link bg-red-lt">
<div class="card-body text-center">
<div class="text-red mb-3">
<i class="fas fa-wrench fa-2x"></i>
</div>
<div class="font-weight-medium">사이트 설정</div>
<div class="text-muted">전체 설정 관리</div>
</div>
</a>
</div>
</div>
</div>
</div>
</div>
<!-- System Status & Analytics -->
<div class="col-lg-6">
<div class="card">
<div class="card-header">
<h3 class="card-title">시스템 상태</h3>
</div>
<div class="card-body">
<div class="row">
<div class="col-6">
<div class="d-flex align-items-center">
<div class="me-3">
<i class="fas fa-server fa-2x text-green"></i>
</div>
<div>
<div class="h4 mb-0">99.2%</div>
<div class="text-muted">서버 업타임</div>
</div>
</div>
</div>
<div class="col-6">
<div class="d-flex align-items-center">
<div class="me-3">
<i class="fas fa-clock fa-2x text-blue"></i>
</div>
<div>
<div class="h4 mb-0">2.3초</div>
<div class="text-muted">평균 응답시간</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Telegram Bot Status -->
<div class="col-lg-6">
<div class="card">
<div class="card-header">
<h3 class="card-title">텔레그램 봇</h3>
</div>
<div class="card-body">
<div class="row">
<div class="col-6">
<div class="d-flex align-items-center">
<div class="me-3">
<i class="fab fa-telegram fa-2x text-green"></i>
</div>
<div>
<div class="h4 mb-0 text-green">연결됨</div>
<div class="text-muted">봇 상태</div>
</div>
</div>
</div>
<div class="col-6">
<div class="d-flex align-items-center">
<div class="me-3">
<i class="fas fa-paper-plane fa-2x text-blue"></i>
</div>
<div>
<div class="h4 mb-0">24</div>
<div class="text-muted">오늘 전송</div>
</div>
</div>
</div>
</div>
<div class="mt-3">
<a href="/admin/telegram" class="btn btn-outline-primary w-100">
텔레그램 설정
</a>
</div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,360 @@
<!-- Dashboard Content -->
<div class="row">
<!-- Stats Cards -->
<div class="col-lg-3 col-6">
<div class="info-box">
<span class="info-box-icon bg-info elevation-1">
<i class="fas fa-briefcase"></i>
</span>
<div class="info-box-content">
<span class="info-box-text">포트폴리오 프로젝트</span>
<span class="info-box-number"><%= stats.portfolioCount || 0 %></span>
<div class="progress">
<div class="progress-bar bg-info" style="width: 70%"></div>
</div>
<span class="progress-description">
70% 완료된 프로젝트
</span>
</div>
</div>
</div>
<div class="col-lg-3 col-6">
<div class="info-box">
<span class="info-box-icon bg-success elevation-1">
<i class="fas fa-cogs"></i>
</span>
<div class="info-box-content">
<span class="info-box-text">제공 서비스</span>
<span class="info-box-number"><%= stats.servicesCount || 0 %></span>
<div class="progress">
<div class="progress-bar bg-success" style="width: 100%"></div>
</div>
<span class="progress-description">
모든 서비스 활성화
</span>
</div>
</div>
</div>
<div class="col-lg-3 col-6">
<div class="info-box">
<span class="info-box-icon bg-warning elevation-1">
<i class="fas fa-envelope"></i>
</span>
<div class="info-box-content">
<span class="info-box-text">문의 메시지</span>
<span class="info-box-number"><%= stats.contactsCount || 0 %></span>
<div class="progress">
<div class="progress-bar bg-warning" style="width: 60%"></div>
</div>
<span class="progress-description">
60% 응답 완료
</span>
</div>
</div>
</div>
<div class="col-lg-3 col-6">
<div class="info-box">
<span class="info-box-icon bg-danger elevation-1">
<i class="fas fa-users"></i>
</span>
<div class="info-box-content">
<span class="info-box-text">관리자 계정</span>
<span class="info-box-number"><%= stats.usersCount || 0 %></span>
<div class="progress">
<div class="progress-bar bg-danger" style="width: 100%"></div>
</div>
<span class="progress-description">
모든 계정 활성화
</span>
</div>
</div>
</div>
</div>
<!-- Main Content Row -->
<div class="row">
<!-- Recent Portfolio Projects -->
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h3 class="card-title">
<i class="fas fa-briefcase mr-1"></i>
최근 포트폴리오 프로젝트
</h3>
<div class="card-tools">
<button type="button" class="btn btn-tool" data-card-widget="collapse">
<i class="fas fa-minus"></i>
</button>
</div>
</div>
<div class="card-body">
<% if (recentPortfolio && recentPortfolio.length > 0) { %>
<ul class="list-unstyled">
<% recentPortfolio.forEach(function(project, index) { %>
<li class="d-flex align-items-center <%= index < recentPortfolio.length - 1 ? 'border-bottom pb-3 mb-3' : '' %>">
<div class="flex-shrink-0">
<span class="badge badge-info badge-pill">
<i class="fas fa-code"></i>
</span>
</div>
<div class="flex-grow-1 ml-3">
<h6 class="mb-1 font-weight-bold"><%= project.title %></h6>
<p class="text-muted mb-1"><%= project.category %></p>
<small class="text-muted">
<i class="fas fa-calendar mr-1"></i>
<%= project.createdAt ? project.createdAt.toLocaleDateString('ko-KR') : '날짜 없음' %>
</small>
</div>
<div class="flex-shrink-0">
<span class="badge badge-<%= project.status === 'completed' ? 'success' : project.status === 'in-progress' ? 'warning' : 'secondary' %>">
<%= project.status === 'completed' ? '완료' : project.status === 'in-progress' ? '진행중' : '계획' %>
</span>
</div>
</li>
<% }); %>
</ul>
<div class="text-center mt-3">
<a href="/admin/portfolio" class="btn btn-primary btn-sm">
<i class="fas fa-eye mr-1"></i>
모든 프로젝트 보기
</a>
</div>
<% } else { %>
<div class="text-center py-4">
<i class="fas fa-briefcase fa-3x text-muted mb-3"></i>
<p class="text-muted">아직 포트폴리오 프로젝트가 없습니다.</p>
<a href="/admin/portfolio/add" class="btn btn-primary btn-sm">
<i class="fas fa-plus mr-1"></i>
첫 번째 프로젝트 추가
</a>
</div>
<% } %>
</div>
</div>
</div>
<!-- Recent Contact Messages -->
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h3 class="card-title">
<i class="fas fa-envelope mr-1"></i>
최근 문의 메시지
</h3>
<div class="card-tools">
<button type="button" class="btn btn-tool" data-card-widget="collapse">
<i class="fas fa-minus"></i>
</button>
</div>
</div>
<div class="card-body">
<% if (recentContacts && recentContacts.length > 0) { %>
<ul class="list-unstyled">
<% recentContacts.forEach(function(contact, index) { %>
<li class="d-flex align-items-center <%= index < recentContacts.length - 1 ? 'border-bottom pb-3 mb-3' : '' %>">
<div class="flex-shrink-0">
<div class="bg-primary text-white rounded-circle d-flex align-items-center justify-content-center" style="width: 40px; height: 40px;">
<%= contact.name ? contact.name.charAt(0).toUpperCase() : 'U' %>
</div>
</div>
<div class="flex-grow-1 ml-3">
<h6 class="mb-1 font-weight-bold"><%= contact.name %></h6>
<p class="text-muted mb-1"><%= contact.email %></p>
<small class="text-muted">
<i class="fas fa-clock mr-1"></i>
<%= contact.createdAt ? contact.createdAt.toLocaleDateString('ko-KR') : '날짜 없음' %>
</small>
</div>
<div class="flex-shrink-0">
<span class="badge badge-<%= contact.status === 'replied' ? 'success' : contact.status === 'pending' ? 'warning' : 'secondary' %>">
<%= contact.status === 'replied' ? '답변완료' : contact.status === 'pending' ? '대기중' : '신규' %>
</span>
</div>
</li>
<% }); %>
</ul>
<div class="text-center mt-3">
<a href="/admin/contacts" class="btn btn-warning btn-sm">
<i class="fas fa-envelope-open mr-1"></i>
모든 문의 보기
</a>
</div>
<% } else { %>
<div class="text-center py-4">
<i class="fas fa-envelope fa-3x text-muted mb-3"></i>
<p class="text-muted">새로운 문의가 없습니다.</p>
<small class="text-muted">고객 문의가 들어오면 여기에 표시됩니다.</small>
</div>
<% } %>
</div>
</div>
</div>
</div>
<!-- Quick Actions & Tools -->
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header">
<h3 class="card-title">
<i class="fas fa-bolt mr-1"></i>
빠른 작업
</h3>
</div>
<div class="card-body">
<div class="row">
<div class="col-lg-3 col-md-6">
<div class="info-box bg-gradient-info">
<span class="info-box-icon">
<i class="fas fa-plus"></i>
</span>
<div class="info-box-content">
<span class="info-box-text">새 프로젝트</span>
<span class="info-box-number">추가하기</span>
<div class="progress">
<div class="progress-bar" style="width: 100%"></div>
</div>
<a href="/admin/portfolio/add" class="progress-description text-white">
포트폴리오에 새 프로젝트 추가 →
</a>
</div>
</div>
</div>
<div class="col-lg-3 col-md-6">
<div class="info-box bg-gradient-success">
<span class="info-box-icon">
<i class="fas fa-cog"></i>
</span>
<div class="info-box-content">
<span class="info-box-text">서비스 관리</span>
<span class="info-box-number">설정</span>
<div class="progress">
<div class="progress-bar" style="width: 100%"></div>
</div>
<a href="/admin/services" class="progress-description text-white">
서비스 가격 및 내용 수정 →
</a>
</div>
</div>
</div>
<div class="col-lg-3 col-md-6">
<div class="info-box bg-gradient-warning">
<span class="info-box-icon">
<i class="fas fa-images"></i>
</span>
<div class="info-box-content">
<span class="info-box-text">미디어</span>
<span class="info-box-number">업로드</span>
<div class="progress">
<div class="progress-bar" style="width: 100%"></div>
</div>
<a href="/admin/media" class="progress-description text-white">
이미지 및 파일 관리 →
</a>
</div>
</div>
</div>
<div class="col-lg-3 col-md-6">
<div class="info-box bg-gradient-danger">
<span class="info-box-icon">
<i class="fas fa-wrench"></i>
</span>
<div class="info-box-content">
<span class="info-box-text">사이트 설정</span>
<span class="info-box-number">관리</span>
<div class="progress">
<div class="progress-bar" style="width: 100%"></div>
</div>
<a href="/admin/settings" class="progress-description text-white">
전체 사이트 설정 변경 →
</a>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- System Status -->
<div class="row">
<div class="col-md-6">
<div class="card card-primary">
<div class="card-header">
<h3 class="card-title">
<i class="fas fa-server mr-1"></i>
시스템 상태
</h3>
</div>
<div class="card-body">
<div class="row">
<div class="col-6">
<div class="description-block border-right">
<span class="description-percentage text-success">
<i class="fas fa-caret-up"></i> 99.2%
</span>
<h5 class="description-header">서버 업타임</h5>
<span class="description-text">지난 30일</span>
</div>
</div>
<div class="col-6">
<div class="description-block">
<span class="description-percentage text-info">
<i class="fas fa-caret-up"></i> 2.3초
</span>
<h5 class="description-header">평균 응답시간</h5>
<span class="description-text">페이지 로딩</span>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card card-success">
<div class="card-header">
<h3 class="card-title">
<i class="fab fa-telegram mr-1"></i>
텔레그램 봇 상태
</h3>
</div>
<div class="card-body">
<div class="row">
<div class="col-6">
<div class="description-block border-right">
<span class="description-percentage text-success">
<i class="fas fa-check-circle"></i> 연결됨
</span>
<h5 class="description-header">봇 상태</h5>
<span class="description-text">정상 작동</span>
</div>
</div>
<div class="col-6">
<div class="description-block">
<span class="description-percentage text-info">
<i class="fas fa-paper-plane"></i> 24개
</span>
<h5 class="description-header">전송된 알림</h5>
<span class="description-text">오늘</span>
</div>
</div>
</div>
<div class="text-center mt-2">
<a href="/admin/telegram" class="btn btn-success btn-sm">
<i class="fab fa-telegram mr-1"></i>
텔레그램 설정
</a>
</div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,360 @@
<!-- Dashboard Content -->
<div class="row">
<!-- Stats Cards -->
<div class="col-lg-3 col-6">
<div class="info-box">
<span class="info-box-icon bg-info elevation-1">
<i class="fas fa-briefcase"></i>
</span>
<div class="info-box-content">
<span class="info-box-text">포트폴리오 프로젝트</span>
<span class="info-box-number"><%= stats.portfolioCount || 0 %></span>
<div class="progress">
<div class="progress-bar bg-info" style="width: 70%"></div>
</div>
<span class="progress-description">
70% 완료된 프로젝트
</span>
</div>
</div>
</div>
<div class="col-lg-3 col-6">
<div class="info-box">
<span class="info-box-icon bg-success elevation-1">
<i class="fas fa-cogs"></i>
</span>
<div class="info-box-content">
<span class="info-box-text">제공 서비스</span>
<span class="info-box-number"><%= stats.servicesCount || 0 %></span>
<div class="progress">
<div class="progress-bar bg-success" style="width: 100%"></div>
</div>
<span class="progress-description">
모든 서비스 활성화
</span>
</div>
</div>
</div>
<div class="col-lg-3 col-6">
<div class="info-box">
<span class="info-box-icon bg-warning elevation-1">
<i class="fas fa-envelope"></i>
</span>
<div class="info-box-content">
<span class="info-box-text">문의 메시지</span>
<span class="info-box-number"><%= stats.contactsCount || 0 %></span>
<div class="progress">
<div class="progress-bar bg-warning" style="width: 60%"></div>
</div>
<span class="progress-description">
60% 응답 완료
</span>
</div>
</div>
</div>
<div class="col-lg-3 col-6">
<div class="info-box">
<span class="info-box-icon bg-danger elevation-1">
<i class="fas fa-users"></i>
</span>
<div class="info-box-content">
<span class="info-box-text">관리자 계정</span>
<span class="info-box-number"><%= stats.usersCount || 0 %></span>
<div class="progress">
<div class="progress-bar bg-danger" style="width: 100%"></div>
</div>
<span class="progress-description">
모든 계정 활성화
</span>
</div>
</div>
</div>
</div>
<!-- Main Content Row -->
<div class="row">
<!-- Recent Portfolio Projects -->
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h3 class="card-title">
<i class="fas fa-briefcase mr-1"></i>
최근 포트폴리오 프로젝트
</h3>
<div class="card-tools">
<button type="button" class="btn btn-tool" data-card-widget="collapse">
<i class="fas fa-minus"></i>
</button>
</div>
</div>
<div class="card-body">
<% if (recentPortfolio && recentPortfolio.length > 0) { %>
<ul class="list-unstyled">
<% recentPortfolio.forEach(function(project, index) { %>
<li class="d-flex align-items-center <%= index < recentPortfolio.length - 1 ? 'border-bottom pb-3 mb-3' : '' %>">
<div class="flex-shrink-0">
<span class="badge badge-info badge-pill">
<i class="fas fa-code"></i>
</span>
</div>
<div class="flex-grow-1 ml-3">
<h6 class="mb-1 font-weight-bold"><%= project.title %></h6>
<p class="text-muted mb-1"><%= project.category %></p>
<small class="text-muted">
<i class="fas fa-calendar mr-1"></i>
<%= project.createdAt ? project.createdAt.toLocaleDateString('ko-KR') : '날짜 없음' %>
</small>
</div>
<div class="flex-shrink-0">
<span class="badge badge-<%= project.status === 'completed' ? 'success' : project.status === 'in-progress' ? 'warning' : 'secondary' %>">
<%= project.status === 'completed' ? '완료' : project.status === 'in-progress' ? '진행중' : '계획' %>
</span>
</div>
</li>
<% }); %>
</ul>
<div class="text-center mt-3">
<a href="/admin/portfolio" class="btn btn-primary btn-sm">
<i class="fas fa-eye mr-1"></i>
모든 프로젝트 보기
</a>
</div>
<% } else { %>
<div class="text-center py-4">
<i class="fas fa-briefcase fa-3x text-muted mb-3"></i>
<p class="text-muted">아직 포트폴리오 프로젝트가 없습니다.</p>
<a href="/admin/portfolio/add" class="btn btn-primary btn-sm">
<i class="fas fa-plus mr-1"></i>
첫 번째 프로젝트 추가
</a>
</div>
<% } %>
</div>
</div>
</div>
<!-- Recent Contact Messages -->
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h3 class="card-title">
<i class="fas fa-envelope mr-1"></i>
최근 문의 메시지
</h3>
<div class="card-tools">
<button type="button" class="btn btn-tool" data-card-widget="collapse">
<i class="fas fa-minus"></i>
</button>
</div>
</div>
<div class="card-body">
<% if (recentContacts && recentContacts.length > 0) { %>
<ul class="list-unstyled">
<% recentContacts.forEach(function(contact, index) { %>
<li class="d-flex align-items-center <%= index < recentContacts.length - 1 ? 'border-bottom pb-3 mb-3' : '' %>">
<div class="flex-shrink-0">
<div class="bg-primary text-white rounded-circle d-flex align-items-center justify-content-center" style="width: 40px; height: 40px;">
<%= contact.name ? contact.name.charAt(0).toUpperCase() : 'U' %>
</div>
</div>
<div class="flex-grow-1 ml-3">
<h6 class="mb-1 font-weight-bold"><%= contact.name %></h6>
<p class="text-muted mb-1"><%= contact.email %></p>
<small class="text-muted">
<i class="fas fa-clock mr-1"></i>
<%= contact.createdAt ? contact.createdAt.toLocaleDateString('ko-KR') : '날짜 없음' %>
</small>
</div>
<div class="flex-shrink-0">
<span class="badge badge-<%= contact.status === 'replied' ? 'success' : contact.status === 'pending' ? 'warning' : 'secondary' %>">
<%= contact.status === 'replied' ? '답변완료' : contact.status === 'pending' ? '대기중' : '신규' %>
</span>
</div>
</li>
<% }); %>
</ul>
<div class="text-center mt-3">
<a href="/admin/contacts" class="btn btn-warning btn-sm">
<i class="fas fa-envelope-open mr-1"></i>
모든 문의 보기
</a>
</div>
<% } else { %>
<div class="text-center py-4">
<i class="fas fa-envelope fa-3x text-muted mb-3"></i>
<p class="text-muted">새로운 문의가 없습니다.</p>
<small class="text-muted">고객 문의가 들어오면 여기에 표시됩니다.</small>
</div>
<% } %>
</div>
</div>
</div>
</div>
<!-- Quick Actions & Tools -->
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header">
<h3 class="card-title">
<i class="fas fa-bolt mr-1"></i>
빠른 작업
</h3>
</div>
<div class="card-body">
<div class="row">
<div class="col-lg-3 col-md-6">
<div class="info-box bg-gradient-info">
<span class="info-box-icon">
<i class="fas fa-plus"></i>
</span>
<div class="info-box-content">
<span class="info-box-text">새 프로젝트</span>
<span class="info-box-number">추가하기</span>
<div class="progress">
<div class="progress-bar" style="width: 100%"></div>
</div>
<a href="/admin/portfolio/add" class="progress-description text-white">
포트폴리오에 새 프로젝트 추가 →
</a>
</div>
</div>
</div>
<div class="col-lg-3 col-md-6">
<div class="info-box bg-gradient-success">
<span class="info-box-icon">
<i class="fas fa-cog"></i>
</span>
<div class="info-box-content">
<span class="info-box-text">서비스 관리</span>
<span class="info-box-number">설정</span>
<div class="progress">
<div class="progress-bar" style="width: 100%"></div>
</div>
<a href="/admin/services" class="progress-description text-white">
서비스 가격 및 내용 수정 →
</a>
</div>
</div>
</div>
<div class="col-lg-3 col-md-6">
<div class="info-box bg-gradient-warning">
<span class="info-box-icon">
<i class="fas fa-images"></i>
</span>
<div class="info-box-content">
<span class="info-box-text">미디어</span>
<span class="info-box-number">업로드</span>
<div class="progress">
<div class="progress-bar" style="width: 100%"></div>
</div>
<a href="/admin/media" class="progress-description text-white">
이미지 및 파일 관리 →
</a>
</div>
</div>
</div>
<div class="col-lg-3 col-md-6">
<div class="info-box bg-gradient-danger">
<span class="info-box-icon">
<i class="fas fa-wrench"></i>
</span>
<div class="info-box-content">
<span class="info-box-text">사이트 설정</span>
<span class="info-box-number">관리</span>
<div class="progress">
<div class="progress-bar" style="width: 100%"></div>
</div>
<a href="/admin/settings" class="progress-description text-white">
전체 사이트 설정 변경 →
</a>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- System Status -->
<div class="row">
<div class="col-md-6">
<div class="card card-primary">
<div class="card-header">
<h3 class="card-title">
<i class="fas fa-server mr-1"></i>
시스템 상태
</h3>
</div>
<div class="card-body">
<div class="row">
<div class="col-6">
<div class="description-block border-right">
<span class="description-percentage text-success">
<i class="fas fa-caret-up"></i> 99.2%
</span>
<h5 class="description-header">서버 업타임</h5>
<span class="description-text">지난 30일</span>
</div>
</div>
<div class="col-6">
<div class="description-block">
<span class="description-percentage text-info">
<i class="fas fa-caret-up"></i> 2.3초
</span>
<h5 class="description-header">평균 응답시간</h5>
<span class="description-text">페이지 로딩</span>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card card-success">
<div class="card-header">
<h3 class="card-title">
<i class="fab fa-telegram mr-1"></i>
텔레그램 봇 상태
</h3>
</div>
<div class="card-body">
<div class="row">
<div class="col-6">
<div class="description-block border-right">
<span class="description-percentage text-success">
<i class="fas fa-check-circle"></i> 연결됨
</span>
<h5 class="description-header">봇 상태</h5>
<span class="description-text">정상 작동</span>
</div>
</div>
<div class="col-6">
<div class="description-block">
<span class="description-percentage text-info">
<i class="fas fa-paper-plane"></i> 24개
</span>
<h5 class="description-header">전송된 알림</h5>
<span class="description-text">오늘</span>
</div>
</div>
</div>
<div class="text-center mt-2">
<a href="/admin/telegram" class="btn btn-success btn-sm">
<i class="fab fa-telegram mr-1"></i>
텔레그램 설정
</a>
</div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,268 @@
<!DOCTYPE html>
<html lang="<%= currentLanguage %>">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><%= title %> - SmartSolTech Admin</title>
<!-- AdminLTE CSS -->
<link rel="stylesheet" href="/node_modules/admin-lte/dist/css/adminlte.min.css">
<!-- Font Awesome -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<!-- Google Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@300;400;500;700&display=swap" rel="stylesheet">
<!-- Custom Korean Admin Styles -->
<style>
body, .content-wrapper, .main-sidebar {
font-family: 'Noto Sans KR', 'Malgun Gothic', 'Apple SD Gothic Neo', sans-serif;
}
.brand-text {
font-weight: 700;
color: #007bff !important;
}
.nav-sidebar .nav-link {
border-radius: 8px;
margin: 2px 0;
}
.nav-sidebar .nav-link.active {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.content-header h1 {
font-weight: 600;
color: #2c3e50;
}
.card {
border-radius: 12px;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
}
.btn {
border-radius: 8px;
font-weight: 500;
}
.info-box {
border-radius: 12px;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
}
.info-box-icon {
border-radius: 12px 0 0 12px;
}
.navbar-light {
background: white !important;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.main-sidebar {
background: linear-gradient(180deg, #f8f9fa 0%, #e9ecef 100%);
}
.sidebar-dark-primary .nav-sidebar > .nav-item > .nav-link:hover {
background-color: rgba(255,255,255,0.1);
}
</style>
</head>
<body class="hold-transition sidebar-mini layout-fixed">
<div class="wrapper">
<!-- Navbar -->
<nav class="main-header navbar navbar-expand navbar-light">
<!-- Left navbar links -->
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link" data-widget="pushmenu" href="#" role="button">
<i class="fas fa-bars"></i>
</a>
</li>
<li class="nav-item d-none d-sm-inline-block">
<a href="/" class="nav-link" target="_blank">
<i class="fas fa-external-link-alt mr-1"></i>
사이트 보기
</a>
</li>
</ul>
<!-- Right navbar links -->
<ul class="navbar-nav ml-auto">
<!-- User Menu -->
<li class="nav-item dropdown">
<a class="nav-link" data-toggle="dropdown" href="#">
<i class="far fa-user mr-1"></i>
<%= user ? user.name : '관리자' %>
</a>
<div class="dropdown-menu dropdown-menu-lg dropdown-menu-right">
<div class="dropdown-divider"></div>
<a href="/admin/settings" class="dropdown-item">
<i class="fas fa-cog mr-2"></i> 설정
</a>
<div class="dropdown-divider"></div>
<form action="/admin/logout" method="post" class="dropdown-item p-0">
<button type="submit" class="btn btn-link text-left w-100 text-danger">
<i class="fas fa-sign-out-alt mr-2"></i> 로그아웃
</button>
</form>
</div>
</li>
</ul>
</nav>
<!-- Main Sidebar Container -->
<aside class="main-sidebar sidebar-light-primary elevation-4">
<!-- Brand Logo -->
<a href="/admin/dashboard" class="brand-link">
<img src="/images/icons/icon-192x192.png" alt="SmartSolTech" class="brand-image img-circle elevation-3" style="opacity: .8">
<span class="brand-text font-weight-bold">SmartSolTech</span>
</a>
<!-- Sidebar -->
<div class="sidebar">
<!-- Sidebar Menu -->
<nav class="mt-3">
<ul class="nav nav-pills nav-sidebar flex-column" data-widget="treeview" role="menu" data-accordion="false">
<li class="nav-item">
<a href="/admin/dashboard" class="nav-link <%= currentPage === 'dashboard' ? 'active' : '' %>">
<i class="nav-icon fas fa-tachometer-alt"></i>
<p>대시보드</p>
</a>
</li>
<li class="nav-item">
<a href="/admin/portfolio" class="nav-link <%= currentPage === 'portfolio' ? 'active' : '' %>">
<i class="nav-icon fas fa-briefcase"></i>
<p>
포트폴리오
<span class="badge badge-info right"><%= stats?.portfolioCount || 0 %></span>
</p>
</a>
</li>
<li class="nav-item">
<a href="/admin/services" class="nav-link <%= currentPage === 'services' ? 'active' : '' %>">
<i class="nav-icon fas fa-cogs"></i>
<p>
서비스 관리
<span class="badge badge-success right"><%= stats?.servicesCount || 0 %></span>
</p>
</a>
</li>
<li class="nav-item">
<a href="/admin/contacts" class="nav-link <%= currentPage === 'contacts' ? 'active' : '' %>">
<i class="nav-icon fas fa-envelope"></i>
<p>
문의 관리
<span class="badge badge-warning right"><%= stats?.contactsCount || 0 %></span>
</p>
</a>
</li>
<li class="nav-item">
<a href="/admin/media" class="nav-link <%= currentPage === 'media' ? 'active' : '' %>">
<i class="nav-icon fas fa-images"></i>
<p>미디어 관리</p>
</a>
</li>
<li class="nav-header">시스템 설정</li>
<li class="nav-item">
<a href="/admin/settings" class="nav-link <%= currentPage === 'settings' ? 'active' : '' %>">
<i class="nav-icon fas fa-cog"></i>
<p>사이트 설정</p>
</a>
</li>
<li class="nav-item">
<a href="/admin/telegram" class="nav-link <%= currentPage === 'telegram' ? 'active' : '' %>">
<i class="nav-icon fab fa-telegram"></i>
<p>텔레그램 봇</p>
</a>
</li>
<li class="nav-item">
<a href="/admin/banner-editor" class="nav-link <%= currentPage === 'banner-editor' ? 'active' : '' %>">
<i class="nav-icon fas fa-paint-brush"></i>
<p>배너 편집기</p>
</a>
</li>
</ul>
</nav>
</div>
</aside>
<!-- Content Wrapper -->
<div class="content-wrapper">
<!-- Content Header -->
<div class="content-header">
<div class="container-fluid">
<div class="row mb-2">
<div class="col-sm-6">
<h1 class="m-0"><%= title %></h1>
</div>
<div class="col-sm-6">
<ol class="breadcrumb float-sm-right">
<li class="breadcrumb-item"><a href="/admin/dashboard">홈</a></li>
<% if (currentPage !== 'dashboard') { %>
<li class="breadcrumb-item active"><%= title %></li>
<% } %>
</ol>
</div>
</div>
</div>
</div>
<!-- Main content -->
<section class="content">
<div class="container-fluid">
<%- body %>
</div>
</section>
</div>
<!-- Footer -->
<footer class="main-footer">
<strong>&copy; 2024 <a href="/">SmartSolTech</a></strong>
모든 권리 보유.
<div class="float-right d-none d-sm-inline-block">
<b>Version</b> 2.0.0
</div>
</footer>
</div>
<!-- AdminLTE JavaScript -->
<script src="/node_modules/admin-lte/plugins/jquery/jquery.min.js"></script>
<script src="/node_modules/admin-lte/plugins/bootstrap/js/bootstrap.bundle.min.js"></script>
<script src="/node_modules/admin-lte/dist/js/adminlte.min.js"></script>
<!-- Custom JavaScript -->
<script src="/js/main.js"></script>
<script>
// Korean localization for AdminLTE
$(document).ready(function() {
// Update any English text to Korean
$('.brand-link .brand-text').text('스마트솔테크');
// Add smooth transitions
$('.nav-link').on('click', function() {
if (!$(this).hasClass('active')) {
$('.nav-link.active').removeClass('active');
$(this).addClass('active');
}
});
});
</script>
</body>
</html>

View File

@@ -0,0 +1,268 @@
<!DOCTYPE html>
<html lang="<%= currentLanguage %>">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><%= title %> - SmartSolTech Admin</title>
<!-- AdminLTE CSS -->
<link rel="stylesheet" href="/node_modules/admin-lte/dist/css/adminlte.min.css">
<!-- Font Awesome -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<!-- Google Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@300;400;500;700&display=swap" rel="stylesheet">
<!-- Custom Korean Admin Styles -->
<style>
body, .content-wrapper, .main-sidebar {
font-family: 'Noto Sans KR', 'Malgun Gothic', 'Apple SD Gothic Neo', sans-serif;
}
.brand-text {
font-weight: 700;
color: #007bff !important;
}
.nav-sidebar .nav-link {
border-radius: 8px;
margin: 2px 0;
}
.nav-sidebar .nav-link.active {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.content-header h1 {
font-weight: 600;
color: #2c3e50;
}
.card {
border-radius: 12px;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
}
.btn {
border-radius: 8px;
font-weight: 500;
}
.info-box {
border-radius: 12px;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
}
.info-box-icon {
border-radius: 12px 0 0 12px;
}
.navbar-light {
background: white !important;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.main-sidebar {
background: linear-gradient(180deg, #f8f9fa 0%, #e9ecef 100%);
}
.sidebar-dark-primary .nav-sidebar > .nav-item > .nav-link:hover {
background-color: rgba(255,255,255,0.1);
}
</style>
</head>
<body class="hold-transition sidebar-mini layout-fixed">
<div class="wrapper">
<!-- Navbar -->
<nav class="main-header navbar navbar-expand navbar-light">
<!-- Left navbar links -->
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link" data-widget="pushmenu" href="#" role="button">
<i class="fas fa-bars"></i>
</a>
</li>
<li class="nav-item d-none d-sm-inline-block">
<a href="/" class="nav-link" target="_blank">
<i class="fas fa-external-link-alt mr-1"></i>
사이트 보기
</a>
</li>
</ul>
<!-- Right navbar links -->
<ul class="navbar-nav ml-auto">
<!-- User Menu -->
<li class="nav-item dropdown">
<a class="nav-link" data-toggle="dropdown" href="#">
<i class="far fa-user mr-1"></i>
<%= user ? user.name : '관리자' %>
</a>
<div class="dropdown-menu dropdown-menu-lg dropdown-menu-right">
<div class="dropdown-divider"></div>
<a href="/admin/settings" class="dropdown-item">
<i class="fas fa-cog mr-2"></i> 설정
</a>
<div class="dropdown-divider"></div>
<form action="/admin/logout" method="post" class="dropdown-item p-0">
<button type="submit" class="btn btn-link text-left w-100 text-danger">
<i class="fas fa-sign-out-alt mr-2"></i> 로그아웃
</button>
</form>
</div>
</li>
</ul>
</nav>
<!-- Main Sidebar Container -->
<aside class="main-sidebar sidebar-light-primary elevation-4">
<!-- Brand Logo -->
<a href="/admin/dashboard" class="brand-link">
<img src="/images/icons/icon-192x192.png" alt="SmartSolTech" class="brand-image img-circle elevation-3" style="opacity: .8">
<span class="brand-text font-weight-bold">SmartSolTech</span>
</a>
<!-- Sidebar -->
<div class="sidebar">
<!-- Sidebar Menu -->
<nav class="mt-3">
<ul class="nav nav-pills nav-sidebar flex-column" data-widget="treeview" role="menu" data-accordion="false">
<li class="nav-item">
<a href="/admin/dashboard" class="nav-link <%= currentPage === 'dashboard' ? 'active' : '' %>">
<i class="nav-icon fas fa-tachometer-alt"></i>
<p>대시보드</p>
</a>
</li>
<li class="nav-item">
<a href="/admin/portfolio" class="nav-link <%= currentPage === 'portfolio' ? 'active' : '' %>">
<i class="nav-icon fas fa-briefcase"></i>
<p>
포트폴리오
<span class="badge badge-info right"><%= stats?.portfolioCount || 0 %></span>
</p>
</a>
</li>
<li class="nav-item">
<a href="/admin/services" class="nav-link <%= currentPage === 'services' ? 'active' : '' %>">
<i class="nav-icon fas fa-cogs"></i>
<p>
서비스 관리
<span class="badge badge-success right"><%= stats?.servicesCount || 0 %></span>
</p>
</a>
</li>
<li class="nav-item">
<a href="/admin/contacts" class="nav-link <%= currentPage === 'contacts' ? 'active' : '' %>">
<i class="nav-icon fas fa-envelope"></i>
<p>
문의 관리
<span class="badge badge-warning right"><%= stats?.contactsCount || 0 %></span>
</p>
</a>
</li>
<li class="nav-item">
<a href="/admin/media" class="nav-link <%= currentPage === 'media' ? 'active' : '' %>">
<i class="nav-icon fas fa-images"></i>
<p>미디어 관리</p>
</a>
</li>
<li class="nav-header">시스템 설정</li>
<li class="nav-item">
<a href="/admin/settings" class="nav-link <%= currentPage === 'settings' ? 'active' : '' %>">
<i class="nav-icon fas fa-cog"></i>
<p>사이트 설정</p>
</a>
</li>
<li class="nav-item">
<a href="/admin/telegram" class="nav-link <%= currentPage === 'telegram' ? 'active' : '' %>">
<i class="nav-icon fab fa-telegram"></i>
<p>텔레그램 봇</p>
</a>
</li>
<li class="nav-item">
<a href="/admin/banner-editor" class="nav-link <%= currentPage === 'banner-editor' ? 'active' : '' %>">
<i class="nav-icon fas fa-paint-brush"></i>
<p>배너 편집기</p>
</a>
</li>
</ul>
</nav>
</div>
</aside>
<!-- Content Wrapper -->
<div class="content-wrapper">
<!-- Content Header -->
<div class="content-header">
<div class="container-fluid">
<div class="row mb-2">
<div class="col-sm-6">
<h1 class="m-0"><%= title %></h1>
</div>
<div class="col-sm-6">
<ol class="breadcrumb float-sm-right">
<li class="breadcrumb-item"><a href="/admin/dashboard">홈</a></li>
<% if (currentPage !== 'dashboard') { %>
<li class="breadcrumb-item active"><%= title %></li>
<% } %>
</ol>
</div>
</div>
</div>
</div>
<!-- Main content -->
<section class="content">
<div class="container-fluid">
<%- body %>
</div>
</section>
</div>
<!-- Footer -->
<footer class="main-footer">
<strong>&copy; 2024 <a href="/">SmartSolTech</a></strong>
모든 권리 보유.
<div class="float-right d-none d-sm-inline-block">
<b>Version</b> 2.0.0
</div>
</footer>
</div>
<!-- AdminLTE JavaScript -->
<script src="/node_modules/admin-lte/plugins/jquery/jquery.min.js"></script>
<script src="/node_modules/admin-lte/plugins/bootstrap/js/bootstrap.bundle.min.js"></script>
<script src="/node_modules/admin-lte/dist/js/adminlte.min.js"></script>
<!-- Custom JavaScript -->
<script src="/js/main.js"></script>
<script>
// Korean localization for AdminLTE
$(document).ready(function() {
// Update any English text to Korean
$('.brand-link .brand-text').text('스마트솔테크');
// Add smooth transitions
$('.nav-link').on('click', function() {
if (!$(this).hasClass('active')) {
$('.nav-link.active').removeClass('active');
$(this).addClass('active');
}
});
});
</script>
</body>
</html>

View File

@@ -0,0 +1,251 @@
<!DOCTYPE html>
<html lang="<%= currentLanguage %>">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><%= title %> - SmartSolTech Admin</title>
<!-- Tabler CSS -->
<link href="/node_modules/@tabler/core/dist/css/tabler.min.css" rel="stylesheet"/>
<link href="/node_modules/@tabler/icons/icons-sprite.svg" rel="stylesheet"/>
<!-- Font Awesome -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<!-- Google Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@300;400;500;700&display=swap" rel="stylesheet">
<!-- Custom Korean Admin Styles -->
<style>
body {
font-family: 'Noto Sans KR', 'Malgun Gothic', 'Apple SD Gothic Neo', sans-serif;
}
.navbar-brand {
font-weight: 700;
color: #206bc4 !important;
}
.nav-link {
border-radius: 8px;
margin: 2px 0;
font-weight: 500;
}
.nav-link.active {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white !important;
}
.page-header h2 {
font-weight: 600;
color: #2c3e50;
}
.card {
border-radius: 12px;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
}
.btn {
border-radius: 8px;
font-weight: 500;
}
.navbar {
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
background: white !important;
}
.navbar-nav .nav-link:hover {
background-color: rgba(32, 107, 196, 0.1);
}
.navbar-vertical .navbar-nav .nav-link {
padding: 0.5rem 1rem;
}
.page-wrapper {
background: #f8f9fa;
}
</style>
</head>
<body>
<div class="page">
<!-- Sidebar -->
<aside class="navbar navbar-vertical navbar-expand-lg" data-bs-theme="light">
<div class="container-fluid">
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#sidebar-menu" aria-controls="sidebar-menu" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<h1 class="navbar-brand navbar-brand-autodark">
<a href="/admin/dashboard">
<img src="/images/icons/icon-192x192.png" width="110" height="32" alt="SmartSolTech" class="navbar-brand-image">
SmartSolTech
</a>
</h1>
<div class="collapse navbar-collapse" id="sidebar-menu">
<ul class="navbar-nav pt-lg-3">
<li class="nav-item">
<a class="nav-link <%= currentPage === 'dashboard' ? 'active' : '' %>" href="/admin/dashboard">
<span class="nav-link-icon d-md-none d-lg-inline-block">
<i class="fas fa-tachometer-alt"></i>
</span>
<span class="nav-link-title">대시보드</span>
</a>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#navbar-help" data-bs-toggle="dropdown" data-bs-auto-close="false" role="button" aria-expanded="false">
<span class="nav-link-icon d-md-none d-lg-inline-block">
<i class="fas fa-briefcase"></i>
</span>
<span class="nav-link-title">포트폴리오</span>
</a>
<div class="dropdown-menu">
<a class="dropdown-item" href="/admin/portfolio">
모든 프로젝트
</a>
<a class="dropdown-item" href="/admin/portfolio/add">
새 프로젝트 추가
</a>
</div>
</li>
<li class="nav-item">
<a class="nav-link <%= currentPage === 'services' ? 'active' : '' %>" href="/admin/services">
<span class="nav-link-icon d-md-none d-lg-inline-block">
<i class="fas fa-cogs"></i>
</span>
<span class="nav-link-title">서비스 관리</span>
<span class="badge badge-sm bg-green text-white ms-2"><%= stats?.servicesCount || 0 %></span>
</a>
</li>
<li class="nav-item">
<a class="nav-link <%= currentPage === 'contacts' ? 'active' : '' %>" href="/admin/contacts">
<span class="nav-link-icon d-md-none d-lg-inline-block">
<i class="fas fa-envelope"></i>
</span>
<span class="nav-link-title">문의 관리</span>
<span class="badge badge-sm bg-yellow text-white ms-2"><%= stats?.contactsCount || 0 %></span>
</a>
</li>
<li class="nav-item">
<a class="nav-link <%= currentPage === 'media' ? 'active' : '' %>" href="/admin/media">
<span class="nav-link-icon d-md-none d-lg-inline-block">
<i class="fas fa-images"></i>
</span>
<span class="nav-link-title">미디어 관리</span>
</a>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#navbar-settings" data-bs-toggle="dropdown" data-bs-auto-close="false" role="button" aria-expanded="false">
<span class="nav-link-icon d-md-none d-lg-inline-block">
<i class="fas fa-cog"></i>
</span>
<span class="nav-link-title">시스템</span>
</a>
<div class="dropdown-menu">
<a class="dropdown-item" href="/admin/settings">
사이트 설정
</a>
<a class="dropdown-item" href="/admin/telegram">
텔레그램 봇
</a>
<a class="dropdown-item" href="/admin/banner-editor">
배너 편집기
</a>
</div>
</li>
</ul>
</div>
</div>
</aside>
<!-- Header -->
<header class="navbar navbar-expand-md d-print-none">
<div class="container-xl">
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbar-menu" aria-controls="navbar-menu" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="navbar-nav flex-row order-md-last">
<div class="nav-item dropdown">
<a href="#" class="nav-link d-flex lh-1 text-reset p-0" data-bs-toggle="dropdown" aria-label="Open user menu">
<span class="avatar avatar-sm" style="background-image: url('/images/icons/icon-192x192.png')"></span>
<div class="d-none d-xl-block ps-2">
<div><%= user ? user.name : '관리자' %></div>
<div class="mt-1 small text-muted">관리자</div>
</div>
</a>
<div class="dropdown-menu dropdown-menu-end dropdown-menu-arrow">
<a href="/admin/settings" class="dropdown-item">설정</a>
<a href="/" target="_blank" class="dropdown-item">사이트 보기</a>
<div class="dropdown-divider"></div>
<form action="/admin/logout" method="post" class="dropdown-item p-0">
<button type="submit" class="btn btn-link text-left w-100 text-danger">
로그아웃
</button>
</form>
</div>
</div>
</div>
</div>
</header>
<div class="page-wrapper">
<!-- Page header -->
<div class="page-header d-print-none">
<div class="container-xl">
<div class="row g-2 align-items-center">
<div class="col">
<h2 class="page-title">
<%= title %>
</h2>
</div>
</div>
</div>
</div>
<!-- Page body -->
<div class="page-body">
<div class="container-xl">
<%- body %>
</div>
</div>
</div>
</div>
<!-- Tabler JavaScript -->
<script src="/node_modules/@tabler/core/dist/js/tabler.min.js"></script>
<!-- Custom JavaScript -->
<script src="/js/main.js"></script>
<script>
// Korean localization and enhancements
document.addEventListener('DOMContentLoaded', function() {
// Add smooth transitions for navigation
const navLinks = document.querySelectorAll('.nav-link');
navLinks.forEach(link => {
link.addEventListener('click', function() {
if (!this.classList.contains('active') && !this.classList.contains('dropdown-toggle')) {
const currentActive = document.querySelector('.nav-link.active');
if (currentActive) {
currentActive.classList.remove('active');
}
this.classList.add('active');
}
});
});
});
</script>
</body>
</html>

View File

@@ -0,0 +1,251 @@
<!DOCTYPE html>
<html lang="<%= currentLanguage %>">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><%= title %> - SmartSolTech Admin</title>
<!-- Tabler CSS -->
<link href="/node_modules/@tabler/core/dist/css/tabler.min.css" rel="stylesheet"/>
<link href="/node_modules/@tabler/icons/icons-sprite.svg" rel="stylesheet"/>
<!-- Font Awesome -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<!-- Google Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@300;400;500;700&display=swap" rel="stylesheet">
<!-- Custom Korean Admin Styles -->
<style>
body {
font-family: 'Noto Sans KR', 'Malgun Gothic', 'Apple SD Gothic Neo', sans-serif;
}
.navbar-brand {
font-weight: 700;
color: #206bc4 !important;
}
.nav-link {
border-radius: 8px;
margin: 2px 0;
font-weight: 500;
}
.nav-link.active {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white !important;
}
.page-header h2 {
font-weight: 600;
color: #2c3e50;
}
.card {
border-radius: 12px;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
}
.btn {
border-radius: 8px;
font-weight: 500;
}
.navbar {
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
background: white !important;
}
.navbar-nav .nav-link:hover {
background-color: rgba(32, 107, 196, 0.1);
}
.navbar-vertical .navbar-nav .nav-link {
padding: 0.5rem 1rem;
}
.page-wrapper {
background: #f8f9fa;
}
</style>
</head>
<body>
<div class="page">
<!-- Sidebar -->
<aside class="navbar navbar-vertical navbar-expand-lg" data-bs-theme="light">
<div class="container-fluid">
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#sidebar-menu" aria-controls="sidebar-menu" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<h1 class="navbar-brand navbar-brand-autodark">
<a href="/admin/dashboard">
<img src="/images/icons/icon-192x192.png" width="110" height="32" alt="SmartSolTech" class="navbar-brand-image">
SmartSolTech
</a>
</h1>
<div class="collapse navbar-collapse" id="sidebar-menu">
<ul class="navbar-nav pt-lg-3">
<li class="nav-item">
<a class="nav-link <%= currentPage === 'dashboard' ? 'active' : '' %>" href="/admin/dashboard">
<span class="nav-link-icon d-md-none d-lg-inline-block">
<i class="fas fa-tachometer-alt"></i>
</span>
<span class="nav-link-title">대시보드</span>
</a>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#navbar-help" data-bs-toggle="dropdown" data-bs-auto-close="false" role="button" aria-expanded="false">
<span class="nav-link-icon d-md-none d-lg-inline-block">
<i class="fas fa-briefcase"></i>
</span>
<span class="nav-link-title">포트폴리오</span>
</a>
<div class="dropdown-menu">
<a class="dropdown-item" href="/admin/portfolio">
모든 프로젝트
</a>
<a class="dropdown-item" href="/admin/portfolio/add">
새 프로젝트 추가
</a>
</div>
</li>
<li class="nav-item">
<a class="nav-link <%= currentPage === 'services' ? 'active' : '' %>" href="/admin/services">
<span class="nav-link-icon d-md-none d-lg-inline-block">
<i class="fas fa-cogs"></i>
</span>
<span class="nav-link-title">서비스 관리</span>
<span class="badge badge-sm bg-green text-white ms-2"><%= stats?.servicesCount || 0 %></span>
</a>
</li>
<li class="nav-item">
<a class="nav-link <%= currentPage === 'contacts' ? 'active' : '' %>" href="/admin/contacts">
<span class="nav-link-icon d-md-none d-lg-inline-block">
<i class="fas fa-envelope"></i>
</span>
<span class="nav-link-title">문의 관리</span>
<span class="badge badge-sm bg-yellow text-white ms-2"><%= stats?.contactsCount || 0 %></span>
</a>
</li>
<li class="nav-item">
<a class="nav-link <%= currentPage === 'media' ? 'active' : '' %>" href="/admin/media">
<span class="nav-link-icon d-md-none d-lg-inline-block">
<i class="fas fa-images"></i>
</span>
<span class="nav-link-title">미디어 관리</span>
</a>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#navbar-settings" data-bs-toggle="dropdown" data-bs-auto-close="false" role="button" aria-expanded="false">
<span class="nav-link-icon d-md-none d-lg-inline-block">
<i class="fas fa-cog"></i>
</span>
<span class="nav-link-title">시스템</span>
</a>
<div class="dropdown-menu">
<a class="dropdown-item" href="/admin/settings">
사이트 설정
</a>
<a class="dropdown-item" href="/admin/telegram">
텔레그램 봇
</a>
<a class="dropdown-item" href="/admin/banner-editor">
배너 편집기
</a>
</div>
</li>
</ul>
</div>
</div>
</aside>
<!-- Header -->
<header class="navbar navbar-expand-md d-print-none">
<div class="container-xl">
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbar-menu" aria-controls="navbar-menu" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="navbar-nav flex-row order-md-last">
<div class="nav-item dropdown">
<a href="#" class="nav-link d-flex lh-1 text-reset p-0" data-bs-toggle="dropdown" aria-label="Open user menu">
<span class="avatar avatar-sm" style="background-image: url('/images/icons/icon-192x192.png')"></span>
<div class="d-none d-xl-block ps-2">
<div><%= user ? user.name : '관리자' %></div>
<div class="mt-1 small text-muted">관리자</div>
</div>
</a>
<div class="dropdown-menu dropdown-menu-end dropdown-menu-arrow">
<a href="/admin/settings" class="dropdown-item">설정</a>
<a href="/" target="_blank" class="dropdown-item">사이트 보기</a>
<div class="dropdown-divider"></div>
<form action="/admin/logout" method="post" class="dropdown-item p-0">
<button type="submit" class="btn btn-link text-left w-100 text-danger">
로그아웃
</button>
</form>
</div>
</div>
</div>
</div>
</header>
<div class="page-wrapper">
<!-- Page header -->
<div class="page-header d-print-none">
<div class="container-xl">
<div class="row g-2 align-items-center">
<div class="col">
<h2 class="page-title">
<%= title %>
</h2>
</div>
</div>
</div>
</div>
<!-- Page body -->
<div class="page-body">
<div class="container-xl">
<%- body %>
</div>
</div>
</div>
</div>
<!-- Tabler JavaScript -->
<script src="/node_modules/@tabler/core/dist/js/tabler.min.js"></script>
<!-- Custom JavaScript -->
<script src="/js/main.js"></script>
<script>
// Korean localization and enhancements
document.addEventListener('DOMContentLoaded', function() {
// Add smooth transitions for navigation
const navLinks = document.querySelectorAll('.nav-link');
navLinks.forEach(link => {
link.addEventListener('click', function() {
if (!this.classList.contains('active') && !this.classList.contains('dropdown-toggle')) {
const currentActive = document.querySelector('.nav-link.active');
if (currentActive) {
currentActive.classList.remove('active');
}
this.classList.add('active');
}
});
});
});
</script>
</body>
</html>

View File

@@ -0,0 +1,276 @@
<!DOCTYPE html>
<html lang="<%= currentLanguage %>">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><%= title %> - SmartSolTech Admin</title>
<!-- AdminLTE CSS -->
<link rel="stylesheet" href="/node_modules/admin-lte/dist/css/adminlte.min.css">
<!-- Font Awesome -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<!-- Google Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@300;400;500;700&display=swap" rel="stylesheet">
<!-- Custom Korean Admin Styles -->
<style>
body, .content-wrapper, .main-sidebar {
font-family: 'Noto Sans KR', 'Malgun Gothic', 'Apple SD Gothic Neo', sans-serif;
}
.brand-text {
font-weight: 700;
color: #007bff !important;
}
.nav-sidebar .nav-link {
border-radius: 8px;
margin: 2px 0;
}
.nav-sidebar .nav-link.active {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.content-header h1 {
font-weight: 600;
color: #2c3e50;
}
.card {
border-radius: 12px;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
}
.btn {
border-radius: 8px;
font-weight: 500;
}
.info-box {
border-radius: 12px;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
}
.info-box-icon {
border-radius: 12px 0 0 12px;
}
.navbar-light {
background: white !important;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.main-sidebar {
background: linear-gradient(180deg, #f8f9fa 0%, #e9ecef 100%);
}
.sidebar-dark-primary .nav-sidebar > .nav-item > .nav-link:hover {
background-color: rgba(255,255,255,0.1);
}
</style>
</head>
<body class="hold-transition sidebar-mini layout-fixed">
<div class="wrapper">
<!-- Navbar -->
<nav class="main-header navbar navbar-expand navbar-light">
<!-- Left navbar links -->
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link" data-widget="pushmenu" href="#" role="button">
<i class="fas fa-bars"></i>
</a>
</li>
<li class="nav-item d-none d-sm-inline-block">
<a href="/" class="nav-link" target="_blank">
<i class="fas fa-external-link-alt mr-1"></i>
사이트 보기
</a>
</li>
</ul>
<!-- Right navbar links -->
<ul class="navbar-nav ml-auto">
<!-- User Menu -->
<li class="nav-item dropdown">
<a class="nav-link" data-toggle="dropdown" href="#">
<i class="far fa-user mr-1"></i>
<%= user ? user.name : '관리자' %>
</a>
<div class="dropdown-menu dropdown-menu-lg dropdown-menu-right">
<div class="dropdown-divider"></div>
<a href="/admin/settings" class="dropdown-item">
<i class="fas fa-cog mr-2"></i> 설정
</a>
<div class="dropdown-divider"></div>
<form action="/admin/logout" method="post" class="dropdown-item p-0">
<button type="submit" class="btn btn-link text-left w-100 text-danger">
<i class="fas fa-sign-out-alt mr-2"></i> 로그아웃
</button>
</form>
</div>
</li>
</ul>
</nav>
<!-- Main Sidebar Container -->
<aside class="main-sidebar sidebar-light-primary elevation-4">
<!-- Brand Logo -->
<a href="/admin/dashboard" class="brand-link">
<img src="/images/icons/icon-192x192.png" alt="SmartSolTech" class="brand-image img-circle elevation-3" style="opacity: .8">
<span class="brand-text font-weight-bold">SmartSolTech</span>
</a>
<!-- Sidebar -->
<div class="sidebar">
<!-- Sidebar Menu -->
<nav class="mt-3">
<ul class="nav nav-pills nav-sidebar flex-column" data-widget="treeview" role="menu" data-accordion="false">
<li class="nav-item">
<a href="/admin/dashboard" class="nav-link <%= currentPage === 'dashboard' ? 'active' : '' %>">
<i class="nav-icon fas fa-tachometer-alt"></i>
<p>대시보드</p>
</a>
</li>
<li class="nav-item">
<a href="/admin/portfolio" class="nav-link <%= currentPage === 'portfolio' ? 'active' : '' %>">
<i class="nav-icon fas fa-briefcase"></i>
<p>
포트폴리오
<% if (typeof stats !== 'undefined' && stats.portfolioCount) { %>
<span class="badge badge-info right"><%= stats.portfolioCount %></span>
<% } %>
</p>
</a>
</li>
<li class="nav-item">
<a href="/admin/services" class="nav-link <%= currentPage === 'services' ? 'active' : '' %>">
<i class="nav-icon fas fa-cogs"></i>
<p>
서비스 관리
<% if (typeof stats !== 'undefined' && stats.servicesCount) { %>
<span class="badge badge-success right"><%= stats.servicesCount %></span>
<% } %>
</p>
</a>
</li>
<li class="nav-item">
<a href="/admin/contacts" class="nav-link <%= currentPage === 'contacts' ? 'active' : '' %>">
<i class="nav-icon fas fa-envelope"></i>
<p>
문의 관리
<% if (typeof stats !== 'undefined' && stats.contactsCount) { %>
<span class="badge badge-warning right"><%= stats.contactsCount %></span>
<% } %>
</p>
</a>
</li>
<li class="nav-item">
<a href="/admin/media" class="nav-link <%= currentPage === 'media' ? 'active' : '' %>">
<i class="nav-icon fas fa-images"></i>
<p>미디어 관리</p>
</a>
</li>
<li class="nav-header">시스템 설정</li>
<li class="nav-item">
<a href="/admin/settings" class="nav-link <%= currentPage === 'settings' ? 'active' : '' %>">
<i class="nav-icon fas fa-cog"></i>
<p>사이트 설정</p>
</a>
</li>
<li class="nav-item">
<a href="/admin/telegram" class="nav-link <%= currentPage === 'telegram' ? 'active' : '' %>">
<i class="nav-icon fab fa-telegram"></i>
<p>텔레그램 봇</p>
</a>
</li>
<li class="nav-item">
<a href="/admin/banner-editor" class="nav-link <%= currentPage === 'banner-editor' ? 'active' : '' %>">
<i class="nav-icon fas fa-paint-brush"></i>
<p>배너 편집기</p>
</a>
</li>
</ul>
</nav>
</div>
</aside>
<!-- Content Wrapper -->
<div class="content-wrapper">
<!-- Content Header -->
<div class="content-header">
<div class="container-fluid">
<div class="row mb-2">
<div class="col-sm-6">
<h1 class="m-0"><%= title %></h1>
</div>
<div class="col-sm-6">
<ol class="breadcrumb float-sm-right">
<li class="breadcrumb-item"><a href="/admin/dashboard">홈</a></li>
<% if (currentPage !== 'dashboard') { %>
<li class="breadcrumb-item active"><%= title %></li>
<% } %>
</ol>
</div>
</div>
</div>
</div>
<!-- Main content -->
<section class="content">
<div class="container-fluid">
<%- body %>
</div>
</section>
</div>
<!-- Footer -->
<footer class="main-footer">
<strong>&copy; 2024 <a href="/">SmartSolTech</a></strong>
모든 권리 보유.
<div class="float-right d-none d-sm-inline-block">
<b>Version</b> 2.0.0
</div>
</footer>
</div>
<!-- jQuery -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<!-- Bootstrap 4 -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/js/bootstrap.bundle.min.js"></script>
<!-- AdminLTE App -->
<script src="/node_modules/admin-lte/dist/js/adminlte.min.js"></script>
<!-- Custom JavaScript -->
<script src="/js/main.js"></script>
<script>
// Korean localization for AdminLTE
$(document).ready(function() {
// Update any English text to Korean
$('.brand-link .brand-text').text('스마트솔테크');
// Add smooth transitions
$('.nav-link').on('click', function() {
if (!$(this).hasClass('active')) {
$('.nav-link.active').removeClass('active');
$(this).addClass('active');
}
});
});
</script>
</body>
</html>

View File

@@ -0,0 +1,276 @@
<!DOCTYPE html>
<html lang="<%= currentLanguage %>">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><%= title %> - SmartSolTech Admin</title>
<!-- AdminLTE CSS -->
<link rel="stylesheet" href="/node_modules/admin-lte/dist/css/adminlte.min.css">
<!-- Font Awesome -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<!-- Google Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@300;400;500;700&display=swap" rel="stylesheet">
<!-- Custom Korean Admin Styles -->
<style>
body, .content-wrapper, .main-sidebar {
font-family: 'Noto Sans KR', 'Malgun Gothic', 'Apple SD Gothic Neo', sans-serif;
}
.brand-text {
font-weight: 700;
color: #007bff !important;
}
.nav-sidebar .nav-link {
border-radius: 8px;
margin: 2px 0;
}
.nav-sidebar .nav-link.active {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.content-header h1 {
font-weight: 600;
color: #2c3e50;
}
.card {
border-radius: 12px;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
}
.btn {
border-radius: 8px;
font-weight: 500;
}
.info-box {
border-radius: 12px;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
}
.info-box-icon {
border-radius: 12px 0 0 12px;
}
.navbar-light {
background: white !important;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.main-sidebar {
background: linear-gradient(180deg, #f8f9fa 0%, #e9ecef 100%);
}
.sidebar-dark-primary .nav-sidebar > .nav-item > .nav-link:hover {
background-color: rgba(255,255,255,0.1);
}
</style>
</head>
<body class="hold-transition sidebar-mini layout-fixed">
<div class="wrapper">
<!-- Navbar -->
<nav class="main-header navbar navbar-expand navbar-light">
<!-- Left navbar links -->
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link" data-widget="pushmenu" href="#" role="button">
<i class="fas fa-bars"></i>
</a>
</li>
<li class="nav-item d-none d-sm-inline-block">
<a href="/" class="nav-link" target="_blank">
<i class="fas fa-external-link-alt mr-1"></i>
사이트 보기
</a>
</li>
</ul>
<!-- Right navbar links -->
<ul class="navbar-nav ml-auto">
<!-- User Menu -->
<li class="nav-item dropdown">
<a class="nav-link" data-toggle="dropdown" href="#">
<i class="far fa-user mr-1"></i>
<%= user ? user.name : '관리자' %>
</a>
<div class="dropdown-menu dropdown-menu-lg dropdown-menu-right">
<div class="dropdown-divider"></div>
<a href="/admin/settings" class="dropdown-item">
<i class="fas fa-cog mr-2"></i> 설정
</a>
<div class="dropdown-divider"></div>
<form action="/admin/logout" method="post" class="dropdown-item p-0">
<button type="submit" class="btn btn-link text-left w-100 text-danger">
<i class="fas fa-sign-out-alt mr-2"></i> 로그아웃
</button>
</form>
</div>
</li>
</ul>
</nav>
<!-- Main Sidebar Container -->
<aside class="main-sidebar sidebar-light-primary elevation-4">
<!-- Brand Logo -->
<a href="/admin/dashboard" class="brand-link">
<img src="/images/icons/icon-192x192.png" alt="SmartSolTech" class="brand-image img-circle elevation-3" style="opacity: .8">
<span class="brand-text font-weight-bold">SmartSolTech</span>
</a>
<!-- Sidebar -->
<div class="sidebar">
<!-- Sidebar Menu -->
<nav class="mt-3">
<ul class="nav nav-pills nav-sidebar flex-column" data-widget="treeview" role="menu" data-accordion="false">
<li class="nav-item">
<a href="/admin/dashboard" class="nav-link <%= currentPage === 'dashboard' ? 'active' : '' %>">
<i class="nav-icon fas fa-tachometer-alt"></i>
<p>대시보드</p>
</a>
</li>
<li class="nav-item">
<a href="/admin/portfolio" class="nav-link <%= currentPage === 'portfolio' ? 'active' : '' %>">
<i class="nav-icon fas fa-briefcase"></i>
<p>
포트폴리오
<% if (typeof stats !== 'undefined' && stats.portfolioCount) { %>
<span class="badge badge-info right"><%= stats.portfolioCount %></span>
<% } %>
</p>
</a>
</li>
<li class="nav-item">
<a href="/admin/services" class="nav-link <%= currentPage === 'services' ? 'active' : '' %>">
<i class="nav-icon fas fa-cogs"></i>
<p>
서비스 관리
<% if (typeof stats !== 'undefined' && stats.servicesCount) { %>
<span class="badge badge-success right"><%= stats.servicesCount %></span>
<% } %>
</p>
</a>
</li>
<li class="nav-item">
<a href="/admin/contacts" class="nav-link <%= currentPage === 'contacts' ? 'active' : '' %>">
<i class="nav-icon fas fa-envelope"></i>
<p>
문의 관리
<% if (typeof stats !== 'undefined' && stats.contactsCount) { %>
<span class="badge badge-warning right"><%= stats.contactsCount %></span>
<% } %>
</p>
</a>
</li>
<li class="nav-item">
<a href="/admin/media" class="nav-link <%= currentPage === 'media' ? 'active' : '' %>">
<i class="nav-icon fas fa-images"></i>
<p>미디어 관리</p>
</a>
</li>
<li class="nav-header">시스템 설정</li>
<li class="nav-item">
<a href="/admin/settings" class="nav-link <%= currentPage === 'settings' ? 'active' : '' %>">
<i class="nav-icon fas fa-cog"></i>
<p>사이트 설정</p>
</a>
</li>
<li class="nav-item">
<a href="/admin/telegram" class="nav-link <%= currentPage === 'telegram' ? 'active' : '' %>">
<i class="nav-icon fab fa-telegram"></i>
<p>텔레그램 봇</p>
</a>
</li>
<li class="nav-item">
<a href="/admin/banner-editor" class="nav-link <%= currentPage === 'banner-editor' ? 'active' : '' %>">
<i class="nav-icon fas fa-paint-brush"></i>
<p>배너 편집기</p>
</a>
</li>
</ul>
</nav>
</div>
</aside>
<!-- Content Wrapper -->
<div class="content-wrapper">
<!-- Content Header -->
<div class="content-header">
<div class="container-fluid">
<div class="row mb-2">
<div class="col-sm-6">
<h1 class="m-0"><%= title %></h1>
</div>
<div class="col-sm-6">
<ol class="breadcrumb float-sm-right">
<li class="breadcrumb-item"><a href="/admin/dashboard">홈</a></li>
<% if (currentPage !== 'dashboard') { %>
<li class="breadcrumb-item active"><%= title %></li>
<% } %>
</ol>
</div>
</div>
</div>
</div>
<!-- Main content -->
<section class="content">
<div class="container-fluid">
<%- body %>
</div>
</section>
</div>
<!-- Footer -->
<footer class="main-footer">
<strong>&copy; 2024 <a href="/">SmartSolTech</a></strong>
모든 권리 보유.
<div class="float-right d-none d-sm-inline-block">
<b>Version</b> 2.0.0
</div>
</footer>
</div>
<!-- jQuery -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<!-- Bootstrap 4 -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/js/bootstrap.bundle.min.js"></script>
<!-- AdminLTE App -->
<script src="/node_modules/admin-lte/dist/js/adminlte.min.js"></script>
<!-- Custom JavaScript -->
<script src="/js/main.js"></script>
<script>
// Korean localization for AdminLTE
$(document).ready(function() {
// Update any English text to Korean
$('.brand-link .brand-text').text('스마트솔테크');
// Add smooth transitions
$('.nav-link').on('click', function() {
if (!$(this).hasClass('active')) {
$('.nav-link.active').removeClass('active');
$(this).addClass('active');
}
});
});
</script>
</body>
</html>

View File

@@ -0,0 +1,279 @@
<!DOCTYPE html>
<html lang="<%= currentLanguage %>">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><%= title %> - SmartSolTech Admin</title>
<!-- Bootstrap CSS (Local) -->
<link rel="stylesheet" href="/vendor/bootstrap/bootstrap.min.css">
<!-- AdminLTE CSS (Local) -->
<link rel="stylesheet" href="/vendor/adminlte/adminlte.min.css">
<!-- Font Awesome (Local) -->
<link rel="stylesheet" href="/vendor/adminlte/fontawesome.min.css">
<!-- Google Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@300;400;500;700&display=swap" rel="stylesheet">
<!-- Custom Korean Admin Styles -->
<style>
body, .content-wrapper, .main-sidebar {
font-family: 'Noto Sans KR', 'Malgun Gothic', 'Apple SD Gothic Neo', sans-serif;
}
.brand-text {
font-weight: 700;
color: #007bff !important;
}
.nav-sidebar .nav-link {
border-radius: 8px;
margin: 2px 0;
}
.nav-sidebar .nav-link.active {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.content-header h1 {
font-weight: 600;
color: #2c3e50;
}
.card {
border-radius: 12px;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
}
.btn {
border-radius: 8px;
font-weight: 500;
}
.info-box {
border-radius: 12px;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
}
.info-box-icon {
border-radius: 12px 0 0 12px;
}
.navbar-light {
background: white !important;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.main-sidebar {
background: linear-gradient(180deg, #f8f9fa 0%, #e9ecef 100%);
}
.sidebar-dark-primary .nav-sidebar > .nav-item > .nav-link:hover {
background-color: rgba(255,255,255,0.1);
}
</style>
</head>
<body class="hold-transition sidebar-mini layout-fixed">
<div class="wrapper">
<!-- Navbar -->
<nav class="main-header navbar navbar-expand navbar-light">
<!-- Left navbar links -->
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link" data-widget="pushmenu" href="#" role="button">
<i class="fas fa-bars"></i>
</a>
</li>
<li class="nav-item d-none d-sm-inline-block">
<a href="/" class="nav-link" target="_blank">
<i class="fas fa-external-link-alt mr-1"></i>
사이트 보기
</a>
</li>
</ul>
<!-- Right navbar links -->
<ul class="navbar-nav ml-auto">
<!-- User Menu -->
<li class="nav-item dropdown">
<a class="nav-link" data-toggle="dropdown" href="#">
<i class="far fa-user mr-1"></i>
<%= user ? user.name : '관리자' %>
</a>
<div class="dropdown-menu dropdown-menu-lg dropdown-menu-right">
<div class="dropdown-divider"></div>
<a href="/admin/settings" class="dropdown-item">
<i class="fas fa-cog mr-2"></i> 설정
</a>
<div class="dropdown-divider"></div>
<form action="/admin/logout" method="post" class="dropdown-item p-0">
<button type="submit" class="btn btn-link text-left w-100 text-danger">
<i class="fas fa-sign-out-alt mr-2"></i> 로그아웃
</button>
</form>
</div>
</li>
</ul>
</nav>
<!-- Main Sidebar Container -->
<aside class="main-sidebar sidebar-light-primary elevation-4">
<!-- Brand Logo -->
<a href="/admin/dashboard" class="brand-link">
<img src="/images/icons/icon-192x192.png" alt="SmartSolTech" class="brand-image img-circle elevation-3" style="opacity: .8">
<span class="brand-text font-weight-bold">SmartSolTech</span>
</a>
<!-- Sidebar -->
<div class="sidebar">
<!-- Sidebar Menu -->
<nav class="mt-3">
<ul class="nav nav-pills nav-sidebar flex-column" data-widget="treeview" role="menu" data-accordion="false">
<li class="nav-item">
<a href="/admin/dashboard" class="nav-link <%= currentPage === 'dashboard' ? 'active' : '' %>">
<i class="nav-icon fas fa-tachometer-alt"></i>
<p>대시보드</p>
</a>
</li>
<li class="nav-item">
<a href="/admin/portfolio" class="nav-link <%= currentPage === 'portfolio' ? 'active' : '' %>">
<i class="nav-icon fas fa-briefcase"></i>
<p>
포트폴리오
<% if (typeof stats !== 'undefined' && stats.portfolioCount) { %>
<span class="badge badge-info right"><%= stats.portfolioCount %></span>
<% } %>
</p>
</a>
</li>
<li class="nav-item">
<a href="/admin/services" class="nav-link <%= currentPage === 'services' ? 'active' : '' %>">
<i class="nav-icon fas fa-cogs"></i>
<p>
서비스 관리
<% if (typeof stats !== 'undefined' && stats.servicesCount) { %>
<span class="badge badge-success right"><%= stats.servicesCount %></span>
<% } %>
</p>
</a>
</li>
<li class="nav-item">
<a href="/admin/contacts" class="nav-link <%= currentPage === 'contacts' ? 'active' : '' %>">
<i class="nav-icon fas fa-envelope"></i>
<p>
문의 관리
<% if (typeof stats !== 'undefined' && stats.contactsCount) { %>
<span class="badge badge-warning right"><%= stats.contactsCount %></span>
<% } %>
</p>
</a>
</li>
<li class="nav-item">
<a href="/admin/media" class="nav-link <%= currentPage === 'media' ? 'active' : '' %>">
<i class="nav-icon fas fa-images"></i>
<p>미디어 관리</p>
</a>
</li>
<li class="nav-header">시스템 설정</li>
<li class="nav-item">
<a href="/admin/settings" class="nav-link <%= currentPage === 'settings' ? 'active' : '' %>">
<i class="nav-icon fas fa-cog"></i>
<p>사이트 설정</p>
</a>
</li>
<li class="nav-item">
<a href="/admin/telegram" class="nav-link <%= currentPage === 'telegram' ? 'active' : '' %>">
<i class="nav-icon fab fa-telegram"></i>
<p>텔레그램 봇</p>
</a>
</li>
<li class="nav-item">
<a href="/admin/banner-editor" class="nav-link <%= currentPage === 'banner-editor' ? 'active' : '' %>">
<i class="nav-icon fas fa-paint-brush"></i>
<p>배너 편집기</p>
</a>
</li>
</ul>
</nav>
</div>
</aside>
<!-- Content Wrapper -->
<div class="content-wrapper">
<!-- Content Header -->
<div class="content-header">
<div class="container-fluid">
<div class="row mb-2">
<div class="col-sm-6">
<h1 class="m-0"><%= title %></h1>
</div>
<div class="col-sm-6">
<ol class="breadcrumb float-sm-right">
<li class="breadcrumb-item"><a href="/admin/dashboard">홈</a></li>
<% if (currentPage !== 'dashboard') { %>
<li class="breadcrumb-item active"><%= title %></li>
<% } %>
</ol>
</div>
</div>
</div>
</div>
<!-- Main content -->
<section class="content">
<div class="container-fluid">
<%- body %>
</div>
</section>
</div>
<!-- Footer -->
<footer class="main-footer">
<strong>&copy; 2024 <a href="/">SmartSolTech</a></strong>
모든 권리 보유.
<div class="float-right d-none d-sm-inline-block">
<b>Version</b> 2.0.0
</div>
</footer>
</div>
<!-- jQuery -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<!-- Bootstrap 4 -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/js/bootstrap.bundle.min.js"></script>
<!-- AdminLTE App -->
<script src="/node_modules/admin-lte/dist/js/adminlte.min.js"></script>
<!-- Custom JavaScript -->
<script src="/js/main.js"></script>
<script>
// Korean localization for AdminLTE
$(document).ready(function() {
// Update any English text to Korean
$('.brand-link .brand-text').text('스마트솔테크');
// Add smooth transitions
$('.nav-link').on('click', function() {
if (!$(this).hasClass('active')) {
$('.nav-link.active').removeClass('active');
$(this).addClass('active');
}
});
});
</script>
</body>
</html>

View File

@@ -0,0 +1,279 @@
<!DOCTYPE html>
<html lang="<%= currentLanguage %>">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><%= title %> - SmartSolTech Admin</title>
<!-- Bootstrap CSS (Local) -->
<link rel="stylesheet" href="/vendor/bootstrap/bootstrap.min.css">
<!-- AdminLTE CSS (Local) -->
<link rel="stylesheet" href="/vendor/adminlte/adminlte.min.css">
<!-- Font Awesome (Local) -->
<link rel="stylesheet" href="/vendor/adminlte/fontawesome.min.css">
<!-- Google Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@300;400;500;700&display=swap" rel="stylesheet">
<!-- Custom Korean Admin Styles -->
<style>
body, .content-wrapper, .main-sidebar {
font-family: 'Noto Sans KR', 'Malgun Gothic', 'Apple SD Gothic Neo', sans-serif;
}
.brand-text {
font-weight: 700;
color: #007bff !important;
}
.nav-sidebar .nav-link {
border-radius: 8px;
margin: 2px 0;
}
.nav-sidebar .nav-link.active {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.content-header h1 {
font-weight: 600;
color: #2c3e50;
}
.card {
border-radius: 12px;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
}
.btn {
border-radius: 8px;
font-weight: 500;
}
.info-box {
border-radius: 12px;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
}
.info-box-icon {
border-radius: 12px 0 0 12px;
}
.navbar-light {
background: white !important;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.main-sidebar {
background: linear-gradient(180deg, #f8f9fa 0%, #e9ecef 100%);
}
.sidebar-dark-primary .nav-sidebar > .nav-item > .nav-link:hover {
background-color: rgba(255,255,255,0.1);
}
</style>
</head>
<body class="hold-transition sidebar-mini layout-fixed">
<div class="wrapper">
<!-- Navbar -->
<nav class="main-header navbar navbar-expand navbar-light">
<!-- Left navbar links -->
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link" data-widget="pushmenu" href="#" role="button">
<i class="fas fa-bars"></i>
</a>
</li>
<li class="nav-item d-none d-sm-inline-block">
<a href="/" class="nav-link" target="_blank">
<i class="fas fa-external-link-alt mr-1"></i>
사이트 보기
</a>
</li>
</ul>
<!-- Right navbar links -->
<ul class="navbar-nav ml-auto">
<!-- User Menu -->
<li class="nav-item dropdown">
<a class="nav-link" data-toggle="dropdown" href="#">
<i class="far fa-user mr-1"></i>
<%= user ? user.name : '관리자' %>
</a>
<div class="dropdown-menu dropdown-menu-lg dropdown-menu-right">
<div class="dropdown-divider"></div>
<a href="/admin/settings" class="dropdown-item">
<i class="fas fa-cog mr-2"></i> 설정
</a>
<div class="dropdown-divider"></div>
<form action="/admin/logout" method="post" class="dropdown-item p-0">
<button type="submit" class="btn btn-link text-left w-100 text-danger">
<i class="fas fa-sign-out-alt mr-2"></i> 로그아웃
</button>
</form>
</div>
</li>
</ul>
</nav>
<!-- Main Sidebar Container -->
<aside class="main-sidebar sidebar-light-primary elevation-4">
<!-- Brand Logo -->
<a href="/admin/dashboard" class="brand-link">
<img src="/images/icons/icon-192x192.png" alt="SmartSolTech" class="brand-image img-circle elevation-3" style="opacity: .8">
<span class="brand-text font-weight-bold">SmartSolTech</span>
</a>
<!-- Sidebar -->
<div class="sidebar">
<!-- Sidebar Menu -->
<nav class="mt-3">
<ul class="nav nav-pills nav-sidebar flex-column" data-widget="treeview" role="menu" data-accordion="false">
<li class="nav-item">
<a href="/admin/dashboard" class="nav-link <%= currentPage === 'dashboard' ? 'active' : '' %>">
<i class="nav-icon fas fa-tachometer-alt"></i>
<p>대시보드</p>
</a>
</li>
<li class="nav-item">
<a href="/admin/portfolio" class="nav-link <%= currentPage === 'portfolio' ? 'active' : '' %>">
<i class="nav-icon fas fa-briefcase"></i>
<p>
포트폴리오
<% if (typeof stats !== 'undefined' && stats.portfolioCount) { %>
<span class="badge badge-info right"><%= stats.portfolioCount %></span>
<% } %>
</p>
</a>
</li>
<li class="nav-item">
<a href="/admin/services" class="nav-link <%= currentPage === 'services' ? 'active' : '' %>">
<i class="nav-icon fas fa-cogs"></i>
<p>
서비스 관리
<% if (typeof stats !== 'undefined' && stats.servicesCount) { %>
<span class="badge badge-success right"><%= stats.servicesCount %></span>
<% } %>
</p>
</a>
</li>
<li class="nav-item">
<a href="/admin/contacts" class="nav-link <%= currentPage === 'contacts' ? 'active' : '' %>">
<i class="nav-icon fas fa-envelope"></i>
<p>
문의 관리
<% if (typeof stats !== 'undefined' && stats.contactsCount) { %>
<span class="badge badge-warning right"><%= stats.contactsCount %></span>
<% } %>
</p>
</a>
</li>
<li class="nav-item">
<a href="/admin/media" class="nav-link <%= currentPage === 'media' ? 'active' : '' %>">
<i class="nav-icon fas fa-images"></i>
<p>미디어 관리</p>
</a>
</li>
<li class="nav-header">시스템 설정</li>
<li class="nav-item">
<a href="/admin/settings" class="nav-link <%= currentPage === 'settings' ? 'active' : '' %>">
<i class="nav-icon fas fa-cog"></i>
<p>사이트 설정</p>
</a>
</li>
<li class="nav-item">
<a href="/admin/telegram" class="nav-link <%= currentPage === 'telegram' ? 'active' : '' %>">
<i class="nav-icon fab fa-telegram"></i>
<p>텔레그램 봇</p>
</a>
</li>
<li class="nav-item">
<a href="/admin/banner-editor" class="nav-link <%= currentPage === 'banner-editor' ? 'active' : '' %>">
<i class="nav-icon fas fa-paint-brush"></i>
<p>배너 편집기</p>
</a>
</li>
</ul>
</nav>
</div>
</aside>
<!-- Content Wrapper -->
<div class="content-wrapper">
<!-- Content Header -->
<div class="content-header">
<div class="container-fluid">
<div class="row mb-2">
<div class="col-sm-6">
<h1 class="m-0"><%= title %></h1>
</div>
<div class="col-sm-6">
<ol class="breadcrumb float-sm-right">
<li class="breadcrumb-item"><a href="/admin/dashboard">홈</a></li>
<% if (currentPage !== 'dashboard') { %>
<li class="breadcrumb-item active"><%= title %></li>
<% } %>
</ol>
</div>
</div>
</div>
</div>
<!-- Main content -->
<section class="content">
<div class="container-fluid">
<%- body %>
</div>
</section>
</div>
<!-- Footer -->
<footer class="main-footer">
<strong>&copy; 2024 <a href="/">SmartSolTech</a></strong>
모든 권리 보유.
<div class="float-right d-none d-sm-inline-block">
<b>Version</b> 2.0.0
</div>
</footer>
</div>
<!-- jQuery (Local) -->
<script src="/vendor/jquery/jquery-3.6.0.min.js"></script>
<!-- Bootstrap 4 (Local) -->
<script src="/vendor/bootstrap/bootstrap.bundle.min.js"></script>
<!-- AdminLTE App (Local) -->
<script src="/vendor/adminlte/adminlte.min.js"></script>
<!-- Custom JavaScript -->
<script src="/js/main.js"></script>
<script>
// Korean localization for AdminLTE
$(document).ready(function() {
// Update any English text to Korean
$('.brand-link .brand-text').text('스마트솔테크');
// Add smooth transitions
$('.nav-link').on('click', function() {
if (!$(this).hasClass('active')) {
$('.nav-link.active').removeClass('active');
$(this).addClass('active');
}
});
});
</script>
</body>
</html>

View File

@@ -0,0 +1,279 @@
<!DOCTYPE html>
<html lang="<%= currentLanguage %>">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><%= title %> - SmartSolTech Admin</title>
<!-- Bootstrap CSS (Local) -->
<link rel="stylesheet" href="/vendor/bootstrap/bootstrap.min.css">
<!-- AdminLTE CSS (Local) -->
<link rel="stylesheet" href="/vendor/adminlte/adminlte.min.css">
<!-- Font Awesome (Local) -->
<link rel="stylesheet" href="/vendor/adminlte/fontawesome.min.css">
<!-- Google Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@300;400;500;700&display=swap" rel="stylesheet">
<!-- Custom Korean Admin Styles -->
<style>
body, .content-wrapper, .main-sidebar {
font-family: 'Noto Sans KR', 'Malgun Gothic', 'Apple SD Gothic Neo', sans-serif;
}
.brand-text {
font-weight: 700;
color: #007bff !important;
}
.nav-sidebar .nav-link {
border-radius: 8px;
margin: 2px 0;
}
.nav-sidebar .nav-link.active {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.content-header h1 {
font-weight: 600;
color: #2c3e50;
}
.card {
border-radius: 12px;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
}
.btn {
border-radius: 8px;
font-weight: 500;
}
.info-box {
border-radius: 12px;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
}
.info-box-icon {
border-radius: 12px 0 0 12px;
}
.navbar-light {
background: white !important;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.main-sidebar {
background: linear-gradient(180deg, #f8f9fa 0%, #e9ecef 100%);
}
.sidebar-dark-primary .nav-sidebar > .nav-item > .nav-link:hover {
background-color: rgba(255,255,255,0.1);
}
</style>
</head>
<body class="hold-transition sidebar-mini layout-fixed">
<div class="wrapper">
<!-- Navbar -->
<nav class="main-header navbar navbar-expand navbar-light">
<!-- Left navbar links -->
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link" data-widget="pushmenu" href="#" role="button">
<i class="fas fa-bars"></i>
</a>
</li>
<li class="nav-item d-none d-sm-inline-block">
<a href="/" class="nav-link" target="_blank">
<i class="fas fa-external-link-alt mr-1"></i>
사이트 보기
</a>
</li>
</ul>
<!-- Right navbar links -->
<ul class="navbar-nav ml-auto">
<!-- User Menu -->
<li class="nav-item dropdown">
<a class="nav-link" data-toggle="dropdown" href="#">
<i class="far fa-user mr-1"></i>
<%= user ? user.name : '관리자' %>
</a>
<div class="dropdown-menu dropdown-menu-lg dropdown-menu-right">
<div class="dropdown-divider"></div>
<a href="/admin/settings" class="dropdown-item">
<i class="fas fa-cog mr-2"></i> 설정
</a>
<div class="dropdown-divider"></div>
<form action="/admin/logout" method="post" class="dropdown-item p-0">
<button type="submit" class="btn btn-link text-left w-100 text-danger">
<i class="fas fa-sign-out-alt mr-2"></i> 로그아웃
</button>
</form>
</div>
</li>
</ul>
</nav>
<!-- Main Sidebar Container -->
<aside class="main-sidebar sidebar-light-primary elevation-4">
<!-- Brand Logo -->
<a href="/admin/dashboard" class="brand-link">
<img src="/images/icons/icon-192x192.png" alt="SmartSolTech" class="brand-image img-circle elevation-3" style="opacity: .8">
<span class="brand-text font-weight-bold">SmartSolTech</span>
</a>
<!-- Sidebar -->
<div class="sidebar">
<!-- Sidebar Menu -->
<nav class="mt-3">
<ul class="nav nav-pills nav-sidebar flex-column" data-widget="treeview" role="menu" data-accordion="false">
<li class="nav-item">
<a href="/admin/dashboard" class="nav-link <%= currentPage === 'dashboard' ? 'active' : '' %>">
<i class="nav-icon fas fa-tachometer-alt"></i>
<p>대시보드</p>
</a>
</li>
<li class="nav-item">
<a href="/admin/portfolio" class="nav-link <%= currentPage === 'portfolio' ? 'active' : '' %>">
<i class="nav-icon fas fa-briefcase"></i>
<p>
포트폴리오
<% if (typeof stats !== 'undefined' && stats.portfolioCount) { %>
<span class="badge badge-info right"><%= stats.portfolioCount %></span>
<% } %>
</p>
</a>
</li>
<li class="nav-item">
<a href="/admin/services" class="nav-link <%= currentPage === 'services' ? 'active' : '' %>">
<i class="nav-icon fas fa-cogs"></i>
<p>
서비스 관리
<% if (typeof stats !== 'undefined' && stats.servicesCount) { %>
<span class="badge badge-success right"><%= stats.servicesCount %></span>
<% } %>
</p>
</a>
</li>
<li class="nav-item">
<a href="/admin/contacts" class="nav-link <%= currentPage === 'contacts' ? 'active' : '' %>">
<i class="nav-icon fas fa-envelope"></i>
<p>
문의 관리
<% if (typeof stats !== 'undefined' && stats.contactsCount) { %>
<span class="badge badge-warning right"><%= stats.contactsCount %></span>
<% } %>
</p>
</a>
</li>
<li class="nav-item">
<a href="/admin/media" class="nav-link <%= currentPage === 'media' ? 'active' : '' %>">
<i class="nav-icon fas fa-images"></i>
<p>미디어 관리</p>
</a>
</li>
<li class="nav-header">시스템 설정</li>
<li class="nav-item">
<a href="/admin/settings" class="nav-link <%= currentPage === 'settings' ? 'active' : '' %>">
<i class="nav-icon fas fa-cog"></i>
<p>사이트 설정</p>
</a>
</li>
<li class="nav-item">
<a href="/admin/telegram" class="nav-link <%= currentPage === 'telegram' ? 'active' : '' %>">
<i class="nav-icon fab fa-telegram"></i>
<p>텔레그램 봇</p>
</a>
</li>
<li class="nav-item">
<a href="/admin/banner-editor" class="nav-link <%= currentPage === 'banner-editor' ? 'active' : '' %>">
<i class="nav-icon fas fa-paint-brush"></i>
<p>배너 편집기</p>
</a>
</li>
</ul>
</nav>
</div>
</aside>
<!-- Content Wrapper -->
<div class="content-wrapper">
<!-- Content Header -->
<div class="content-header">
<div class="container-fluid">
<div class="row mb-2">
<div class="col-sm-6">
<h1 class="m-0"><%= title %></h1>
</div>
<div class="col-sm-6">
<ol class="breadcrumb float-sm-right">
<li class="breadcrumb-item"><a href="/admin/dashboard">홈</a></li>
<% if (currentPage !== 'dashboard') { %>
<li class="breadcrumb-item active"><%= title %></li>
<% } %>
</ol>
</div>
</div>
</div>
</div>
<!-- Main content -->
<section class="content">
<div class="container-fluid">
<%- body %>
</div>
</section>
</div>
<!-- Footer -->
<footer class="main-footer">
<strong>&copy; 2024 <a href="/">SmartSolTech</a></strong>
모든 권리 보유.
<div class="float-right d-none d-sm-inline-block">
<b>Version</b> 2.0.0
</div>
</footer>
</div>
<!-- jQuery (Local) -->
<script src="/vendor/jquery/jquery-3.6.0.min.js"></script>
<!-- Bootstrap 4 (Local) -->
<script src="/vendor/bootstrap/bootstrap.bundle.min.js"></script>
<!-- AdminLTE App (Local) -->
<script src="/vendor/adminlte/adminlte.min.js"></script>
<!-- Custom JavaScript -->
<script src="/js/main.js"></script>
<script>
// Korean localization for AdminLTE
$(document).ready(function() {
// Update any English text to Korean
$('.brand-link .brand-text').text('스마트솔테크');
// Add smooth transitions
$('.nav-link').on('click', function() {
if (!$(this).hasClass('active')) {
$('.nav-link.active').removeClass('active');
$(this).addClass('active');
}
});
});
</script>
</body>
</html>

View File

@@ -0,0 +1,328 @@
<!DOCTYPE html>
<html lang="<%= currentLanguage %>">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><%= title %> - SmartSolTech Admin</title>
<!-- Bootstrap CSS (Local) -->
<link rel="stylesheet" href="/vendor/bootstrap/bootstrap.min.css">
<!-- AdminLTE CSS (Local) -->
<link rel="stylesheet" href="/vendor/adminlte/adminlte.min.css">
<!-- Font Awesome (Local) -->
<link rel="stylesheet" href="/vendor/adminlte/fontawesome.min.css">
<!-- Google Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@300;400;500;700&display=swap" rel="stylesheet">
<!-- Custom Korean Admin Styles -->
<style>
body, .content-wrapper, .main-sidebar {
font-family: 'Noto Sans KR', 'Malgun Gothic', 'Apple SD Gothic Neo', sans-serif;
}
.brand-text {
font-weight: 700;
color: #007bff !important;
}
/* Sidebar navigation styles */
.main-sidebar .nav-sidebar .nav-link {
border-radius: 8px;
margin: 2px 8px;
transition: all 0.3s ease;
color: #495057;
}
.main-sidebar .nav-sidebar .nav-link:hover {
background-color: rgba(0, 123, 255, 0.1);
color: #007bff;
transform: translateX(5px);
}
.main-sidebar .nav-sidebar .nav-link.active {
background: linear-gradient(135deg, #007bff 0%, #0056b3 100%);
color: white !important;
box-shadow: 0 4px 8px rgba(0, 123, 255, 0.3);
}
.main-sidebar .nav-sidebar .nav-link.active i {
color: white !important;
}
.main-sidebar .nav-sidebar .nav-link.active .badge {
background-color: rgba(255, 255, 255, 0.2) !important;
color: white !important;
}
/* Fix for navigation icons */
.nav-icon {
margin-right: 8px;
width: 20px;
text-align: center;
}
.content-header h1 {
font-weight: 600;
color: #2c3e50;
}
.card {
border-radius: 12px;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
border: none;
}
.btn {
border-radius: 8px;
font-weight: 500;
}
.info-box {
border-radius: 12px;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
}
.info-box-icon {
border-radius: 12px 0 0 12px;
}
.navbar-light {
background: white !important;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.main-sidebar {
background: linear-gradient(180deg, #f8f9fa 0%, #e9ecef 100%);
}
/* Breadcrumb styling */
.breadcrumb {
background: transparent;
padding: 0;
}
.breadcrumb-item a {
color: #007bff;
text-decoration: none;
}
.breadcrumb-item.active {
color: #6c757d;
}
/* Content wrapper padding */
.content-wrapper {
padding-top: 20px;
}
/* Badges in navigation */
.nav-sidebar .badge {
font-size: 0.75rem;
padding: 0.25rem 0.5rem;
}
</style>
</head>
<body class="hold-transition sidebar-mini layout-fixed">
<div class="wrapper">
<!-- Navbar -->
<nav class="main-header navbar navbar-expand navbar-light">
<!-- Left navbar links -->
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link" data-widget="pushmenu" href="#" role="button">
<i class="fas fa-bars"></i>
</a>
</li>
<li class="nav-item d-none d-sm-inline-block">
<a href="/" class="nav-link" target="_blank">
<i class="fas fa-external-link-alt mr-1"></i>
사이트 보기
</a>
</li>
</ul>
<!-- Right navbar links -->
<ul class="navbar-nav ml-auto">
<!-- User Menu -->
<li class="nav-item dropdown">
<a class="nav-link" data-toggle="dropdown" href="#">
<i class="far fa-user mr-1"></i>
<%= user ? user.name : '관리자' %>
</a>
<div class="dropdown-menu dropdown-menu-lg dropdown-menu-right">
<div class="dropdown-divider"></div>
<a href="/admin/settings" class="dropdown-item">
<i class="fas fa-cog mr-2"></i> 설정
</a>
<div class="dropdown-divider"></div>
<form action="/admin/logout" method="post" class="dropdown-item p-0">
<button type="submit" class="btn btn-link text-left w-100 text-danger">
<i class="fas fa-sign-out-alt mr-2"></i> 로그아웃
</button>
</form>
</div>
</li>
</ul>
</nav>
<!-- Main Sidebar Container -->
<aside class="main-sidebar sidebar-light-primary elevation-4">
<!-- Brand Logo -->
<a href="/admin/dashboard" class="brand-link">
<img src="/images/icons/icon-192x192.png" alt="SmartSolTech" class="brand-image img-circle elevation-3" style="opacity: .8">
<span class="brand-text font-weight-bold">SmartSolTech</span>
</a>
<!-- Sidebar -->
<div class="sidebar">
<!-- Sidebar Menu -->
<nav class="mt-3">
<ul class="nav nav-pills nav-sidebar flex-column" data-widget="treeview" role="menu" data-accordion="false">
<li class="nav-item">
<a href="/admin/dashboard" class="nav-link <%= currentPage === 'dashboard' ? 'active' : '' %>">
<i class="nav-icon fas fa-tachometer-alt"></i>
<p>대시보드</p>
</a>
</li>
<li class="nav-item">
<a href="/admin/portfolio" class="nav-link <%= currentPage === 'portfolio' ? 'active' : '' %>">
<i class="nav-icon fas fa-briefcase"></i>
<p>
포트폴리오
<% if (typeof stats !== 'undefined' && stats.portfolioCount) { %>
<span class="badge badge-info right"><%= stats.portfolioCount %></span>
<% } %>
</p>
</a>
</li>
<li class="nav-item">
<a href="/admin/services" class="nav-link <%= currentPage === 'services' ? 'active' : '' %>">
<i class="nav-icon fas fa-cogs"></i>
<p>
서비스 관리
<% if (typeof stats !== 'undefined' && stats.servicesCount) { %>
<span class="badge badge-success right"><%= stats.servicesCount %></span>
<% } %>
</p>
</a>
</li>
<li class="nav-item">
<a href="/admin/contacts" class="nav-link <%= currentPage === 'contacts' ? 'active' : '' %>">
<i class="nav-icon fas fa-envelope"></i>
<p>
문의 관리
<% if (typeof stats !== 'undefined' && stats.contactsCount) { %>
<span class="badge badge-warning right"><%= stats.contactsCount %></span>
<% } %>
</p>
</a>
</li>
<li class="nav-item">
<a href="/admin/media" class="nav-link <%= currentPage === 'media' ? 'active' : '' %>">
<i class="nav-icon fas fa-images"></i>
<p>미디어 관리</p>
</a>
</li>
<li class="nav-header">시스템 설정</li>
<li class="nav-item">
<a href="/admin/settings" class="nav-link <%= currentPage === 'settings' ? 'active' : '' %>">
<i class="nav-icon fas fa-cog"></i>
<p>사이트 설정</p>
</a>
</li>
<li class="nav-item">
<a href="/admin/telegram" class="nav-link <%= currentPage === 'telegram' ? 'active' : '' %>">
<i class="nav-icon fab fa-telegram"></i>
<p>텔레그램 봇</p>
</a>
</li>
<li class="nav-item">
<a href="/admin/banner-editor" class="nav-link <%= currentPage === 'banner-editor' ? 'active' : '' %>">
<i class="nav-icon fas fa-paint-brush"></i>
<p>배너 편집기</p>
</a>
</li>
</ul>
</nav>
</div>
</aside>
<!-- Content Wrapper -->
<div class="content-wrapper">
<!-- Content Header -->
<div class="content-header">
<div class="container-fluid">
<div class="row mb-2">
<div class="col-sm-6">
<h1 class="m-0"><%= title %></h1>
</div>
<div class="col-sm-6">
<ol class="breadcrumb float-sm-right">
<li class="breadcrumb-item"><a href="/admin/dashboard">홈</a></li>
<% if (currentPage !== 'dashboard') { %>
<li class="breadcrumb-item active"><%= title %></li>
<% } %>
</ol>
</div>
</div>
</div>
</div>
<!-- Main content -->
<section class="content">
<div class="container-fluid">
<%- body %>
</div>
</section>
</div>
<!-- Footer -->
<footer class="main-footer">
<strong>&copy; 2024 <a href="/">SmartSolTech</a></strong>
모든 권리 보유.
<div class="float-right d-none d-sm-inline-block">
<b>Version</b> 2.0.0
</div>
</footer>
</div>
<!-- jQuery (Local) -->
<script src="/vendor/jquery/jquery-3.6.0.min.js"></script>
<!-- Bootstrap 4 (Local) -->
<script src="/vendor/bootstrap/bootstrap.bundle.min.js"></script>
<!-- AdminLTE App (Local) -->
<script src="/vendor/adminlte/adminlte.min.js"></script>
<!-- Custom JavaScript -->
<script src="/js/main.js"></script>
<script>
// Korean localization for AdminLTE
$(document).ready(function() {
// Update any English text to Korean
$('.brand-link .brand-text').text('스마트솔테크');
// Add smooth transitions
$('.nav-link').on('click', function() {
if (!$(this).hasClass('active')) {
$('.nav-link.active').removeClass('active');
$(this).addClass('active');
}
});
});
</script>
</body>
</html>

View File

@@ -0,0 +1,353 @@
<!DOCTYPE html>
<html lang="<%= currentLanguage %>">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><%= title %> - SmartSolTech Admin</title>
<!-- Bootstrap CSS (Local) -->
<link rel="stylesheet" href="/vendor/bootstrap/bootstrap.min.css">
<!-- AdminLTE CSS (Local) -->
<link rel="stylesheet" href="/vendor/adminlte/adminlte.min.css">
<!-- Font Awesome (Local) -->
<link rel="stylesheet" href="/vendor/adminlte/fontawesome.min.css">
<!-- Google Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@300;400;500;700&display=swap" rel="stylesheet">
<!-- Custom Korean Admin Styles -->
<style>
body, .content-wrapper, .main-sidebar {
font-family: 'Noto Sans KR', 'Malgun Gothic', 'Apple SD Gothic Neo', sans-serif;
}
.brand-text {
font-weight: 700;
color: #007bff !important;
}
/* Sidebar navigation styles */
.main-sidebar .nav-sidebar .nav-link {
border-radius: 8px;
margin: 2px 8px;
transition: all 0.3s ease;
color: #495057;
}
.main-sidebar .nav-sidebar .nav-link:hover {
background-color: rgba(0, 123, 255, 0.1);
color: #007bff;
transform: translateX(5px);
}
.main-sidebar .nav-sidebar .nav-link.active {
background: linear-gradient(135deg, #007bff 0%, #0056b3 100%);
color: white !important;
box-shadow: 0 4px 8px rgba(0, 123, 255, 0.3);
}
.main-sidebar .nav-sidebar .nav-link.active i {
color: white !important;
}
.main-sidebar .nav-sidebar .nav-link.active .badge {
background-color: rgba(255, 255, 255, 0.2) !important;
color: white !important;
}
/* Fix for navigation icons */
.nav-icon {
margin-right: 8px;
width: 20px;
text-align: center;
}
.content-header h1 {
font-weight: 600;
color: #2c3e50;
}
.card {
border-radius: 12px;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
border: none;
}
.btn {
border-radius: 8px;
font-weight: 500;
}
.info-box {
border-radius: 12px;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
}
.info-box-icon {
border-radius: 12px 0 0 12px;
}
.navbar-light {
background: white !important;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.main-sidebar {
background: linear-gradient(180deg, #f8f9fa 0%, #e9ecef 100%);
}
/* Breadcrumb styling */
.breadcrumb {
background: transparent;
padding: 0;
}
.breadcrumb-item a {
color: #007bff;
text-decoration: none;
}
.breadcrumb-item.active {
color: #6c757d;
}
/* Content wrapper padding */
.content-wrapper {
padding-top: 20px;
}
/* Badges in navigation */
.nav-sidebar .badge {
font-size: 0.75rem;
padding: 0.25rem 0.5rem;
}
</style>
</head>
<body class="hold-transition sidebar-mini layout-fixed">
<div class="wrapper">
<!-- Navbar -->
<nav class="main-header navbar navbar-expand navbar-light">
<!-- Left navbar links -->
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link" data-widget="pushmenu" href="#" role="button">
<i class="fas fa-bars"></i>
</a>
</li>
<li class="nav-item d-none d-sm-inline-block">
<a href="/" class="nav-link" target="_blank">
<i class="fas fa-external-link-alt mr-1"></i>
사이트 보기
</a>
</li>
</ul>
<!-- Right navbar links -->
<ul class="navbar-nav ml-auto">
<!-- User Menu -->
<li class="nav-item dropdown">
<a class="nav-link" data-toggle="dropdown" href="#">
<i class="far fa-user mr-1"></i>
<%= user ? user.name : '관리자' %>
</a>
<div class="dropdown-menu dropdown-menu-lg dropdown-menu-right">
<div class="dropdown-divider"></div>
<a href="/admin/settings" class="dropdown-item">
<i class="fas fa-cog mr-2"></i> 설정
</a>
<div class="dropdown-divider"></div>
<form action="/admin/logout" method="post" class="dropdown-item p-0">
<button type="submit" class="btn btn-link text-left w-100 text-danger">
<i class="fas fa-sign-out-alt mr-2"></i> 로그아웃
</button>
</form>
</div>
</li>
</ul>
</nav>
<!-- Main Sidebar Container -->
<aside class="main-sidebar sidebar-light-primary elevation-4">
<!-- Brand Logo -->
<a href="/admin/dashboard" class="brand-link">
<img src="/images/icons/icon-192x192.png" alt="SmartSolTech" class="brand-image img-circle elevation-3" style="opacity: .8">
<span class="brand-text font-weight-bold">SmartSolTech</span>
</a>
<!-- Sidebar -->
<div class="sidebar">
<!-- Sidebar Menu -->
<nav class="mt-3">
<ul class="nav nav-pills nav-sidebar flex-column" data-widget="treeview" role="menu" data-accordion="false">
<li class="nav-item">
<a href="/admin/dashboard" class="nav-link <%= currentPage === 'dashboard' ? 'active' : '' %>">
<i class="nav-icon fas fa-tachometer-alt"></i>
<p>대시보드</p>
</a>
</li>
<li class="nav-item">
<a href="/admin/portfolio" class="nav-link <%= currentPage === 'portfolio' ? 'active' : '' %>">
<i class="nav-icon fas fa-briefcase"></i>
<p>
포트폴리오
<% if (typeof stats !== 'undefined' && stats.portfolioCount) { %>
<span class="badge badge-info right"><%= stats.portfolioCount %></span>
<% } %>
</p>
</a>
</li>
<li class="nav-item">
<a href="/admin/services" class="nav-link <%= currentPage === 'services' ? 'active' : '' %>">
<i class="nav-icon fas fa-cogs"></i>
<p>
서비스 관리
<% if (typeof stats !== 'undefined' && stats.servicesCount) { %>
<span class="badge badge-success right"><%= stats.servicesCount %></span>
<% } %>
</p>
</a>
</li>
<li class="nav-item">
<a href="/admin/contacts" class="nav-link <%= currentPage === 'contacts' ? 'active' : '' %>">
<i class="nav-icon fas fa-envelope"></i>
<p>
문의 관리
<% if (typeof stats !== 'undefined' && stats.contactsCount) { %>
<span class="badge badge-warning right"><%= stats.contactsCount %></span>
<% } %>
</p>
</a>
</li>
<li class="nav-item">
<a href="/admin/media" class="nav-link <%= currentPage === 'media' ? 'active' : '' %>">
<i class="nav-icon fas fa-images"></i>
<p>미디어 관리</p>
</a>
</li>
<li class="nav-header">시스템 설정</li>
<li class="nav-item">
<a href="/admin/settings" class="nav-link <%= currentPage === 'settings' ? 'active' : '' %>">
<i class="nav-icon fas fa-cog"></i>
<p>사이트 설정</p>
</a>
</li>
<li class="nav-item">
<a href="/admin/telegram" class="nav-link <%= currentPage === 'telegram' ? 'active' : '' %>">
<i class="nav-icon fab fa-telegram"></i>
<p>텔레그램 봇</p>
</a>
</li>
<li class="nav-item">
<a href="/admin/banner-editor" class="nav-link <%= currentPage === 'banner-editor' ? 'active' : '' %>">
<i class="nav-icon fas fa-paint-brush"></i>
<p>배너 편집기</p>
</a>
</li>
</ul>
</nav>
</div>
</aside>
<!-- Content Wrapper -->
<div class="content-wrapper">
<!-- Content Header -->
<div class="content-header">
<div class="container-fluid">
<div class="row mb-2">
<div class="col-sm-6">
<h1 class="m-0"><%= title %></h1>
</div>
<div class="col-sm-6">
<ol class="breadcrumb float-sm-right">
<li class="breadcrumb-item"><a href="/admin/dashboard">홈</a></li>
<% if (currentPage !== 'dashboard') { %>
<li class="breadcrumb-item active"><%= title %></li>
<% } %>
</ol>
</div>
</div>
</div>
</div>
<!-- Main content -->
<section class="content">
<div class="container-fluid">
<%- body %>
</div>
</section>
</div>
<!-- Footer -->
<footer class="main-footer">
<strong>&copy; 2024 <a href="/">SmartSolTech</a></strong>
모든 권리 보유.
<div class="float-right d-none d-sm-inline-block">
<b>Version</b> 2.0.0
</div>
</footer>
</div>
<!-- jQuery (Local) -->
<script src="/vendor/jquery/jquery-3.6.0.min.js"></script>
<!-- Bootstrap 4 (Local) -->
<script src="/vendor/bootstrap/bootstrap.bundle.min.js"></script>
<!-- AdminLTE App (Local) -->
<script src="/vendor/adminlte/adminlte.min.js"></script>
<!-- Custom JavaScript -->
<script src="/js/main.js"></script>
<script>
// Korean localization for AdminLTE
$(document).ready(function() {
// Update any English text to Korean
$('.brand-link .brand-text').text('스마트솔테크');
// Get current page from URL
const currentPath = window.location.pathname;
const currentPage = currentPath.split('/').pop() || 'dashboard';
// Remove active class from all nav links
$('.nav-sidebar .nav-link').removeClass('active');
// Add active class to current page nav link
$('.nav-sidebar .nav-link').each(function() {
const href = $(this).attr('href');
if (href) {
const pageName = href.split('/').pop();
if (currentPath.includes(pageName) ||
(currentPath === '/admin' && pageName === 'dashboard') ||
(currentPath === '/admin/' && pageName === 'dashboard')) {
$(this).addClass('active');
}
}
});
// Add smooth transitions
$('.nav-link').on('click', function() {
if (!$(this).hasClass('active')) {
$('.nav-link.active').removeClass('active');
$(this).addClass('active');
}
});
// Initialize AdminLTE components
if (typeof AdminLTE !== 'undefined') {
AdminLTE.init();
}
});
</script>
</body>
</html>

View File

@@ -0,0 +1,353 @@
<!DOCTYPE html>
<html lang="<%= currentLanguage %>">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><%= title %> - SmartSolTech Admin</title>
<!-- Bootstrap CSS (Local) -->
<link rel="stylesheet" href="/vendor/bootstrap/bootstrap.min.css">
<!-- AdminLTE CSS (Local) -->
<link rel="stylesheet" href="/vendor/adminlte/adminlte.min.css">
<!-- Font Awesome (Local) -->
<link rel="stylesheet" href="/vendor/adminlte/fontawesome.min.css">
<!-- Google Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@300;400;500;700&display=swap" rel="stylesheet">
<!-- Custom Korean Admin Styles -->
<style>
body, .content-wrapper, .main-sidebar {
font-family: 'Noto Sans KR', 'Malgun Gothic', 'Apple SD Gothic Neo', sans-serif;
}
.brand-text {
font-weight: 700;
color: #007bff !important;
}
/* Sidebar navigation styles */
.main-sidebar .nav-sidebar .nav-link {
border-radius: 8px;
margin: 2px 8px;
transition: all 0.3s ease;
color: #495057;
}
.main-sidebar .nav-sidebar .nav-link:hover {
background-color: rgba(0, 123, 255, 0.1);
color: #007bff;
transform: translateX(5px);
}
.main-sidebar .nav-sidebar .nav-link.active {
background: linear-gradient(135deg, #007bff 0%, #0056b3 100%);
color: white !important;
box-shadow: 0 4px 8px rgba(0, 123, 255, 0.3);
}
.main-sidebar .nav-sidebar .nav-link.active i {
color: white !important;
}
.main-sidebar .nav-sidebar .nav-link.active .badge {
background-color: rgba(255, 255, 255, 0.2) !important;
color: white !important;
}
/* Fix for navigation icons */
.nav-icon {
margin-right: 8px;
width: 20px;
text-align: center;
}
.content-header h1 {
font-weight: 600;
color: #2c3e50;
}
.card {
border-radius: 12px;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
border: none;
}
.btn {
border-radius: 8px;
font-weight: 500;
}
.info-box {
border-radius: 12px;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
}
.info-box-icon {
border-radius: 12px 0 0 12px;
}
.navbar-light {
background: white !important;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.main-sidebar {
background: linear-gradient(180deg, #f8f9fa 0%, #e9ecef 100%);
}
/* Breadcrumb styling */
.breadcrumb {
background: transparent;
padding: 0;
}
.breadcrumb-item a {
color: #007bff;
text-decoration: none;
}
.breadcrumb-item.active {
color: #6c757d;
}
/* Content wrapper padding */
.content-wrapper {
padding-top: 20px;
}
/* Badges in navigation */
.nav-sidebar .badge {
font-size: 0.75rem;
padding: 0.25rem 0.5rem;
}
</style>
</head>
<body class="hold-transition sidebar-mini layout-fixed">
<div class="wrapper">
<!-- Navbar -->
<nav class="main-header navbar navbar-expand navbar-light">
<!-- Left navbar links -->
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link" data-widget="pushmenu" href="#" role="button">
<i class="fas fa-bars"></i>
</a>
</li>
<li class="nav-item d-none d-sm-inline-block">
<a href="/" class="nav-link" target="_blank">
<i class="fas fa-external-link-alt mr-1"></i>
사이트 보기
</a>
</li>
</ul>
<!-- Right navbar links -->
<ul class="navbar-nav ml-auto">
<!-- User Menu -->
<li class="nav-item dropdown">
<a class="nav-link" data-toggle="dropdown" href="#">
<i class="far fa-user mr-1"></i>
<%= user ? user.name : '관리자' %>
</a>
<div class="dropdown-menu dropdown-menu-lg dropdown-menu-right">
<div class="dropdown-divider"></div>
<a href="/admin/settings" class="dropdown-item">
<i class="fas fa-cog mr-2"></i> 설정
</a>
<div class="dropdown-divider"></div>
<form action="/admin/logout" method="post" class="dropdown-item p-0">
<button type="submit" class="btn btn-link text-left w-100 text-danger">
<i class="fas fa-sign-out-alt mr-2"></i> 로그아웃
</button>
</form>
</div>
</li>
</ul>
</nav>
<!-- Main Sidebar Container -->
<aside class="main-sidebar sidebar-light-primary elevation-4">
<!-- Brand Logo -->
<a href="/admin/dashboard" class="brand-link">
<img src="/images/icons/icon-192x192.png" alt="SmartSolTech" class="brand-image img-circle elevation-3" style="opacity: .8">
<span class="brand-text font-weight-bold">SmartSolTech</span>
</a>
<!-- Sidebar -->
<div class="sidebar">
<!-- Sidebar Menu -->
<nav class="mt-3">
<ul class="nav nav-pills nav-sidebar flex-column" data-widget="treeview" role="menu" data-accordion="false">
<li class="nav-item">
<a href="/admin/dashboard" class="nav-link <%= currentPage === 'dashboard' ? 'active' : '' %>">
<i class="nav-icon fas fa-tachometer-alt"></i>
<p>대시보드</p>
</a>
</li>
<li class="nav-item">
<a href="/admin/portfolio" class="nav-link <%= currentPage === 'portfolio' ? 'active' : '' %>">
<i class="nav-icon fas fa-briefcase"></i>
<p>
포트폴리오
<% if (typeof stats !== 'undefined' && stats.portfolioCount) { %>
<span class="badge badge-info right"><%= stats.portfolioCount %></span>
<% } %>
</p>
</a>
</li>
<li class="nav-item">
<a href="/admin/services" class="nav-link <%= currentPage === 'services' ? 'active' : '' %>">
<i class="nav-icon fas fa-cogs"></i>
<p>
서비스 관리
<% if (typeof stats !== 'undefined' && stats.servicesCount) { %>
<span class="badge badge-success right"><%= stats.servicesCount %></span>
<% } %>
</p>
</a>
</li>
<li class="nav-item">
<a href="/admin/contacts" class="nav-link <%= currentPage === 'contacts' ? 'active' : '' %>">
<i class="nav-icon fas fa-envelope"></i>
<p>
문의 관리
<% if (typeof stats !== 'undefined' && stats.contactsCount) { %>
<span class="badge badge-warning right"><%= stats.contactsCount %></span>
<% } %>
</p>
</a>
</li>
<li class="nav-item">
<a href="/admin/media" class="nav-link <%= currentPage === 'media' ? 'active' : '' %>">
<i class="nav-icon fas fa-images"></i>
<p>미디어 관리</p>
</a>
</li>
<li class="nav-header">시스템 설정</li>
<li class="nav-item">
<a href="/admin/settings" class="nav-link <%= currentPage === 'settings' ? 'active' : '' %>">
<i class="nav-icon fas fa-cog"></i>
<p>사이트 설정</p>
</a>
</li>
<li class="nav-item">
<a href="/admin/telegram" class="nav-link <%= currentPage === 'telegram' ? 'active' : '' %>">
<i class="nav-icon fab fa-telegram"></i>
<p>텔레그램 봇</p>
</a>
</li>
<li class="nav-item">
<a href="/admin/banner-editor" class="nav-link <%= currentPage === 'banner-editor' ? 'active' : '' %>">
<i class="nav-icon fas fa-paint-brush"></i>
<p>배너 편집기</p>
</a>
</li>
</ul>
</nav>
</div>
</aside>
<!-- Content Wrapper -->
<div class="content-wrapper">
<!-- Content Header -->
<div class="content-header">
<div class="container-fluid">
<div class="row mb-2">
<div class="col-sm-6">
<h1 class="m-0"><%= title %></h1>
</div>
<div class="col-sm-6">
<ol class="breadcrumb float-sm-right">
<li class="breadcrumb-item"><a href="/admin/dashboard">홈</a></li>
<% if (currentPage !== 'dashboard') { %>
<li class="breadcrumb-item active"><%= title %></li>
<% } %>
</ol>
</div>
</div>
</div>
</div>
<!-- Main content -->
<section class="content">
<div class="container-fluid">
<%- body %>
</div>
</section>
</div>
<!-- Footer -->
<footer class="main-footer">
<strong>&copy; 2024 <a href="/">SmartSolTech</a></strong>
모든 권리 보유.
<div class="float-right d-none d-sm-inline-block">
<b>Version</b> 2.0.0
</div>
</footer>
</div>
<!-- jQuery (Local) -->
<script src="/vendor/jquery/jquery-3.6.0.min.js"></script>
<!-- Bootstrap 4 (Local) -->
<script src="/vendor/bootstrap/bootstrap.bundle.min.js"></script>
<!-- AdminLTE App (Local) -->
<script src="/vendor/adminlte/adminlte.min.js"></script>
<!-- Custom JavaScript -->
<script src="/js/main.js"></script>
<script>
// Korean localization for AdminLTE
$(document).ready(function() {
// Update any English text to Korean
$('.brand-link .brand-text').text('스마트솔테크');
// Get current page from URL
const currentPath = window.location.pathname;
const currentPage = currentPath.split('/').pop() || 'dashboard';
// Remove active class from all nav links
$('.nav-sidebar .nav-link').removeClass('active');
// Add active class to current page nav link
$('.nav-sidebar .nav-link').each(function() {
const href = $(this).attr('href');
if (href) {
const pageName = href.split('/').pop();
if (currentPath.includes(pageName) ||
(currentPath === '/admin' && pageName === 'dashboard') ||
(currentPath === '/admin/' && pageName === 'dashboard')) {
$(this).addClass('active');
}
}
});
// Add smooth transitions
$('.nav-link').on('click', function() {
if (!$(this).hasClass('active')) {
$('.nav-link.active').removeClass('active');
$(this).addClass('active');
}
});
// Initialize AdminLTE components
if (typeof AdminLTE !== 'undefined') {
AdminLTE.init();
}
});
</script>
</body>
</html>

View File

@@ -0,0 +1,788 @@
<!-- Content Header (Page header) -->
<section class="content-header">
<div class="container-fluid">
<div class="row mb-2">
<div class="col-sm-6">
<h1><i class="fas fa-images mr-2"></i>Медиа Галерея</h1>
</div>
<div class="col-sm-6">
<div class="float-sm-right">
<button id="refresh-btn" class="btn btn-secondary">
<i class="fas fa-sync-alt mr-1"></i>Обновить
</button>
<button id="upload-btn" class="btn btn-primary">
<i class="fas fa-upload mr-1"></i>Загрузить файлы
</button>
</div>
</div>
</div>
</div>
</section>
<!-- Main content -->
<section class="content">
<div class="container-fluid">
<!-- Upload Zone -->
<div id="upload-zone" class="card" style="display: none;">
<div class="card-body text-center">
<div class="mb-4">
<i class="fas fa-cloud-upload-alt fa-6x text-muted mb-4"></i>
<p class="h5 text-muted mb-2">Перетащите файлы сюда или нажмите для выбора</p>
<p class="text-muted">Поддерживаются: JPG, PNG, GIF, SVG (максимум 10MB каждый)</p>
</div>
<input type="file" id="file-input" multiple accept="image/*" class="d-none">
<button type="button" onclick="document.getElementById('file-input').click()" class="btn btn-primary">
Выбрать файлы
</button>
<button id="cancel-upload" class="btn btn-secondary ml-3">
Отмена
</button>
</div>
</div>
<!-- Upload Progress -->
<div id="upload-progress" class="card" style="display: none;">
<div class="card-header">
<h3 class="card-title">Загрузка файлов</h3>
</div>
<div class="card-body">
<div id="progress-list">
<!-- Progress items will be added here -->
</div>
</div>
</div>
<!-- Filter and Search -->
<div class="card">
<div class="card-body">
<div class="row">
<div class="col-md-3">
<div class="form-group">
<label>Тип файла</label>
<select id="file-type-filter" class="form-control">
<option value="">Все типы</option>
<option value="image/jpeg">JPEG</option>
<option value="image/png">PNG</option>
<option value="image/gif">GIF</option>
<option value="image/svg+xml">SVG</option>
</select>
</div>
</div>
<div class="col-md-3">
<div class="form-group">
<label>Размер</label>
<select id="size-filter" class="form-control">
<option value="">Любой размер</option>
<option value="small">Маленький (&lt; 1MB)</option>
<option value="medium">Средний (1-5MB)</option>
<option value="large">Большой (&gt; 5MB)</option>
</select>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label>Поиск</label>
<div class="input-group">
<input type="text" id="search-input" placeholder="Поиск по имени файла..." class="form-control">
<div class="input-group-append">
<span class="input-group-text"><i class="fas fa-search"></i></span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Media Grid -->
<div class="card">
<div class="card-header">
<h3 class="card-title">Файлы</h3>
<div class="card-tools">
<span id="file-count" class="badge badge-secondary">Загрузка...</span>
<div class="btn-group ml-2">
<button id="grid-view" class="btn btn-sm btn-default">
<i class="fas fa-th-large"></i>
</button>
<button id="list-view" class="btn btn-sm btn-default">
<i class="fas fa-list"></i>
</button>
</div>
</div>
</div>
<div class="card-body">
<!-- Loading State -->
<div id="loading" class="text-center py-5">
<div class="spinner-border text-primary" role="status">
<span class="sr-only">Загрузка...</span>
</div>
<p class="mt-2 text-muted">Загрузка медиа файлов...</p>
</div>
<!-- Empty State -->
<div id="empty-state" class="text-center py-5" style="display: none;">
<i class="fas fa-images fa-6x text-muted mb-4"></i>
<h4 class="text-muted mb-2">Нет загруженных файлов</h4>
<p class="text-muted mb-4">Начните с загрузки ваших первых изображений</p>
<button onclick="document.getElementById('upload-btn').click()" class="btn btn-primary">
<i class="fas fa-upload mr-2"></i>Загрузить файлы
</button>
</div>
<!-- Media Grid -->
<div id="media-grid" class="row">
<!-- Media items will be loaded here -->
</div>
<!-- Media List -->
<div id="media-list" style="display: none;">
<!-- List items will be loaded here -->
</div>
</div>
<!-- Pagination -->
<div class="card-footer" id="pagination" style="display: none;">
<nav aria-label="Page navigation">
<ul class="pagination justify-content-center m-0">
<li class="page-item">
<button id="prev-page" class="page-link">
<i class="fas fa-chevron-left"></i>
</button>
</li>
<div id="page-numbers" class="d-flex">
<!-- Page numbers will be added here -->
</div>
<li class="page-item">
<button id="next-page" class="page-link">
<i class="fas fa-chevron-right"></i>
</button>
</li>
</ul>
</nav>
</div>
</div>
</div>
</section>
<!-- Media Preview Modal -->
<div class="modal fade" id="preview-modal" tabindex="-1" role="dialog">
<div class="modal-dialog modal-xl" role="document">
<div class="modal-content">
<div class="modal-header">
<h4 id="modal-title" class="modal-title">Предпросмотр файла</h4>
<button type="button" class="close" data-dismiss="modal">
<span>&times;</span>
</button>
</div>
<div class="modal-body">
<div class="row">
<div class="col-md-8">
<img id="modal-image" src="" alt="" class="img-fluid rounded">
</div>
<div class="col-md-4">
<div class="form-group">
<label>Имя файла</label>
<input id="modal-filename" type="text" class="form-control" readonly>
</div>
<div class="form-group">
<label>URL</label>
<div class="input-group">
<input id="modal-url" type="text" class="form-control" readonly>
<div class="input-group-append">
<button onclick="copyToClipboard()" class="btn btn-outline-secondary" type="button">
<i class="fas fa-copy"></i>
</button>
</div>
</div>
</div>
<div class="row">
<div class="col-6">
<div class="form-group">
<label>Размер</label>
<p id="modal-size" class="form-control-plaintext">-</p>
</div>
</div>
<div class="col-6">
<div class="form-group">
<label>Тип</label>
<p id="modal-type" class="form-control-plaintext">-</p>
</div>
</div>
</div>
<div class="row">
<div class="col-6">
<div class="form-group">
<label>Ширина</label>
<p id="modal-width" class="form-control-plaintext">-</p>
</div>
</div>
<div class="col-6">
<div class="form-group">
<label>Высота</label>
<p id="modal-height" class="form-control-plaintext">-</p>
</div>
</div>
</div>
<div class="form-group">
<label>Загружено</label>
<p id="modal-date" class="form-control-plaintext">-</p>
</div>
<div class="btn-group d-flex">
<button onclick="downloadFile()" class="btn btn-primary">
<i class="fas fa-download mr-1"></i>Скачать
</button>
<button onclick="deleteFile()" class="btn btn-danger">
<i class="fas fa-trash mr-1"></i>Удалить
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
class MediaGallery {
constructor() {
this.currentFiles = [];
this.filteredFiles = [];
this.currentView = 'grid';
this.currentPage = 1;
this.itemsPerPage = 24;
this.currentFile = null;
this.init();
}
init() {
this.setupEventListeners();
this.loadMedia();
}
setupEventListeners() {
// Upload button
document.getElementById('upload-btn').addEventListener('click', () => {
this.showUploadZone();
});
// Cancel upload
document.getElementById('cancel-upload').addEventListener('click', () => {
this.hideUploadZone();
});
// File input
document.getElementById('file-input').addEventListener('change', (e) => {
this.handleFiles(e.target.files);
});
// Refresh button
document.getElementById('refresh-btn').addEventListener('click', () => {
this.loadMedia();
});
// View toggle
document.getElementById('grid-view').addEventListener('click', () => {
this.setView('grid');
});
document.getElementById('list-view').addEventListener('click', () => {
this.setView('list');
});
// Filters
document.getElementById('file-type-filter').addEventListener('change', () => {
this.applyFilters();
});
document.getElementById('size-filter').addEventListener('change', () => {
this.applyFilters();
});
document.getElementById('search-input').addEventListener('input', () => {
this.applyFilters();
});
// Modal
document.getElementById('close-modal').addEventListener('click', () => {
this.closeModal();
});
// Upload zone drag and drop
const uploadZone = document.getElementById('upload-zone');
uploadZone.addEventListener('dragover', (e) => {
e.preventDefault();
uploadZone.classList.add('border-blue-500', 'bg-blue-50');
});
uploadZone.addEventListener('dragleave', () => {
uploadZone.classList.remove('border-blue-500', 'bg-blue-50');
});
uploadZone.addEventListener('drop', (e) => {
e.preventDefault();
uploadZone.classList.remove('border-blue-500', 'bg-blue-50');
this.handleFiles(e.dataTransfer.files);
});
}
async loadMedia() {
try {
document.getElementById('loading').style.display = 'block';
document.getElementById('empty-state').style.display = 'none';
document.getElementById('media-grid').style.display = 'none';
const response = await fetch('/api/media/list');
const data = await response.json();
if (data.success) {
this.currentFiles = data.images || [];
this.applyFilters();
} else {
throw new Error(data.message || 'Failed to load media');
}
} catch (error) {
console.error('Error loading media:', error);
this.showError('Ошибка загрузки медиа файлов');
} finally {
document.getElementById('loading').style.display = 'none';
}
}
applyFilters() {
const typeFilter = document.getElementById('file-type-filter').value;
const sizeFilter = document.getElementById('size-filter').value;
const searchQuery = document.getElementById('search-input').value.toLowerCase();
this.filteredFiles = this.currentFiles.filter(file => {
// Type filter
if (typeFilter && file.mimetype !== typeFilter) {
return false;
}
// Size filter
if (sizeFilter) {
const sizeInMB = file.size / (1024 * 1024);
if (sizeFilter === 'small' && sizeInMB >= 1) return false;
if (sizeFilter === 'medium' && (sizeInMB < 1 || sizeInMB > 5)) return false;
if (sizeFilter === 'large' && sizeInMB <= 5) return false;
}
// Search filter
if (searchQuery && !file.filename.toLowerCase().includes(searchQuery)) {
return false;
}
return true;
});
this.updateFileCount();
this.renderMedia();
}
updateFileCount() {
const total = this.currentFiles.length;
const filtered = this.filteredFiles.length;
const countText = filtered === total ?
`${total} файлов` :
`${filtered} из ${total} файлов`;
document.getElementById('file-count').textContent = countText;
}
renderMedia() {
if (this.filteredFiles.length === 0) {
document.getElementById('empty-state').style.display = 'block';
document.getElementById('media-grid').style.display = 'none';
document.getElementById('media-list').style.display = 'none';
document.getElementById('pagination').style.display = 'none';
return;
}
document.getElementById('empty-state').style.display = 'none';
const startIndex = (this.currentPage - 1) * this.itemsPerPage;
const endIndex = startIndex + this.itemsPerPage;
const pageFiles = this.filteredFiles.slice(startIndex, endIndex);
if (this.currentView === 'grid') {
this.renderGrid(pageFiles);
} else {
this.renderList(pageFiles);
}
this.updatePagination();
}
renderGrid(files) {
document.getElementById('media-grid').style.display = 'grid';
document.getElementById('media-list').style.display = 'none';
const grid = document.getElementById('media-grid');
grid.innerHTML = files.map(file => `
<div class="group relative bg-gray-100 rounded-lg overflow-hidden cursor-pointer hover:shadow-lg transition-shadow"
onclick="mediaGallery.openModal('${file.filename}')">
<div class="aspect-square">
<img src="${file.url}" alt="${file.filename}"
class="w-full h-full object-cover">
</div>
<div class="absolute inset-0 bg-black bg-opacity-0 group-hover:bg-opacity-25 transition-opacity flex items-center justify-center">
<div class="opacity-0 group-hover:opacity-100 transition-opacity">
<button class="bg-white bg-opacity-90 text-gray-800 px-3 py-2 rounded-lg mr-2"
onclick="event.stopPropagation(); mediaGallery.downloadFile('${file.filename}')">
<i class="fas fa-download"></i>
</button>
<button class="bg-red-500 bg-opacity-90 text-white px-3 py-2 rounded-lg"
onclick="event.stopPropagation(); mediaGallery.deleteFile('${file.filename}')">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
<div class="absolute bottom-0 left-0 right-0 bg-black bg-opacity-75 text-white p-2">
<p class="text-xs truncate">${file.filename}</p>
<p class="text-xs text-gray-300">${this.formatFileSize(file.size)}</p>
</div>
</div>
`).join('');
}
renderList(files) {
document.getElementById('media-grid').style.display = 'none';
document.getElementById('media-list').style.display = 'block';
const list = document.getElementById('media-list');
list.innerHTML = files.map(file => `
<div class="flex items-center p-4 bg-gray-50 rounded-lg hover:bg-gray-100 cursor-pointer"
onclick="mediaGallery.openModal('${file.filename}')">
<div class="w-16 h-16 flex-shrink-0 mr-4">
<img src="${file.url}" alt="${file.filename}"
class="w-full h-full object-cover rounded">
</div>
<div class="flex-1 min-w-0">
<h4 class="text-sm font-medium text-gray-900 truncate">${file.filename}</h4>
<p class="text-sm text-gray-500">${this.formatFileSize(file.size)} • ${file.mimetype}</p>
<p class="text-xs text-gray-400">${new Date(file.uploadedAt).toLocaleDateString('ru-RU')}</p>
</div>
<div class="flex space-x-2">
<button class="text-blue-600 hover:text-blue-800 p-2"
onclick="event.stopPropagation(); mediaGallery.downloadFile('${file.filename}')">
<i class="fas fa-download"></i>
</button>
<button class="text-red-600 hover:text-red-800 p-2"
onclick="event.stopPropagation(); mediaGallery.deleteFile('${file.filename}')">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
`).join('');
}
setView(view) {
this.currentView = view;
// Update button states
document.getElementById('grid-view').classList.toggle('bg-blue-600', view === 'grid');
document.getElementById('grid-view').classList.toggle('text-white', view === 'grid');
document.getElementById('list-view').classList.toggle('bg-blue-600', view === 'list');
document.getElementById('list-view').classList.toggle('text-white', view === 'list');
this.renderMedia();
}
showUploadZone() {
document.getElementById('upload-zone').style.display = 'block';
}
hideUploadZone() {
document.getElementById('upload-zone').style.display = 'none';
document.getElementById('file-input').value = '';
}
async handleFiles(files) {
const validFiles = Array.from(files).filter(file => {
if (!file.type.startsWith('image/')) {
this.showError(`${file.name} не является изображением`);
return false;
}
if (file.size > 10 * 1024 * 1024) {
this.showError(`${file.name} слишком большой (максимум 10MB)`);
return false;
}
return true;
});
if (validFiles.length === 0) return;
this.hideUploadZone();
await this.uploadFiles(validFiles);
}
async uploadFiles(files) {
const progressContainer = document.getElementById('upload-progress');
const progressList = document.getElementById('progress-list');
progressContainer.style.display = 'block';
progressList.innerHTML = '';
for (const file of files) {
const progressItem = this.createProgressItem(file);
progressList.appendChild(progressItem);
try {
await this.uploadSingleFile(file, progressItem);
} catch (error) {
this.updateProgressItem(progressItem, 'error', error.message);
}
}
setTimeout(() => {
progressContainer.style.display = 'none';
this.loadMedia();
}, 2000);
}
createProgressItem(file) {
const div = document.createElement('div');
div.className = 'flex items-center justify-between p-3 bg-gray-50 rounded';
div.innerHTML = `
<div class="flex items-center space-x-3">
<i class="fas fa-image text-gray-400"></i>
<span class="text-sm text-gray-900">${file.name}</span>
<span class="text-xs text-gray-500">${this.formatFileSize(file.size)}</span>
</div>
<div class="flex items-center space-x-3">
<div class="w-32 bg-gray-200 rounded-full h-2">
<div class="bg-blue-600 h-2 rounded-full progress-bar" style="width: 0%"></div>
</div>
<span class="text-sm text-gray-600 status">0%</span>
</div>
`;
return div;
}
updateProgressItem(item, status, message = '') {
const statusElement = item.querySelector('.status');
const progressBar = item.querySelector('.progress-bar');
if (status === 'error') {
statusElement.textContent = 'Ошибка';
statusElement.className = 'text-sm text-red-600 status';
progressBar.className = 'bg-red-600 h-2 rounded-full progress-bar';
progressBar.style.width = '100%';
} else if (status === 'success') {
statusElement.textContent = 'Готово';
statusElement.className = 'text-sm text-green-600 status';
progressBar.className = 'bg-green-600 h-2 rounded-full progress-bar';
progressBar.style.width = '100%';
}
}
async uploadSingleFile(file, progressItem) {
const formData = new FormData();
formData.append('images', file);
const xhr = new XMLHttpRequest();
const progressBar = progressItem.querySelector('.progress-bar');
const status = progressItem.querySelector('.status');
return new Promise((resolve, reject) => {
xhr.upload.addEventListener('progress', (e) => {
if (e.lengthComputable) {
const percentComplete = (e.loaded / e.total) * 100;
progressBar.style.width = percentComplete + '%';
status.textContent = Math.round(percentComplete) + '%';
}
});
xhr.addEventListener('load', () => {
if (xhr.status === 200) {
const response = JSON.parse(xhr.responseText);
if (response.success) {
this.updateProgressItem(progressItem, 'success');
resolve();
} else {
reject(new Error(response.message));
}
} else {
reject(new Error('Upload failed'));
}
});
xhr.addEventListener('error', () => {
reject(new Error('Network error'));
});
xhr.open('POST', '/api/media/upload-multiple');
xhr.send(formData);
});
}
openModal(filename) {
const file = this.currentFiles.find(f => f.filename === filename);
if (!file) return;
this.currentFile = file;
document.getElementById('modal-title').textContent = file.filename;
document.getElementById('modal-image').src = file.url;
document.getElementById('modal-filename').value = file.filename;
document.getElementById('modal-url').value = window.location.origin + file.url;
document.getElementById('modal-size').textContent = this.formatFileSize(file.size);
document.getElementById('modal-type').textContent = file.mimetype;
document.getElementById('modal-date').textContent = new Date(file.uploadedAt).toLocaleDateString('ru-RU');
// Load image to get dimensions
const img = new Image();
img.onload = () => {
document.getElementById('modal-width').textContent = img.width + 'px';
document.getElementById('modal-height').textContent = img.height + 'px';
};
img.src = file.url;
document.getElementById('preview-modal').style.display = 'flex';
}
closeModal() {
document.getElementById('preview-modal').style.display = 'none';
this.currentFile = null;
}
async deleteFile(filename) {
if (!confirm(`Вы уверены, что хотите удалить файл "${filename}"?`)) {
return;
}
try {
const response = await fetch(`/api/media/${filename}`, {
method: 'DELETE'
});
if (response.ok) {
this.showSuccess('Файл удален');
this.loadMedia();
if (this.currentFile && this.currentFile.filename === filename) {
this.closeModal();
}
} else {
throw new Error('Failed to delete file');
}
} catch (error) {
console.error('Error deleting file:', error);
this.showError('Ошибка удаления файла');
}
}
downloadFile(filename) {
const file = filename ?
this.currentFiles.find(f => f.filename === filename) :
this.currentFile;
if (!file) return;
const link = document.createElement('a');
link.href = file.url;
link.download = file.filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
updatePagination() {
const totalPages = Math.ceil(this.filteredFiles.length / this.itemsPerPage);
if (totalPages <= 1) {
document.getElementById('pagination').style.display = 'none';
return;
}
document.getElementById('pagination').style.display = 'block';
// Update prev/next buttons
document.getElementById('prev-page').disabled = this.currentPage === 1;
document.getElementById('next-page').disabled = this.currentPage === totalPages;
// Update page numbers
const pageNumbers = document.getElementById('page-numbers');
pageNumbers.innerHTML = '';
for (let i = 1; i <= totalPages; i++) {
if (i === 1 || i === totalPages || (i >= this.currentPage - 2 && i <= this.currentPage + 2)) {
const button = document.createElement('button');
button.className = `px-3 py-2 rounded ${
i === this.currentPage ? 'bg-blue-600 text-white' : 'bg-gray-200 text-gray-600 hover:bg-gray-300'
}`;
button.textContent = i;
button.onclick = () => this.goToPage(i);
pageNumbers.appendChild(button);
} else if (i === this.currentPage - 3 || i === this.currentPage + 3) {
const span = document.createElement('span');
span.className = 'px-2 py-2 text-gray-400';
span.textContent = '...';
pageNumbers.appendChild(span);
}
}
}
goToPage(page) {
this.currentPage = page;
this.renderMedia();
}
formatFileSize(bytes) {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
showError(message) {
this.showNotification(message, 'error');
}
showSuccess(message) {
this.showNotification(message, 'success');
}
showNotification(message, type = 'info') {
const notification = document.createElement('div');
notification.className = `fixed top-4 right-4 px-6 py-3 rounded-lg text-white z-50 ${
type === 'success' ? 'bg-green-500' :
type === 'error' ? 'bg-red-500' : 'bg-blue-500'
}`;
notification.textContent = message;
document.body.appendChild(notification);
setTimeout(() => {
notification.remove();
}, 3000);
}
}
// Global functions for modal
function copyToClipboard() {
const urlInput = document.getElementById('modal-url');
urlInput.select();
document.execCommand('copy');
mediaGallery.showSuccess('URL скопирован в буфер обмена');
}
function downloadFile() {
mediaGallery.downloadFile();
}
function deleteFile() {
if (mediaGallery.currentFile) {
mediaGallery.deleteFile(mediaGallery.currentFile.filename);
}
}
// Initialize
let mediaGallery;
document.addEventListener('DOMContentLoaded', () => {
mediaGallery = new MediaGallery();
});
</script>
</body>
</html>

View File

@@ -0,0 +1,788 @@
<!-- Content Header (Page header) -->
<section class="content-header">
<div class="container-fluid">
<div class="row mb-2">
<div class="col-sm-6">
<h1><i class="fas fa-images mr-2"></i>Медиа Галерея</h1>
</div>
<div class="col-sm-6">
<div class="float-sm-right">
<button id="refresh-btn" class="btn btn-secondary">
<i class="fas fa-sync-alt mr-1"></i>Обновить
</button>
<button id="upload-btn" class="btn btn-primary">
<i class="fas fa-upload mr-1"></i>Загрузить файлы
</button>
</div>
</div>
</div>
</div>
</section>
<!-- Main content -->
<section class="content">
<div class="container-fluid">
<!-- Upload Zone -->
<div id="upload-zone" class="card" style="display: none;">
<div class="card-body text-center">
<div class="mb-4">
<i class="fas fa-cloud-upload-alt fa-6x text-muted mb-4"></i>
<p class="h5 text-muted mb-2">Перетащите файлы сюда или нажмите для выбора</p>
<p class="text-muted">Поддерживаются: JPG, PNG, GIF, SVG (максимум 10MB каждый)</p>
</div>
<input type="file" id="file-input" multiple accept="image/*" class="d-none">
<button type="button" onclick="document.getElementById('file-input').click()" class="btn btn-primary">
Выбрать файлы
</button>
<button id="cancel-upload" class="btn btn-secondary ml-3">
Отмена
</button>
</div>
</div>
<!-- Upload Progress -->
<div id="upload-progress" class="card" style="display: none;">
<div class="card-header">
<h3 class="card-title">Загрузка файлов</h3>
</div>
<div class="card-body">
<div id="progress-list">
<!-- Progress items will be added here -->
</div>
</div>
</div>
<!-- Filter and Search -->
<div class="card">
<div class="card-body">
<div class="row">
<div class="col-md-3">
<div class="form-group">
<label>Тип файла</label>
<select id="file-type-filter" class="form-control">
<option value="">Все типы</option>
<option value="image/jpeg">JPEG</option>
<option value="image/png">PNG</option>
<option value="image/gif">GIF</option>
<option value="image/svg+xml">SVG</option>
</select>
</div>
</div>
<div class="col-md-3">
<div class="form-group">
<label>Размер</label>
<select id="size-filter" class="form-control">
<option value="">Любой размер</option>
<option value="small">Маленький (&lt; 1MB)</option>
<option value="medium">Средний (1-5MB)</option>
<option value="large">Большой (&gt; 5MB)</option>
</select>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label>Поиск</label>
<div class="input-group">
<input type="text" id="search-input" placeholder="Поиск по имени файла..." class="form-control">
<div class="input-group-append">
<span class="input-group-text"><i class="fas fa-search"></i></span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Media Grid -->
<div class="card">
<div class="card-header">
<h3 class="card-title">Файлы</h3>
<div class="card-tools">
<span id="file-count" class="badge badge-secondary">Загрузка...</span>
<div class="btn-group ml-2">
<button id="grid-view" class="btn btn-sm btn-default">
<i class="fas fa-th-large"></i>
</button>
<button id="list-view" class="btn btn-sm btn-default">
<i class="fas fa-list"></i>
</button>
</div>
</div>
</div>
<div class="card-body">
<!-- Loading State -->
<div id="loading" class="text-center py-5">
<div class="spinner-border text-primary" role="status">
<span class="sr-only">Загрузка...</span>
</div>
<p class="mt-2 text-muted">Загрузка медиа файлов...</p>
</div>
<!-- Empty State -->
<div id="empty-state" class="text-center py-5" style="display: none;">
<i class="fas fa-images fa-6x text-muted mb-4"></i>
<h4 class="text-muted mb-2">Нет загруженных файлов</h4>
<p class="text-muted mb-4">Начните с загрузки ваших первых изображений</p>
<button onclick="document.getElementById('upload-btn').click()" class="btn btn-primary">
<i class="fas fa-upload mr-2"></i>Загрузить файлы
</button>
</div>
<!-- Media Grid -->
<div id="media-grid" class="row">
<!-- Media items will be loaded here -->
</div>
<!-- Media List -->
<div id="media-list" style="display: none;">
<!-- List items will be loaded here -->
</div>
</div>
<!-- Pagination -->
<div class="card-footer" id="pagination" style="display: none;">
<nav aria-label="Page navigation">
<ul class="pagination justify-content-center m-0">
<li class="page-item">
<button id="prev-page" class="page-link">
<i class="fas fa-chevron-left"></i>
</button>
</li>
<div id="page-numbers" class="d-flex">
<!-- Page numbers will be added here -->
</div>
<li class="page-item">
<button id="next-page" class="page-link">
<i class="fas fa-chevron-right"></i>
</button>
</li>
</ul>
</nav>
</div>
</div>
</div>
</section>
<!-- Media Preview Modal -->
<div class="modal fade" id="preview-modal" tabindex="-1" role="dialog">
<div class="modal-dialog modal-xl" role="document">
<div class="modal-content">
<div class="modal-header">
<h4 id="modal-title" class="modal-title">Предпросмотр файла</h4>
<button type="button" class="close" data-dismiss="modal">
<span>&times;</span>
</button>
</div>
<div class="modal-body">
<div class="row">
<div class="col-md-8">
<img id="modal-image" src="" alt="" class="img-fluid rounded">
</div>
<div class="col-md-4">
<div class="form-group">
<label>Имя файла</label>
<input id="modal-filename" type="text" class="form-control" readonly>
</div>
<div class="form-group">
<label>URL</label>
<div class="input-group">
<input id="modal-url" type="text" class="form-control" readonly>
<div class="input-group-append">
<button onclick="copyToClipboard()" class="btn btn-outline-secondary" type="button">
<i class="fas fa-copy"></i>
</button>
</div>
</div>
</div>
<div class="row">
<div class="col-6">
<div class="form-group">
<label>Размер</label>
<p id="modal-size" class="form-control-plaintext">-</p>
</div>
</div>
<div class="col-6">
<div class="form-group">
<label>Тип</label>
<p id="modal-type" class="form-control-plaintext">-</p>
</div>
</div>
</div>
<div class="row">
<div class="col-6">
<div class="form-group">
<label>Ширина</label>
<p id="modal-width" class="form-control-plaintext">-</p>
</div>
</div>
<div class="col-6">
<div class="form-group">
<label>Высота</label>
<p id="modal-height" class="form-control-plaintext">-</p>
</div>
</div>
</div>
<div class="form-group">
<label>Загружено</label>
<p id="modal-date" class="form-control-plaintext">-</p>
</div>
<div class="btn-group d-flex">
<button onclick="downloadFile()" class="btn btn-primary">
<i class="fas fa-download mr-1"></i>Скачать
</button>
<button onclick="deleteFile()" class="btn btn-danger">
<i class="fas fa-trash mr-1"></i>Удалить
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
class MediaGallery {
constructor() {
this.currentFiles = [];
this.filteredFiles = [];
this.currentView = 'grid';
this.currentPage = 1;
this.itemsPerPage = 24;
this.currentFile = null;
this.init();
}
init() {
this.setupEventListeners();
this.loadMedia();
}
setupEventListeners() {
// Upload button
document.getElementById('upload-btn').addEventListener('click', () => {
this.showUploadZone();
});
// Cancel upload
document.getElementById('cancel-upload').addEventListener('click', () => {
this.hideUploadZone();
});
// File input
document.getElementById('file-input').addEventListener('change', (e) => {
this.handleFiles(e.target.files);
});
// Refresh button
document.getElementById('refresh-btn').addEventListener('click', () => {
this.loadMedia();
});
// View toggle
document.getElementById('grid-view').addEventListener('click', () => {
this.setView('grid');
});
document.getElementById('list-view').addEventListener('click', () => {
this.setView('list');
});
// Filters
document.getElementById('file-type-filter').addEventListener('change', () => {
this.applyFilters();
});
document.getElementById('size-filter').addEventListener('change', () => {
this.applyFilters();
});
document.getElementById('search-input').addEventListener('input', () => {
this.applyFilters();
});
// Modal
document.getElementById('close-modal').addEventListener('click', () => {
this.closeModal();
});
// Upload zone drag and drop
const uploadZone = document.getElementById('upload-zone');
uploadZone.addEventListener('dragover', (e) => {
e.preventDefault();
uploadZone.classList.add('border-blue-500', 'bg-blue-50');
});
uploadZone.addEventListener('dragleave', () => {
uploadZone.classList.remove('border-blue-500', 'bg-blue-50');
});
uploadZone.addEventListener('drop', (e) => {
e.preventDefault();
uploadZone.classList.remove('border-blue-500', 'bg-blue-50');
this.handleFiles(e.dataTransfer.files);
});
}
async loadMedia() {
try {
document.getElementById('loading').style.display = 'block';
document.getElementById('empty-state').style.display = 'none';
document.getElementById('media-grid').style.display = 'none';
const response = await fetch('/api/media/list');
const data = await response.json();
if (data.success) {
this.currentFiles = data.images || [];
this.applyFilters();
} else {
throw new Error(data.message || 'Failed to load media');
}
} catch (error) {
console.error('Error loading media:', error);
this.showError('Ошибка загрузки медиа файлов');
} finally {
document.getElementById('loading').style.display = 'none';
}
}
applyFilters() {
const typeFilter = document.getElementById('file-type-filter').value;
const sizeFilter = document.getElementById('size-filter').value;
const searchQuery = document.getElementById('search-input').value.toLowerCase();
this.filteredFiles = this.currentFiles.filter(file => {
// Type filter
if (typeFilter && file.mimetype !== typeFilter) {
return false;
}
// Size filter
if (sizeFilter) {
const sizeInMB = file.size / (1024 * 1024);
if (sizeFilter === 'small' && sizeInMB >= 1) return false;
if (sizeFilter === 'medium' && (sizeInMB < 1 || sizeInMB > 5)) return false;
if (sizeFilter === 'large' && sizeInMB <= 5) return false;
}
// Search filter
if (searchQuery && !file.filename.toLowerCase().includes(searchQuery)) {
return false;
}
return true;
});
this.updateFileCount();
this.renderMedia();
}
updateFileCount() {
const total = this.currentFiles.length;
const filtered = this.filteredFiles.length;
const countText = filtered === total ?
`${total} файлов` :
`${filtered} из ${total} файлов`;
document.getElementById('file-count').textContent = countText;
}
renderMedia() {
if (this.filteredFiles.length === 0) {
document.getElementById('empty-state').style.display = 'block';
document.getElementById('media-grid').style.display = 'none';
document.getElementById('media-list').style.display = 'none';
document.getElementById('pagination').style.display = 'none';
return;
}
document.getElementById('empty-state').style.display = 'none';
const startIndex = (this.currentPage - 1) * this.itemsPerPage;
const endIndex = startIndex + this.itemsPerPage;
const pageFiles = this.filteredFiles.slice(startIndex, endIndex);
if (this.currentView === 'grid') {
this.renderGrid(pageFiles);
} else {
this.renderList(pageFiles);
}
this.updatePagination();
}
renderGrid(files) {
document.getElementById('media-grid').style.display = 'grid';
document.getElementById('media-list').style.display = 'none';
const grid = document.getElementById('media-grid');
grid.innerHTML = files.map(file => `
<div class="group relative bg-gray-100 rounded-lg overflow-hidden cursor-pointer hover:shadow-lg transition-shadow"
onclick="mediaGallery.openModal('${file.filename}')">
<div class="aspect-square">
<img src="${file.url}" alt="${file.filename}"
class="w-full h-full object-cover">
</div>
<div class="absolute inset-0 bg-black bg-opacity-0 group-hover:bg-opacity-25 transition-opacity flex items-center justify-center">
<div class="opacity-0 group-hover:opacity-100 transition-opacity">
<button class="bg-white bg-opacity-90 text-gray-800 px-3 py-2 rounded-lg mr-2"
onclick="event.stopPropagation(); mediaGallery.downloadFile('${file.filename}')">
<i class="fas fa-download"></i>
</button>
<button class="bg-red-500 bg-opacity-90 text-white px-3 py-2 rounded-lg"
onclick="event.stopPropagation(); mediaGallery.deleteFile('${file.filename}')">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
<div class="absolute bottom-0 left-0 right-0 bg-black bg-opacity-75 text-white p-2">
<p class="text-xs truncate">${file.filename}</p>
<p class="text-xs text-gray-300">${this.formatFileSize(file.size)}</p>
</div>
</div>
`).join('');
}
renderList(files) {
document.getElementById('media-grid').style.display = 'none';
document.getElementById('media-list').style.display = 'block';
const list = document.getElementById('media-list');
list.innerHTML = files.map(file => `
<div class="flex items-center p-4 bg-gray-50 rounded-lg hover:bg-gray-100 cursor-pointer"
onclick="mediaGallery.openModal('${file.filename}')">
<div class="w-16 h-16 flex-shrink-0 mr-4">
<img src="${file.url}" alt="${file.filename}"
class="w-full h-full object-cover rounded">
</div>
<div class="flex-1 min-w-0">
<h4 class="text-sm font-medium text-gray-900 truncate">${file.filename}</h4>
<p class="text-sm text-gray-500">${this.formatFileSize(file.size)} • ${file.mimetype}</p>
<p class="text-xs text-gray-400">${new Date(file.uploadedAt).toLocaleDateString('ru-RU')}</p>
</div>
<div class="flex space-x-2">
<button class="text-blue-600 hover:text-blue-800 p-2"
onclick="event.stopPropagation(); mediaGallery.downloadFile('${file.filename}')">
<i class="fas fa-download"></i>
</button>
<button class="text-red-600 hover:text-red-800 p-2"
onclick="event.stopPropagation(); mediaGallery.deleteFile('${file.filename}')">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
`).join('');
}
setView(view) {
this.currentView = view;
// Update button states
document.getElementById('grid-view').classList.toggle('bg-blue-600', view === 'grid');
document.getElementById('grid-view').classList.toggle('text-white', view === 'grid');
document.getElementById('list-view').classList.toggle('bg-blue-600', view === 'list');
document.getElementById('list-view').classList.toggle('text-white', view === 'list');
this.renderMedia();
}
showUploadZone() {
document.getElementById('upload-zone').style.display = 'block';
}
hideUploadZone() {
document.getElementById('upload-zone').style.display = 'none';
document.getElementById('file-input').value = '';
}
async handleFiles(files) {
const validFiles = Array.from(files).filter(file => {
if (!file.type.startsWith('image/')) {
this.showError(`${file.name} не является изображением`);
return false;
}
if (file.size > 10 * 1024 * 1024) {
this.showError(`${file.name} слишком большой (максимум 10MB)`);
return false;
}
return true;
});
if (validFiles.length === 0) return;
this.hideUploadZone();
await this.uploadFiles(validFiles);
}
async uploadFiles(files) {
const progressContainer = document.getElementById('upload-progress');
const progressList = document.getElementById('progress-list');
progressContainer.style.display = 'block';
progressList.innerHTML = '';
for (const file of files) {
const progressItem = this.createProgressItem(file);
progressList.appendChild(progressItem);
try {
await this.uploadSingleFile(file, progressItem);
} catch (error) {
this.updateProgressItem(progressItem, 'error', error.message);
}
}
setTimeout(() => {
progressContainer.style.display = 'none';
this.loadMedia();
}, 2000);
}
createProgressItem(file) {
const div = document.createElement('div');
div.className = 'flex items-center justify-between p-3 bg-gray-50 rounded';
div.innerHTML = `
<div class="flex items-center space-x-3">
<i class="fas fa-image text-gray-400"></i>
<span class="text-sm text-gray-900">${file.name}</span>
<span class="text-xs text-gray-500">${this.formatFileSize(file.size)}</span>
</div>
<div class="flex items-center space-x-3">
<div class="w-32 bg-gray-200 rounded-full h-2">
<div class="bg-blue-600 h-2 rounded-full progress-bar" style="width: 0%"></div>
</div>
<span class="text-sm text-gray-600 status">0%</span>
</div>
`;
return div;
}
updateProgressItem(item, status, message = '') {
const statusElement = item.querySelector('.status');
const progressBar = item.querySelector('.progress-bar');
if (status === 'error') {
statusElement.textContent = 'Ошибка';
statusElement.className = 'text-sm text-red-600 status';
progressBar.className = 'bg-red-600 h-2 rounded-full progress-bar';
progressBar.style.width = '100%';
} else if (status === 'success') {
statusElement.textContent = 'Готово';
statusElement.className = 'text-sm text-green-600 status';
progressBar.className = 'bg-green-600 h-2 rounded-full progress-bar';
progressBar.style.width = '100%';
}
}
async uploadSingleFile(file, progressItem) {
const formData = new FormData();
formData.append('images', file);
const xhr = new XMLHttpRequest();
const progressBar = progressItem.querySelector('.progress-bar');
const status = progressItem.querySelector('.status');
return new Promise((resolve, reject) => {
xhr.upload.addEventListener('progress', (e) => {
if (e.lengthComputable) {
const percentComplete = (e.loaded / e.total) * 100;
progressBar.style.width = percentComplete + '%';
status.textContent = Math.round(percentComplete) + '%';
}
});
xhr.addEventListener('load', () => {
if (xhr.status === 200) {
const response = JSON.parse(xhr.responseText);
if (response.success) {
this.updateProgressItem(progressItem, 'success');
resolve();
} else {
reject(new Error(response.message));
}
} else {
reject(new Error('Upload failed'));
}
});
xhr.addEventListener('error', () => {
reject(new Error('Network error'));
});
xhr.open('POST', '/api/media/upload-multiple');
xhr.send(formData);
});
}
openModal(filename) {
const file = this.currentFiles.find(f => f.filename === filename);
if (!file) return;
this.currentFile = file;
document.getElementById('modal-title').textContent = file.filename;
document.getElementById('modal-image').src = file.url;
document.getElementById('modal-filename').value = file.filename;
document.getElementById('modal-url').value = window.location.origin + file.url;
document.getElementById('modal-size').textContent = this.formatFileSize(file.size);
document.getElementById('modal-type').textContent = file.mimetype;
document.getElementById('modal-date').textContent = new Date(file.uploadedAt).toLocaleDateString('ru-RU');
// Load image to get dimensions
const img = new Image();
img.onload = () => {
document.getElementById('modal-width').textContent = img.width + 'px';
document.getElementById('modal-height').textContent = img.height + 'px';
};
img.src = file.url;
document.getElementById('preview-modal').style.display = 'flex';
}
closeModal() {
document.getElementById('preview-modal').style.display = 'none';
this.currentFile = null;
}
async deleteFile(filename) {
if (!confirm(`Вы уверены, что хотите удалить файл "${filename}"?`)) {
return;
}
try {
const response = await fetch(`/api/media/${filename}`, {
method: 'DELETE'
});
if (response.ok) {
this.showSuccess('Файл удален');
this.loadMedia();
if (this.currentFile && this.currentFile.filename === filename) {
this.closeModal();
}
} else {
throw new Error('Failed to delete file');
}
} catch (error) {
console.error('Error deleting file:', error);
this.showError('Ошибка удаления файла');
}
}
downloadFile(filename) {
const file = filename ?
this.currentFiles.find(f => f.filename === filename) :
this.currentFile;
if (!file) return;
const link = document.createElement('a');
link.href = file.url;
link.download = file.filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
updatePagination() {
const totalPages = Math.ceil(this.filteredFiles.length / this.itemsPerPage);
if (totalPages <= 1) {
document.getElementById('pagination').style.display = 'none';
return;
}
document.getElementById('pagination').style.display = 'block';
// Update prev/next buttons
document.getElementById('prev-page').disabled = this.currentPage === 1;
document.getElementById('next-page').disabled = this.currentPage === totalPages;
// Update page numbers
const pageNumbers = document.getElementById('page-numbers');
pageNumbers.innerHTML = '';
for (let i = 1; i <= totalPages; i++) {
if (i === 1 || i === totalPages || (i >= this.currentPage - 2 && i <= this.currentPage + 2)) {
const button = document.createElement('button');
button.className = `px-3 py-2 rounded ${
i === this.currentPage ? 'bg-blue-600 text-white' : 'bg-gray-200 text-gray-600 hover:bg-gray-300'
}`;
button.textContent = i;
button.onclick = () => this.goToPage(i);
pageNumbers.appendChild(button);
} else if (i === this.currentPage - 3 || i === this.currentPage + 3) {
const span = document.createElement('span');
span.className = 'px-2 py-2 text-gray-400';
span.textContent = '...';
pageNumbers.appendChild(span);
}
}
}
goToPage(page) {
this.currentPage = page;
this.renderMedia();
}
formatFileSize(bytes) {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
showError(message) {
this.showNotification(message, 'error');
}
showSuccess(message) {
this.showNotification(message, 'success');
}
showNotification(message, type = 'info') {
const notification = document.createElement('div');
notification.className = `fixed top-4 right-4 px-6 py-3 rounded-lg text-white z-50 ${
type === 'success' ? 'bg-green-500' :
type === 'error' ? 'bg-red-500' : 'bg-blue-500'
}`;
notification.textContent = message;
document.body.appendChild(notification);
setTimeout(() => {
notification.remove();
}, 3000);
}
}
// Global functions for modal
function copyToClipboard() {
const urlInput = document.getElementById('modal-url');
urlInput.select();
document.execCommand('copy');
mediaGallery.showSuccess('URL скопирован в буфер обмена');
}
function downloadFile() {
mediaGallery.downloadFile();
}
function deleteFile() {
if (mediaGallery.currentFile) {
mediaGallery.deleteFile(mediaGallery.currentFile.filename);
}
}
// Initialize
let mediaGallery;
document.addEventListener('DOMContentLoaded', () => {
mediaGallery = new MediaGallery();
});
</script>
</body>
</html>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

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>

View File

@@ -0,0 +1,344 @@
<!-- 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-plus 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"><a href="/admin/services">Услуги</a></li>
<li class="breadcrumb-item active">Добавить</li>
</ol>
</div>
</div>
</div>
</div>
<!-- Main content -->
<section class="content">
<div class="container-fluid">
<form id="serviceForm" action="/api/admin/services" method="POST">
<div class="row">
<div class="col-md-8">
<!-- Basic Information -->
<div class="card card-primary">
<div class="card-header">
<h3 class="card-title">Основная информация</h3>
</div>
<div class="card-body">
<div class="form-group">
<label for="name">Название услуги *</label>
<input type="text" class="form-control" id="name" name="name" required>
</div>
<div class="form-group">
<label for="shortDescription">Краткое описание</label>
<textarea class="form-control" id="shortDescription" name="shortDescription" rows="2" placeholder="Краткое описание для списков и карточек"></textarea>
</div>
<div class="form-group">
<label for="description">Полное описание</label>
<textarea class="form-control" id="description" name="description" rows="6" placeholder="Детальное описание услуги"></textarea>
</div>
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label for="category">Категория *</label>
<select class="form-control" id="category" name="category" required>
<option value="">Выберите категорию</option>
<option value="web-development">Веб-разработка</option>
<option value="mobile-development">Мобильная разработка</option>
<option value="ui-ux-design">UI/UX Дизайн</option>
<option value="consulting">Консалтинг</option>
<option value="support">Поддержка</option>
<option value="other">Другое</option>
</select>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="icon">Иконка</label>
<input type="text" class="form-control" id="icon" name="icon" placeholder="fas fa-code" value="fas fa-cog">
<small class="form-text text-muted">FontAwesome класс иконки</small>
</div>
</div>
</div>
<div class="form-group">
<label for="estimatedTime">Время выполнения</label>
<input type="text" class="form-control" id="estimatedTime" name="estimatedTime" placeholder="2-4 недели">
</div>
</div>
</div>
<!-- Pricing -->
<div class="card card-info">
<div class="card-header">
<h3 class="card-title">Ценообразование</h3>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label for="basePrice">Базовая цена ($)</label>
<input type="number" class="form-control" id="basePrice" name="pricing[basePrice]" min="0" step="0.01">
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="currency">Валюта</label>
<select class="form-control" id="currency" name="pricing[currency]">
<option value="USD">USD ($)</option>
<option value="EUR">EUR (€)</option>
<option value="KRW">KRW (₩)</option>
</select>
</div>
</div>
</div>
<div class="form-group">
<label for="pricingType">Тип ценообразования</label>
<select class="form-control" id="pricingType" name="pricing[type]">
<option value="fixed">Фиксированная цена</option>
<option value="hourly">Почасовая оплата</option>
<option value="project">За проект</option>
<option value="subscription">Подписка</option>
</select>
</div>
</div>
</div>
<!-- Features -->
<div class="card card-success">
<div class="card-header">
<h3 class="card-title">Функции и возможности</h3>
</div>
<div class="card-body">
<div id="featuresContainer">
<div class="feature-item mb-3">
<div class="row">
<div class="col-md-8">
<input type="text" class="form-control" name="features[0][name]" placeholder="Название функции">
</div>
<div class="col-md-3">
<select class="form-control" name="features[0][included]">
<option value="true">Включено</option>
<option value="false">Не включено</option>
</select>
</div>
<div class="col-md-1">
<button type="button" class="btn btn-danger btn-sm remove-feature">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
</div>
</div>
<button type="button" class="btn btn-sm btn-outline-primary" id="addFeature">
<i class="fas fa-plus mr-1"></i> Добавить функцию
</button>
</div>
</div>
</div>
<div class="col-md-4">
<!-- Status & Settings -->
<div class="card card-warning">
<div class="card-header">
<h3 class="card-title">Настройки</h3>
</div>
<div class="card-body">
<div class="form-group">
<div class="custom-control custom-switch">
<input type="checkbox" class="custom-control-input" id="isActive" name="isActive" checked>
<label class="custom-control-label" for="isActive">Активная услуга</label>
</div>
</div>
<div class="form-group">
<div class="custom-control custom-switch">
<input type="checkbox" class="custom-control-input" id="featured" name="featured">
<label class="custom-control-label" for="featured">Рекомендуемая услуга</label>
</div>
</div>
<div class="form-group">
<label for="order">Порядок отображения</label>
<input type="number" class="form-control" id="order" name="order" value="0" min="0">
</div>
</div>
</div>
<!-- Tags -->
<div class="card card-secondary">
<div class="card-header">
<h3 class="card-title">Теги</h3>
</div>
<div class="card-body">
<div class="form-group">
<label for="tags">Теги (через запятую)</label>
<input type="text" class="form-control" id="tags" name="tags" placeholder="веб-дизайн, frontend, react">
<small class="form-text text-muted">Разделите теги запятыми</small>
</div>
</div>
</div>
<!-- SEO -->
<div class="card card-dark">
<div class="card-header">
<h3 class="card-title">SEO настройки</h3>
</div>
<div class="card-body">
<div class="form-group">
<label for="seoTitle">SEO заголовок</label>
<input type="text" class="form-control" id="seoTitle" name="seo[title]">
</div>
<div class="form-group">
<label for="seoDescription">SEO описание</label>
<textarea class="form-control" id="seoDescription" name="seo[description]" rows="3"></textarea>
</div>
<div class="form-group">
<label for="seoKeywords">Ключевые слова</label>
<input type="text" class="form-control" id="seoKeywords" name="seo[keywords]" placeholder="через, запятую">
</div>
</div>
</div>
</div>
</div>
<!-- Submit Buttons -->
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-body">
<button type="submit" class="btn btn-success">
<i class="fas fa-save mr-1"></i> Сохранить услугу
</button>
<a href="/admin/services" class="btn btn-secondary ml-2">
<i class="fas fa-times mr-1"></i> Отмена
</a>
</div>
</div>
</div>
</div>
</form>
</div>
</section>
<script>
$(document).ready(function() {
let featureIndex = 1;
// Add feature
$('#addFeature').click(function() {
const featureHtml = `
<div class="feature-item mb-3">
<div class="row">
<div class="col-md-8">
<input type="text" class="form-control" name="features[${featureIndex}][name]" placeholder="Название функции">
</div>
<div class="col-md-3">
<select class="form-control" name="features[${featureIndex}][included]">
<option value="true">Включено</option>
<option value="false">Не включено</option>
</select>
</div>
<div class="col-md-1">
<button type="button" class="btn btn-danger btn-sm remove-feature">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
</div>
`;
$('#featuresContainer').append(featureHtml);
featureIndex++;
});
// Remove feature
$(document).on('click', '.remove-feature', function() {
$(this).closest('.feature-item').remove();
});
// Form submission
$('#serviceForm').submit(function(e) {
e.preventDefault();
const formData = new FormData(this);
const data = {};
// Convert FormData to object
for (let [key, value] of formData.entries()) {
if (key.includes('[') && key.includes(']')) {
// Handle nested objects (pricing, seo, features)
const matches = key.match(/(\w+)\[(\w+|\d+)\](?:\[(\w+)\])?/);
if (matches) {
const [, parent, child, grandchild] = matches;
if (!data[parent]) data[parent] = {};
if (grandchild) {
// features array handling
if (!data[parent][child]) data[parent][child] = {};
data[parent][child][grandchild] = value;
} else {
data[parent][child] = value;
}
}
} else {
data[key] = value;
}
}
// Convert features object to array
if (data.features) {
const featuresArray = [];
Object.keys(data.features).forEach(index => {
if (data.features[index].name) {
featuresArray.push({
name: data.features[index].name,
included: data.features[index].included === 'true'
});
}
});
data.features = featuresArray;
}
// Convert checkboxes
data.isActive = $('#isActive').is(':checked');
data.featured = $('#featured').is(':checked');
// Convert tags to array
if (data.tags) {
data.tags = data.tags.split(',').map(tag => tag.trim()).filter(tag => tag);
}
fetch('/api/admin/services', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data)
})
.then(response => response.json())
.then(result => {
if (result.success) {
toastr.success('Услуга успешно создана!');
setTimeout(() => {
window.location.href = '/admin/services';
}, 1500);
} else {
toastr.error(result.message || 'Ошибка при создании услуги');
}
})
.catch(error => {
console.error('Error:', error);
toastr.error('Произошла ошибка при создании услуги');
});
});
});
</script>

View File

@@ -0,0 +1,344 @@
<!-- 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-plus 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"><a href="/admin/services">Услуги</a></li>
<li class="breadcrumb-item active">Добавить</li>
</ol>
</div>
</div>
</div>
</div>
<!-- Main content -->
<section class="content">
<div class="container-fluid">
<form id="serviceForm" action="/api/admin/services" method="POST">
<div class="row">
<div class="col-md-8">
<!-- Basic Information -->
<div class="card card-primary">
<div class="card-header">
<h3 class="card-title">Основная информация</h3>
</div>
<div class="card-body">
<div class="form-group">
<label for="name">Название услуги *</label>
<input type="text" class="form-control" id="name" name="name" required>
</div>
<div class="form-group">
<label for="shortDescription">Краткое описание</label>
<textarea class="form-control" id="shortDescription" name="shortDescription" rows="2" placeholder="Краткое описание для списков и карточек"></textarea>
</div>
<div class="form-group">
<label for="description">Полное описание</label>
<textarea class="form-control" id="description" name="description" rows="6" placeholder="Детальное описание услуги"></textarea>
</div>
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label for="category">Категория *</label>
<select class="form-control" id="category" name="category" required>
<option value="">Выберите категорию</option>
<option value="web-development">Веб-разработка</option>
<option value="mobile-development">Мобильная разработка</option>
<option value="ui-ux-design">UI/UX Дизайн</option>
<option value="consulting">Консалтинг</option>
<option value="support">Поддержка</option>
<option value="other">Другое</option>
</select>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="icon">Иконка</label>
<input type="text" class="form-control" id="icon" name="icon" placeholder="fas fa-code" value="fas fa-cog">
<small class="form-text text-muted">FontAwesome класс иконки</small>
</div>
</div>
</div>
<div class="form-group">
<label for="estimatedTime">Время выполнения</label>
<input type="text" class="form-control" id="estimatedTime" name="estimatedTime" placeholder="2-4 недели">
</div>
</div>
</div>
<!-- Pricing -->
<div class="card card-info">
<div class="card-header">
<h3 class="card-title">Ценообразование</h3>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label for="basePrice">Базовая цена ($)</label>
<input type="number" class="form-control" id="basePrice" name="pricing[basePrice]" min="0" step="0.01">
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="currency">Валюта</label>
<select class="form-control" id="currency" name="pricing[currency]">
<option value="USD">USD ($)</option>
<option value="EUR">EUR (€)</option>
<option value="KRW">KRW (₩)</option>
</select>
</div>
</div>
</div>
<div class="form-group">
<label for="pricingType">Тип ценообразования</label>
<select class="form-control" id="pricingType" name="pricing[type]">
<option value="fixed">Фиксированная цена</option>
<option value="hourly">Почасовая оплата</option>
<option value="project">За проект</option>
<option value="subscription">Подписка</option>
</select>
</div>
</div>
</div>
<!-- Features -->
<div class="card card-success">
<div class="card-header">
<h3 class="card-title">Функции и возможности</h3>
</div>
<div class="card-body">
<div id="featuresContainer">
<div class="feature-item mb-3">
<div class="row">
<div class="col-md-8">
<input type="text" class="form-control" name="features[0][name]" placeholder="Название функции">
</div>
<div class="col-md-3">
<select class="form-control" name="features[0][included]">
<option value="true">Включено</option>
<option value="false">Не включено</option>
</select>
</div>
<div class="col-md-1">
<button type="button" class="btn btn-danger btn-sm remove-feature">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
</div>
</div>
<button type="button" class="btn btn-sm btn-outline-primary" id="addFeature">
<i class="fas fa-plus mr-1"></i> Добавить функцию
</button>
</div>
</div>
</div>
<div class="col-md-4">
<!-- Status & Settings -->
<div class="card card-warning">
<div class="card-header">
<h3 class="card-title">Настройки</h3>
</div>
<div class="card-body">
<div class="form-group">
<div class="custom-control custom-switch">
<input type="checkbox" class="custom-control-input" id="isActive" name="isActive" checked>
<label class="custom-control-label" for="isActive">Активная услуга</label>
</div>
</div>
<div class="form-group">
<div class="custom-control custom-switch">
<input type="checkbox" class="custom-control-input" id="featured" name="featured">
<label class="custom-control-label" for="featured">Рекомендуемая услуга</label>
</div>
</div>
<div class="form-group">
<label for="order">Порядок отображения</label>
<input type="number" class="form-control" id="order" name="order" value="0" min="0">
</div>
</div>
</div>
<!-- Tags -->
<div class="card card-secondary">
<div class="card-header">
<h3 class="card-title">Теги</h3>
</div>
<div class="card-body">
<div class="form-group">
<label for="tags">Теги (через запятую)</label>
<input type="text" class="form-control" id="tags" name="tags" placeholder="веб-дизайн, frontend, react">
<small class="form-text text-muted">Разделите теги запятыми</small>
</div>
</div>
</div>
<!-- SEO -->
<div class="card card-dark">
<div class="card-header">
<h3 class="card-title">SEO настройки</h3>
</div>
<div class="card-body">
<div class="form-group">
<label for="seoTitle">SEO заголовок</label>
<input type="text" class="form-control" id="seoTitle" name="seo[title]">
</div>
<div class="form-group">
<label for="seoDescription">SEO описание</label>
<textarea class="form-control" id="seoDescription" name="seo[description]" rows="3"></textarea>
</div>
<div class="form-group">
<label for="seoKeywords">Ключевые слова</label>
<input type="text" class="form-control" id="seoKeywords" name="seo[keywords]" placeholder="через, запятую">
</div>
</div>
</div>
</div>
</div>
<!-- Submit Buttons -->
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-body">
<button type="submit" class="btn btn-success">
<i class="fas fa-save mr-1"></i> Сохранить услугу
</button>
<a href="/admin/services" class="btn btn-secondary ml-2">
<i class="fas fa-times mr-1"></i> Отмена
</a>
</div>
</div>
</div>
</div>
</form>
</div>
</section>
<script>
$(document).ready(function() {
let featureIndex = 1;
// Add feature
$('#addFeature').click(function() {
const featureHtml = `
<div class="feature-item mb-3">
<div class="row">
<div class="col-md-8">
<input type="text" class="form-control" name="features[${featureIndex}][name]" placeholder="Название функции">
</div>
<div class="col-md-3">
<select class="form-control" name="features[${featureIndex}][included]">
<option value="true">Включено</option>
<option value="false">Не включено</option>
</select>
</div>
<div class="col-md-1">
<button type="button" class="btn btn-danger btn-sm remove-feature">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
</div>
`;
$('#featuresContainer').append(featureHtml);
featureIndex++;
});
// Remove feature
$(document).on('click', '.remove-feature', function() {
$(this).closest('.feature-item').remove();
});
// Form submission
$('#serviceForm').submit(function(e) {
e.preventDefault();
const formData = new FormData(this);
const data = {};
// Convert FormData to object
for (let [key, value] of formData.entries()) {
if (key.includes('[') && key.includes(']')) {
// Handle nested objects (pricing, seo, features)
const matches = key.match(/(\w+)\[(\w+|\d+)\](?:\[(\w+)\])?/);
if (matches) {
const [, parent, child, grandchild] = matches;
if (!data[parent]) data[parent] = {};
if (grandchild) {
// features array handling
if (!data[parent][child]) data[parent][child] = {};
data[parent][child][grandchild] = value;
} else {
data[parent][child] = value;
}
}
} else {
data[key] = value;
}
}
// Convert features object to array
if (data.features) {
const featuresArray = [];
Object.keys(data.features).forEach(index => {
if (data.features[index].name) {
featuresArray.push({
name: data.features[index].name,
included: data.features[index].included === 'true'
});
}
});
data.features = featuresArray;
}
// Convert checkboxes
data.isActive = $('#isActive').is(':checked');
data.featured = $('#featured').is(':checked');
// Convert tags to array
if (data.tags) {
data.tags = data.tags.split(',').map(tag => tag.trim()).filter(tag => tag);
}
fetch('/api/admin/services', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data)
})
.then(response => response.json())
.then(result => {
if (result.success) {
toastr.success('Услуга успешно создана!');
setTimeout(() => {
window.location.href = '/admin/services';
}, 1500);
} else {
toastr.error(result.message || 'Ошибка при создании услуги');
}
})
.catch(error => {
console.error('Error:', error);
toastr.error('Произошла ошибка при создании услуги');
});
});
});
</script>

View File

@@ -0,0 +1,410 @@
<!-- 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-edit 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"><a href="/admin/services">Услуги</a></li>
<li class="breadcrumb-item active">Редактировать</li>
</ol>
</div>
</div>
</div>
</div>
<!-- Main content -->
<section class="content">
<div class="container-fluid">
<form id="serviceForm">
<input type="hidden" id="serviceId" value="<%= service.id %>">
<div class="row">
<div class="col-md-8">
<!-- Basic Information -->
<div class="card card-primary">
<div class="card-header">
<h3 class="card-title">Основная информация</h3>
</div>
<div class="card-body">
<div class="form-group">
<label for="name">Название услуги *</label>
<input type="text" class="form-control" id="name" name="name" value="<%= service.name %>" required>
</div>
<div class="form-group">
<label for="shortDescription">Краткое описание</label>
<textarea class="form-control" id="shortDescription" name="shortDescription" rows="2"><%= service.shortDescription || '' %></textarea>
</div>
<div class="form-group">
<label for="description">Полное описание</label>
<textarea class="form-control" id="description" name="description" rows="6"><%= service.description || '' %></textarea>
</div>
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label for="category">Категория *</label>
<select class="form-control" id="category" name="category" required>
<option value="">Выберите категорию</option>
<option value="web-development" <%= service.category === 'web-development' ? 'selected' : '' %>>Веб-разработка</option>
<option value="mobile-development" <%= service.category === 'mobile-development' ? 'selected' : '' %>>Мобильная разработка</option>
<option value="ui-ux-design" <%= service.category === 'ui-ux-design' ? 'selected' : '' %>>UI/UX Дизайн</option>
<option value="consulting" <%= service.category === 'consulting' ? 'selected' : '' %>>Консалтинг</option>
<option value="support" <%= service.category === 'support' ? 'selected' : '' %>>Поддержка</option>
<option value="other" <%= service.category === 'other' ? 'selected' : '' %>>Другое</option>
</select>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="icon">Иконка</label>
<input type="text" class="form-control" id="icon" name="icon" value="<%= service.icon || 'fas fa-cog' %>">
<small class="form-text text-muted">FontAwesome класс иконки</small>
</div>
</div>
</div>
<div class="form-group">
<label for="estimatedTime">Время выполнения</label>
<input type="text" class="form-control" id="estimatedTime" name="estimatedTime" value="<%= service.estimatedTime || '' %>">
</div>
</div>
</div>
<!-- Pricing -->
<div class="card card-info">
<div class="card-header">
<h3 class="card-title">Ценообразование</h3>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label for="basePrice">Базовая цена ($)</label>
<input type="number" class="form-control" id="basePrice" name="pricing[basePrice]" value="<%= service.pricing?.basePrice || '' %>" min="0" step="0.01">
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="currency">Валюта</label>
<select class="form-control" id="currency" name="pricing[currency]">
<option value="USD" <%= service.pricing?.currency === 'USD' ? 'selected' : '' %>>USD ($)</option>
<option value="EUR" <%= service.pricing?.currency === 'EUR' ? 'selected' : '' %>>EUR (€)</option>
<option value="KRW" <%= service.pricing?.currency === 'KRW' ? 'selected' : '' %>>KRW (₩)</option>
</select>
</div>
</div>
</div>
<div class="form-group">
<label for="pricingType">Тип ценообразования</label>
<select class="form-control" id="pricingType" name="pricing[type]">
<option value="fixed" <%= service.pricing?.type === 'fixed' ? 'selected' : '' %>>Фиксированная цена</option>
<option value="hourly" <%= service.pricing?.type === 'hourly' ? 'selected' : '' %>>Почасовая оплата</option>
<option value="project" <%= service.pricing?.type === 'project' ? 'selected' : '' %>>За проект</option>
<option value="subscription" <%= service.pricing?.type === 'subscription' ? 'selected' : '' %>>Подписка</option>
</select>
</div>
</div>
</div>
<!-- Features -->
<div class="card card-success">
<div class="card-header">
<h3 class="card-title">Функции и возможности</h3>
</div>
<div class="card-body">
<div id="featuresContainer">
<% if (service.features && service.features.length > 0) { %>
<% service.features.forEach((feature, index) => { %>
<div class="feature-item mb-3">
<div class="row">
<div class="col-md-8">
<input type="text" class="form-control" name="features[<%= index %>][name]" value="<%= feature.name %>">
</div>
<div class="col-md-3">
<select class="form-control" name="features[<%= index %>][included]">
<option value="true" <%= feature.included ? 'selected' : '' %>>Включено</option>
<option value="false" <%= !feature.included ? 'selected' : '' %>>Не включено</option>
</select>
</div>
<div class="col-md-1">
<button type="button" class="btn btn-danger btn-sm remove-feature">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
</div>
<% }) %>
<% } else { %>
<div class="feature-item mb-3">
<div class="row">
<div class="col-md-8">
<input type="text" class="form-control" name="features[0][name]" placeholder="Название функции">
</div>
<div class="col-md-3">
<select class="form-control" name="features[0][included]">
<option value="true">Включено</option>
<option value="false">Не включено</option>
</select>
</div>
<div class="col-md-1">
<button type="button" class="btn btn-danger btn-sm remove-feature">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
</div>
<% } %>
</div>
<button type="button" class="btn btn-sm btn-outline-primary" id="addFeature">
<i class="fas fa-plus mr-1"></i> Добавить функцию
</button>
</div>
</div>
</div>
<div class="col-md-4">
<!-- Status & Settings -->
<div class="card card-warning">
<div class="card-header">
<h3 class="card-title">Настройки</h3>
</div>
<div class="card-body">
<div class="form-group">
<div class="custom-control custom-switch">
<input type="checkbox" class="custom-control-input" id="isActive" name="isActive" <%= service.isActive ? 'checked' : '' %>>
<label class="custom-control-label" for="isActive">Активная услуга</label>
</div>
</div>
<div class="form-group">
<div class="custom-control custom-switch">
<input type="checkbox" class="custom-control-input" id="featured" name="featured" <%= service.featured ? 'checked' : '' %>>
<label class="custom-control-label" for="featured">Рекомендуемая услуга</label>
</div>
</div>
<div class="form-group">
<label for="order">Порядок отображения</label>
<input type="number" class="form-control" id="order" name="order" value="<%= service.order || 0 %>" min="0">
</div>
</div>
</div>
<!-- Tags -->
<div class="card card-secondary">
<div class="card-header">
<h3 class="card-title">Теги</h3>
</div>
<div class="card-body">
<div class="form-group">
<label for="tags">Теги (через запятую)</label>
<input type="text" class="form-control" id="tags" name="tags" value="<%= service.tags ? service.tags.join(', ') : '' %>">
<small class="form-text text-muted">Разделите теги запятыми</small>
</div>
</div>
</div>
<!-- SEO -->
<div class="card card-dark">
<div class="card-header">
<h3 class="card-title">SEO настройки</h3>
</div>
<div class="card-body">
<div class="form-group">
<label for="seoTitle">SEO заголовок</label>
<input type="text" class="form-control" id="seoTitle" name="seo[title]" value="<%= service.seo?.title || '' %>">
</div>
<div class="form-group">
<label for="seoDescription">SEO описание</label>
<textarea class="form-control" id="seoDescription" name="seo[description]" rows="3"><%= service.seo?.description || '' %></textarea>
</div>
<div class="form-group">
<label for="seoKeywords">Ключевые слова</label>
<input type="text" class="form-control" id="seoKeywords" name="seo[keywords]" value="<%= service.seo?.keywords || '' %>">
</div>
</div>
</div>
</div>
</div>
<!-- Submit Buttons -->
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-body">
<button type="submit" class="btn btn-success">
<i class="fas fa-save mr-1"></i> Сохранить изменения
</button>
<a href="/admin/services" class="btn btn-secondary ml-2">
<i class="fas fa-times mr-1"></i> Отмена
</a>
<button type="button" class="btn btn-danger ml-2" onclick="deleteService()">
<i class="fas fa-trash mr-1"></i> Удалить услугу
</button>
</div>
</div>
</div>
</div>
</form>
</div>
</section>
<script>
$(document).ready(function() {
let featureIndex = <%= service.features ? service.features.length : 1 %>;
// Add feature
$('#addFeature').click(function() {
const featureHtml = `
<div class="feature-item mb-3">
<div class="row">
<div class="col-md-8">
<input type="text" class="form-control" name="features[${featureIndex}][name]" placeholder="Название функции">
</div>
<div class="col-md-3">
<select class="form-control" name="features[${featureIndex}][included]">
<option value="true">Включено</option>
<option value="false">Не включено</option>
</select>
</div>
<div class="col-md-1">
<button type="button" class="btn btn-danger btn-sm remove-feature">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
</div>
`;
$('#featuresContainer').append(featureHtml);
featureIndex++;
});
// Remove feature
$(document).on('click', '.remove-feature', function() {
$(this).closest('.feature-item').remove();
});
// Form submission
$('#serviceForm').submit(function(e) {
e.preventDefault();
const serviceId = $('#serviceId').val();
const formData = new FormData(this);
const data = {};
// Convert FormData to object
for (let [key, value] of formData.entries()) {
if (key.includes('[') && key.includes(']')) {
// Handle nested objects (pricing, seo, features)
const matches = key.match(/(\w+)\[(\w+|\d+)\](?:\[(\w+)\])?/);
if (matches) {
const [, parent, child, grandchild] = matches;
if (!data[parent]) data[parent] = {};
if (grandchild) {
// features array handling
if (!data[parent][child]) data[parent][child] = {};
data[parent][child][grandchild] = value;
} else {
data[parent][child] = value;
}
}
} else {
data[key] = value;
}
}
// Convert features object to array
if (data.features) {
const featuresArray = [];
Object.keys(data.features).forEach(index => {
if (data.features[index].name) {
featuresArray.push({
name: data.features[index].name,
included: data.features[index].included === 'true'
});
}
});
data.features = featuresArray;
}
// Convert checkboxes
data.isActive = $('#isActive').is(':checked');
data.featured = $('#featured').is(':checked');
// Convert tags to array
if (data.tags) {
data.tags = data.tags.split(',').map(tag => tag.trim()).filter(tag => tag);
}
fetch(`/api/admin/services/${serviceId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data)
})
.then(response => response.json())
.then(result => {
if (result.success) {
toastr.success('Услуга успешно обновлена!');
setTimeout(() => {
window.location.href = '/admin/services';
}, 1500);
} else {
toastr.error(result.message || 'Ошибка при обновлении услуги');
}
})
.catch(error => {
console.error('Error:', error);
toastr.error('Произошла ошибка при обновлении услуги');
});
});
});
function deleteService() {
const serviceId = $('#serviceId').val();
Swal.fire({
title: 'Удалить услугу?',
text: 'Это действие невозможно отменить!',
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#d33',
cancelButtonColor: '#3085d6',
confirmButtonText: 'Да, удалить!',
cancelButtonText: 'Отмена'
}).then((result) => {
if (result.isConfirmed) {
fetch(`/api/admin/services/${serviceId}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
Swal.fire('Удалено!', 'Услуга была удалена.', 'success').then(() => {
window.location.href = '/admin/services';
});
} else {
Swal.fire('Ошибка!', data.message || 'Ошибка при удалении услуги', 'error');
}
})
.catch(error => {
console.error('Error:', error);
Swal.fire('Ошибка!', 'Произошла ошибка при удалении услуги', 'error');
});
}
});
}
</script>

View File

@@ -0,0 +1,410 @@
<!-- 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-edit 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"><a href="/admin/services">Услуги</a></li>
<li class="breadcrumb-item active">Редактировать</li>
</ol>
</div>
</div>
</div>
</div>
<!-- Main content -->
<section class="content">
<div class="container-fluid">
<form id="serviceForm">
<input type="hidden" id="serviceId" value="<%= service.id %>">
<div class="row">
<div class="col-md-8">
<!-- Basic Information -->
<div class="card card-primary">
<div class="card-header">
<h3 class="card-title">Основная информация</h3>
</div>
<div class="card-body">
<div class="form-group">
<label for="name">Название услуги *</label>
<input type="text" class="form-control" id="name" name="name" value="<%= service.name %>" required>
</div>
<div class="form-group">
<label for="shortDescription">Краткое описание</label>
<textarea class="form-control" id="shortDescription" name="shortDescription" rows="2"><%= service.shortDescription || '' %></textarea>
</div>
<div class="form-group">
<label for="description">Полное описание</label>
<textarea class="form-control" id="description" name="description" rows="6"><%= service.description || '' %></textarea>
</div>
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label for="category">Категория *</label>
<select class="form-control" id="category" name="category" required>
<option value="">Выберите категорию</option>
<option value="web-development" <%= service.category === 'web-development' ? 'selected' : '' %>>Веб-разработка</option>
<option value="mobile-development" <%= service.category === 'mobile-development' ? 'selected' : '' %>>Мобильная разработка</option>
<option value="ui-ux-design" <%= service.category === 'ui-ux-design' ? 'selected' : '' %>>UI/UX Дизайн</option>
<option value="consulting" <%= service.category === 'consulting' ? 'selected' : '' %>>Консалтинг</option>
<option value="support" <%= service.category === 'support' ? 'selected' : '' %>>Поддержка</option>
<option value="other" <%= service.category === 'other' ? 'selected' : '' %>>Другое</option>
</select>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="icon">Иконка</label>
<input type="text" class="form-control" id="icon" name="icon" value="<%= service.icon || 'fas fa-cog' %>">
<small class="form-text text-muted">FontAwesome класс иконки</small>
</div>
</div>
</div>
<div class="form-group">
<label for="estimatedTime">Время выполнения</label>
<input type="text" class="form-control" id="estimatedTime" name="estimatedTime" value="<%= service.estimatedTime || '' %>">
</div>
</div>
</div>
<!-- Pricing -->
<div class="card card-info">
<div class="card-header">
<h3 class="card-title">Ценообразование</h3>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label for="basePrice">Базовая цена ($)</label>
<input type="number" class="form-control" id="basePrice" name="pricing[basePrice]" value="<%= service.pricing?.basePrice || '' %>" min="0" step="0.01">
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="currency">Валюта</label>
<select class="form-control" id="currency" name="pricing[currency]">
<option value="USD" <%= service.pricing?.currency === 'USD' ? 'selected' : '' %>>USD ($)</option>
<option value="EUR" <%= service.pricing?.currency === 'EUR' ? 'selected' : '' %>>EUR (€)</option>
<option value="KRW" <%= service.pricing?.currency === 'KRW' ? 'selected' : '' %>>KRW (₩)</option>
</select>
</div>
</div>
</div>
<div class="form-group">
<label for="pricingType">Тип ценообразования</label>
<select class="form-control" id="pricingType" name="pricing[type]">
<option value="fixed" <%= service.pricing?.type === 'fixed' ? 'selected' : '' %>>Фиксированная цена</option>
<option value="hourly" <%= service.pricing?.type === 'hourly' ? 'selected' : '' %>>Почасовая оплата</option>
<option value="project" <%= service.pricing?.type === 'project' ? 'selected' : '' %>>За проект</option>
<option value="subscription" <%= service.pricing?.type === 'subscription' ? 'selected' : '' %>>Подписка</option>
</select>
</div>
</div>
</div>
<!-- Features -->
<div class="card card-success">
<div class="card-header">
<h3 class="card-title">Функции и возможности</h3>
</div>
<div class="card-body">
<div id="featuresContainer">
<% if (service.features && service.features.length > 0) { %>
<% service.features.forEach((feature, index) => { %>
<div class="feature-item mb-3">
<div class="row">
<div class="col-md-8">
<input type="text" class="form-control" name="features[<%= index %>][name]" value="<%= feature.name %>">
</div>
<div class="col-md-3">
<select class="form-control" name="features[<%= index %>][included]">
<option value="true" <%= feature.included ? 'selected' : '' %>>Включено</option>
<option value="false" <%= !feature.included ? 'selected' : '' %>>Не включено</option>
</select>
</div>
<div class="col-md-1">
<button type="button" class="btn btn-danger btn-sm remove-feature">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
</div>
<% }) %>
<% } else { %>
<div class="feature-item mb-3">
<div class="row">
<div class="col-md-8">
<input type="text" class="form-control" name="features[0][name]" placeholder="Название функции">
</div>
<div class="col-md-3">
<select class="form-control" name="features[0][included]">
<option value="true">Включено</option>
<option value="false">Не включено</option>
</select>
</div>
<div class="col-md-1">
<button type="button" class="btn btn-danger btn-sm remove-feature">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
</div>
<% } %>
</div>
<button type="button" class="btn btn-sm btn-outline-primary" id="addFeature">
<i class="fas fa-plus mr-1"></i> Добавить функцию
</button>
</div>
</div>
</div>
<div class="col-md-4">
<!-- Status & Settings -->
<div class="card card-warning">
<div class="card-header">
<h3 class="card-title">Настройки</h3>
</div>
<div class="card-body">
<div class="form-group">
<div class="custom-control custom-switch">
<input type="checkbox" class="custom-control-input" id="isActive" name="isActive" <%= service.isActive ? 'checked' : '' %>>
<label class="custom-control-label" for="isActive">Активная услуга</label>
</div>
</div>
<div class="form-group">
<div class="custom-control custom-switch">
<input type="checkbox" class="custom-control-input" id="featured" name="featured" <%= service.featured ? 'checked' : '' %>>
<label class="custom-control-label" for="featured">Рекомендуемая услуга</label>
</div>
</div>
<div class="form-group">
<label for="order">Порядок отображения</label>
<input type="number" class="form-control" id="order" name="order" value="<%= service.order || 0 %>" min="0">
</div>
</div>
</div>
<!-- Tags -->
<div class="card card-secondary">
<div class="card-header">
<h3 class="card-title">Теги</h3>
</div>
<div class="card-body">
<div class="form-group">
<label for="tags">Теги (через запятую)</label>
<input type="text" class="form-control" id="tags" name="tags" value="<%= service.tags ? service.tags.join(', ') : '' %>">
<small class="form-text text-muted">Разделите теги запятыми</small>
</div>
</div>
</div>
<!-- SEO -->
<div class="card card-dark">
<div class="card-header">
<h3 class="card-title">SEO настройки</h3>
</div>
<div class="card-body">
<div class="form-group">
<label for="seoTitle">SEO заголовок</label>
<input type="text" class="form-control" id="seoTitle" name="seo[title]" value="<%= service.seo?.title || '' %>">
</div>
<div class="form-group">
<label for="seoDescription">SEO описание</label>
<textarea class="form-control" id="seoDescription" name="seo[description]" rows="3"><%= service.seo?.description || '' %></textarea>
</div>
<div class="form-group">
<label for="seoKeywords">Ключевые слова</label>
<input type="text" class="form-control" id="seoKeywords" name="seo[keywords]" value="<%= service.seo?.keywords || '' %>">
</div>
</div>
</div>
</div>
</div>
<!-- Submit Buttons -->
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-body">
<button type="submit" class="btn btn-success">
<i class="fas fa-save mr-1"></i> Сохранить изменения
</button>
<a href="/admin/services" class="btn btn-secondary ml-2">
<i class="fas fa-times mr-1"></i> Отмена
</a>
<button type="button" class="btn btn-danger ml-2" onclick="deleteService()">
<i class="fas fa-trash mr-1"></i> Удалить услугу
</button>
</div>
</div>
</div>
</div>
</form>
</div>
</section>
<script>
$(document).ready(function() {
let featureIndex = <%= service.features ? service.features.length : 1 %>;
// Add feature
$('#addFeature').click(function() {
const featureHtml = `
<div class="feature-item mb-3">
<div class="row">
<div class="col-md-8">
<input type="text" class="form-control" name="features[${featureIndex}][name]" placeholder="Название функции">
</div>
<div class="col-md-3">
<select class="form-control" name="features[${featureIndex}][included]">
<option value="true">Включено</option>
<option value="false">Не включено</option>
</select>
</div>
<div class="col-md-1">
<button type="button" class="btn btn-danger btn-sm remove-feature">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
</div>
`;
$('#featuresContainer').append(featureHtml);
featureIndex++;
});
// Remove feature
$(document).on('click', '.remove-feature', function() {
$(this).closest('.feature-item').remove();
});
// Form submission
$('#serviceForm').submit(function(e) {
e.preventDefault();
const serviceId = $('#serviceId').val();
const formData = new FormData(this);
const data = {};
// Convert FormData to object
for (let [key, value] of formData.entries()) {
if (key.includes('[') && key.includes(']')) {
// Handle nested objects (pricing, seo, features)
const matches = key.match(/(\w+)\[(\w+|\d+)\](?:\[(\w+)\])?/);
if (matches) {
const [, parent, child, grandchild] = matches;
if (!data[parent]) data[parent] = {};
if (grandchild) {
// features array handling
if (!data[parent][child]) data[parent][child] = {};
data[parent][child][grandchild] = value;
} else {
data[parent][child] = value;
}
}
} else {
data[key] = value;
}
}
// Convert features object to array
if (data.features) {
const featuresArray = [];
Object.keys(data.features).forEach(index => {
if (data.features[index].name) {
featuresArray.push({
name: data.features[index].name,
included: data.features[index].included === 'true'
});
}
});
data.features = featuresArray;
}
// Convert checkboxes
data.isActive = $('#isActive').is(':checked');
data.featured = $('#featured').is(':checked');
// Convert tags to array
if (data.tags) {
data.tags = data.tags.split(',').map(tag => tag.trim()).filter(tag => tag);
}
fetch(`/api/admin/services/${serviceId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data)
})
.then(response => response.json())
.then(result => {
if (result.success) {
toastr.success('Услуга успешно обновлена!');
setTimeout(() => {
window.location.href = '/admin/services';
}, 1500);
} else {
toastr.error(result.message || 'Ошибка при обновлении услуги');
}
})
.catch(error => {
console.error('Error:', error);
toastr.error('Произошла ошибка при обновлении услуги');
});
});
});
function deleteService() {
const serviceId = $('#serviceId').val();
Swal.fire({
title: 'Удалить услугу?',
text: 'Это действие невозможно отменить!',
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#d33',
cancelButtonColor: '#3085d6',
confirmButtonText: 'Да, удалить!',
cancelButtonText: 'Отмена'
}).then((result) => {
if (result.isConfirmed) {
fetch(`/api/admin/services/${serviceId}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
Swal.fire('Удалено!', 'Услуга была удалена.', 'success').then(() => {
window.location.href = '/admin/services';
});
} else {
Swal.fire('Ошибка!', data.message || 'Ошибка при удалении услуги', 'error');
}
})
.catch(error => {
console.error('Error:', error);
Swal.fire('Ошибка!', 'Произошла ошибка при удалении услуги', 'error');
});
}
});
}
</script>

View File

@@ -0,0 +1,410 @@
<!-- 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-edit 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"><a href="/admin/services">Услуги</a></li>
<li class="breadcrumb-item active">Редактировать</li>
</ol>
</div>
</div>
</div>
</div>
<!-- Main content -->
<section class="content">
<div class="container-fluid">
<form id="serviceForm">
<input type="hidden" id="serviceId" value="<%= service.id %>">
<div class="row">
<div class="col-md-8">
<!-- Basic Information -->
<div class="card card-primary">
<div class="card-header">
<h3 class="card-title">Основная информация</h3>
</div>
<div class="card-body">
<div class="form-group">
<label for="name">Название услуги *</label>
<input type="text" class="form-control" id="name" name="name" value="<%= service.name %>" required>
</div>
<div class="form-group">
<label for="shortDescription">Краткое описание</label>
<textarea class="form-control" id="shortDescription" name="shortDescription" rows="2"><%= service.shortDescription || '' %></textarea>
</div>
<div class="form-group">
<label for="description">Полное описание</label>
<textarea class="form-control" id="description" name="description" rows="6"><%= service.description || '' %></textarea>
</div>
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label for="category">Категория *</label>
<select class="form-control" id="category" name="category" required>
<option value="">Выберите категорию</option>
<option value="web-development" <%= service.category === 'web-development' ? 'selected' : '' %>>Веб-разработка</option>
<option value="mobile-development" <%= service.category === 'mobile-development' ? 'selected' : '' %>>Мобильная разработка</option>
<option value="ui-ux-design" <%= service.category === 'ui-ux-design' ? 'selected' : '' %>>UI/UX Дизайн</option>
<option value="consulting" <%= service.category === 'consulting' ? 'selected' : '' %>>Консалтинг</option>
<option value="support" <%= service.category === 'support' ? 'selected' : '' %>>Поддержка</option>
<option value="other" <%= service.category === 'other' ? 'selected' : '' %>>Другое</option>
</select>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="icon">Иконка</label>
<input type="text" class="form-control" id="icon" name="icon" value="<%= service.icon || 'fas fa-cog' %>">
<small class="form-text text-muted">FontAwesome класс иконки</small>
</div>
</div>
</div>
<div class="form-group">
<label for="estimatedTime">Время выполнения</label>
<input type="text" class="form-control" id="estimatedTime" name="estimatedTime" value="<%= service.estimatedTime || '' %>">
</div>
</div>
</div>
<!-- Pricing -->
<div class="card card-info">
<div class="card-header">
<h3 class="card-title">Ценообразование</h3>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label for="basePrice">Базовая цена ($)</label>
<input type="number" class="form-control" id="basePrice" name="pricing[basePrice]" value="<%= service.pricing?.basePrice || '' %>" min="0" step="0.01">
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="currency">Валюта</label>
<select class="form-control" id="currency" name="pricing[currency]">
<option value="USD" <%= service.pricing?.currency === 'USD' ? 'selected' : '' %>>USD ($)</option>
<option value="EUR" <%= service.pricing?.currency === 'EUR' ? 'selected' : '' %>>EUR (€)</option>
<option value="KRW" <%= service.pricing?.currency === 'KRW' ? 'selected' : '' %>>KRW (₩)</option>
</select>
</div>
</div>
</div>
<div class="form-group">
<label for="pricingType">Тип ценообразования</label>
<select class="form-control" id="pricingType" name="pricing[type]">
<option value="fixed" <%= service.pricing?.type === 'fixed' ? 'selected' : '' %>>Фиксированная цена</option>
<option value="hourly" <%= service.pricing?.type === 'hourly' ? 'selected' : '' %>>Почасовая оплата</option>
<option value="project" <%= service.pricing?.type === 'project' ? 'selected' : '' %>>За проект</option>
<option value="subscription" <%= service.pricing?.type === 'subscription' ? 'selected' : '' %>>Подписка</option>
</select>
</div>
</div>
</div>
<!-- Features -->
<div class="card card-success">
<div class="card-header">
<h3 class="card-title">Функции и возможности</h3>
</div>
<div class="card-body">
<div id="featuresContainer">
<% if (service.features && service.features.length > 0) { %>
<% service.features.forEach((feature, index) => { %>
<div class="feature-item mb-3">
<div class="row">
<div class="col-md-8">
<input type="text" class="form-control" name="features[<%= index %>][name]" value="<%= feature.name %>">
</div>
<div class="col-md-3">
<select class="form-control" name="features[<%= index %>][included]">
<option value="true" <%= feature.included ? 'selected' : '' %>>Включено</option>
<option value="false" <%= !feature.included ? 'selected' : '' %>>Не включено</option>
</select>
</div>
<div class="col-md-1">
<button type="button" class="btn btn-danger btn-sm remove-feature">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
</div>
<% }) %>
<% } else { %>
<div class="feature-item mb-3">
<div class="row">
<div class="col-md-8">
<input type="text" class="form-control" name="features[0][name]" placeholder="Название функции">
</div>
<div class="col-md-3">
<select class="form-control" name="features[0][included]">
<option value="true">Включено</option>
<option value="false">Не включено</option>
</select>
</div>
<div class="col-md-1">
<button type="button" class="btn btn-danger btn-sm remove-feature">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
</div>
<% } %>
</div>
<button type="button" class="btn btn-sm btn-outline-primary" id="addFeature">
<i class="fas fa-plus mr-1"></i> Добавить функцию
</button>
</div>
</div>
</div>
<div class="col-md-4">
<!-- Status & Settings -->
<div class="card card-warning">
<div class="card-header">
<h3 class="card-title">Настройки</h3>
</div>
<div class="card-body">
<div class="form-group">
<div class="custom-control custom-switch">
<input type="checkbox" class="custom-control-input" id="isActive" name="isActive" <%= service.isActive ? 'checked' : '' %>>
<label class="custom-control-label" for="isActive">Активная услуга</label>
</div>
</div>
<div class="form-group">
<div class="custom-control custom-switch">
<input type="checkbox" class="custom-control-input" id="featured" name="featured" <%= service.featured ? 'checked' : '' %>>
<label class="custom-control-label" for="featured">Рекомендуемая услуга</label>
</div>
</div>
<div class="form-group">
<label for="order">Порядок отображения</label>
<input type="number" class="form-control" id="order" name="order" value="<%= service.order || 0 %>" min="0">
</div>
</div>
</div>
<!-- Tags -->
<div class="card card-secondary">
<div class="card-header">
<h3 class="card-title">Теги</h3>
</div>
<div class="card-body">
<div class="form-group">
<label for="tags">Теги (через запятую)</label>
<input type="text" class="form-control" id="tags" name="tags" value="<%= service.tags ? service.tags.join(', ') : '' %>">
<small class="form-text text-muted">Разделите теги запятыми</small>
</div>
</div>
</div>
<!-- SEO -->
<div class="card card-dark">
<div class="card-header">
<h3 class="card-title">SEO настройки</h3>
</div>
<div class="card-body">
<div class="form-group">
<label for="seoTitle">SEO заголовок</label>
<input type="text" class="form-control" id="seoTitle" name="seo[title]" value="<%= service.seo?.title || '' %>">
</div>
<div class="form-group">
<label for="seoDescription">SEO описание</label>
<textarea class="form-control" id="seoDescription" name="seo[description]" rows="3"><%= service.seo?.description || '' %></textarea>
</div>
<div class="form-group">
<label for="seoKeywords">Ключевые слова</label>
<input type="text" class="form-control" id="seoKeywords" name="seo[keywords]" value="<%= service.seo?.keywords || '' %>">
</div>
</div>
</div>
</div>
</div>
<!-- Submit Buttons -->
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-body">
<button type="submit" class="btn btn-success">
<i class="fas fa-save mr-1"></i> Сохранить изменения
</button>
<a href="/admin/services" class="btn btn-secondary ml-2">
<i class="fas fa-times mr-1"></i> Отмена
</a>
<button type="button" class="btn btn-danger ml-2" onclick="deleteService()">
<i class="fas fa-trash mr-1"></i> Удалить услугу
</button>
</div>
</div>
</div>
</div>
</form>
</div>
</section>
<script>
$(document).ready(function() {
let featureIndex = <% if (service.features && service.features.length) { %><%= service.features.length %><% } else { %>1<% } %>;
// Add feature
$('#addFeature').click(function() {
const featureHtml = `
<div class="feature-item mb-3">
<div class="row">
<div class="col-md-8">
<input type="text" class="form-control" name="features[${featureIndex}][name]" placeholder="Название функции">
</div>
<div class="col-md-3">
<select class="form-control" name="features[${featureIndex}][included]">
<option value="true">Включено</option>
<option value="false">Не включено</option>
</select>
</div>
<div class="col-md-1">
<button type="button" class="btn btn-danger btn-sm remove-feature">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
</div>
`;
$('#featuresContainer').append(featureHtml);
featureIndex++;
});
// Remove feature
$(document).on('click', '.remove-feature', function() {
$(this).closest('.feature-item').remove();
});
// Form submission
$('#serviceForm').submit(function(e) {
e.preventDefault();
const serviceId = $('#serviceId').val();
const formData = new FormData(this);
const data = {};
// Convert FormData to object
for (let [key, value] of formData.entries()) {
if (key.includes('[') && key.includes(']')) {
// Handle nested objects (pricing, seo, features)
const matches = key.match(/(\w+)\[(\w+|\d+)\](?:\[(\w+)\])?/);
if (matches) {
const [, parent, child, grandchild] = matches;
if (!data[parent]) data[parent] = {};
if (grandchild) {
// features array handling
if (!data[parent][child]) data[parent][child] = {};
data[parent][child][grandchild] = value;
} else {
data[parent][child] = value;
}
}
} else {
data[key] = value;
}
}
// Convert features object to array
if (data.features) {
const featuresArray = [];
Object.keys(data.features).forEach(index => {
if (data.features[index].name) {
featuresArray.push({
name: data.features[index].name,
included: data.features[index].included === 'true'
});
}
});
data.features = featuresArray;
}
// Convert checkboxes
data.isActive = $('#isActive').is(':checked');
data.featured = $('#featured').is(':checked');
// Convert tags to array
if (data.tags) {
data.tags = data.tags.split(',').map(tag => tag.trim()).filter(tag => tag);
}
fetch(`/api/admin/services/${serviceId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data)
})
.then(response => response.json())
.then(result => {
if (result.success) {
toastr.success('Услуга успешно обновлена!');
setTimeout(() => {
window.location.href = '/admin/services';
}, 1500);
} else {
toastr.error(result.message || 'Ошибка при обновлении услуги');
}
})
.catch(error => {
console.error('Error:', error);
toastr.error('Произошла ошибка при обновлении услуги');
});
});
});
function deleteService() {
const serviceId = $('#serviceId').val();
Swal.fire({
title: 'Удалить услугу?',
text: 'Это действие невозможно отменить!',
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#d33',
cancelButtonColor: '#3085d6',
confirmButtonText: 'Да, удалить!',
cancelButtonText: 'Отмена'
}).then((result) => {
if (result.isConfirmed) {
fetch(`/api/admin/services/${serviceId}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
Swal.fire('Удалено!', 'Услуга была удалена.', 'success').then(() => {
window.location.href = '/admin/services';
});
} else {
Swal.fire('Ошибка!', data.message || 'Ошибка при удалении услуги', 'error');
}
})
.catch(error => {
console.error('Error:', error);
Swal.fire('Ошибка!', 'Произошла ошибка при удалении услуги', 'error');
});
}
});
}
</script>

View File

@@ -0,0 +1,411 @@
<!-- 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-edit 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"><a href="/admin/services">Услуги</a></li>
<li class="breadcrumb-item active">Редактировать</li>
</ol>
</div>
</div>
</div>
</div>
<!-- Main content -->
<section class="content">
<div class="container-fluid">
<form id="serviceForm">
<input type="hidden" id="serviceId" value="<%= service.id %>">
<div class="row">
<div class="col-md-8">
<!-- Basic Information -->
<div class="card card-primary">
<div class="card-header">
<h3 class="card-title">Основная информация</h3>
</div>
<div class="card-body">
<div class="form-group">
<label for="name">Название услуги *</label>
<input type="text" class="form-control" id="name" name="name" value="<%= service.name %>" required>
</div>
<div class="form-group">
<label for="shortDescription">Краткое описание</label>
<textarea class="form-control" id="shortDescription" name="shortDescription" rows="2"><%= service.shortDescription || '' %></textarea>
</div>
<div class="form-group">
<label for="description">Полное описание</label>
<textarea class="form-control" id="description" name="description" rows="6"><%= service.description || '' %></textarea>
</div>
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label for="category">Категория *</label>
<select class="form-control" id="category" name="category" required>
<option value="">Выберите категорию</option>
<option value="web-development" <%= service.category === 'web-development' ? 'selected' : '' %>>Веб-разработка</option>
<option value="mobile-development" <%= service.category === 'mobile-development' ? 'selected' : '' %>>Мобильная разработка</option>
<option value="ui-ux-design" <%= service.category === 'ui-ux-design' ? 'selected' : '' %>>UI/UX Дизайн</option>
<option value="consulting" <%= service.category === 'consulting' ? 'selected' : '' %>>Консалтинг</option>
<option value="support" <%= service.category === 'support' ? 'selected' : '' %>>Поддержка</option>
<option value="other" <%= service.category === 'other' ? 'selected' : '' %>>Другое</option>
</select>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="icon">Иконка</label>
<input type="text" class="form-control" id="icon" name="icon" value="<%= service.icon || 'fas fa-cog' %>">
<small class="form-text text-muted">FontAwesome класс иконки</small>
</div>
</div>
</div>
<div class="form-group">
<label for="estimatedTime">Время выполнения</label>
<input type="text" class="form-control" id="estimatedTime" name="estimatedTime" value="<%= service.estimatedTime || '' %>">
</div>
</div>
</div>
<!-- Pricing -->
<div class="card card-info">
<div class="card-header">
<h3 class="card-title">Ценообразование</h3>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label for="basePrice">Базовая цена ($)</label>
<input type="number" class="form-control" id="basePrice" name="pricing[basePrice]" value="<%= service.pricing?.basePrice || '' %>" min="0" step="0.01">
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="currency">Валюта</label>
<select class="form-control" id="currency" name="pricing[currency]">
<option value="USD" <%= service.pricing?.currency === 'USD' ? 'selected' : '' %>>USD ($)</option>
<option value="EUR" <%= service.pricing?.currency === 'EUR' ? 'selected' : '' %>>EUR (€)</option>
<option value="KRW" <%= service.pricing?.currency === 'KRW' ? 'selected' : '' %>>KRW (₩)</option>
</select>
</div>
</div>
</div>
<div class="form-group">
<label for="pricingType">Тип ценообразования</label>
<select class="form-control" id="pricingType" name="pricing[type]">
<option value="fixed" <%= service.pricing?.type === 'fixed' ? 'selected' : '' %>>Фиксированная цена</option>
<option value="hourly" <%= service.pricing?.type === 'hourly' ? 'selected' : '' %>>Почасовая оплата</option>
<option value="project" <%= service.pricing?.type === 'project' ? 'selected' : '' %>>За проект</option>
<option value="subscription" <%= service.pricing?.type === 'subscription' ? 'selected' : '' %>>Подписка</option>
</select>
</div>
</div>
</div>
<!-- Features -->
<div class="card card-success">
<div class="card-header">
<h3 class="card-title">Функции и возможности</h3>
</div>
<div class="card-body">
<div id="featuresContainer">
<% if (service.features && service.features.length > 0) { %>
<% service.features.forEach((feature, index) => { %>
<div class="feature-item mb-3">
<div class="row">
<div class="col-md-8">
<input type="text" class="form-control" name="features[<%= index %>][name]" value="<%= feature.name %>">
</div>
<div class="col-md-3">
<select class="form-control" name="features[<%= index %>][included]">
<option value="true" <%= feature.included ? 'selected' : '' %>>Включено</option>
<option value="false" <%= !feature.included ? 'selected' : '' %>>Не включено</option>
</select>
</div>
<div class="col-md-1">
<button type="button" class="btn btn-danger btn-sm remove-feature">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
</div>
<% }) %>
<% } else { %>
<div class="feature-item mb-3">
<div class="row">
<div class="col-md-8">
<input type="text" class="form-control" name="features[0][name]" placeholder="Название функции">
</div>
<div class="col-md-3">
<select class="form-control" name="features[0][included]">
<option value="true">Включено</option>
<option value="false">Не включено</option>
</select>
</div>
<div class="col-md-1">
<button type="button" class="btn btn-danger btn-sm remove-feature">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
</div>
<% } %>
</div>
<button type="button" class="btn btn-sm btn-outline-primary" id="addFeature">
<i class="fas fa-plus mr-1"></i> Добавить функцию
</button>
</div>
</div>
</div>
<div class="col-md-4">
<!-- Status & Settings -->
<div class="card card-warning">
<div class="card-header">
<h3 class="card-title">Настройки</h3>
</div>
<div class="card-body">
<div class="form-group">
<div class="custom-control custom-switch">
<input type="checkbox" class="custom-control-input" id="isActive" name="isActive" <%= service.isActive ? 'checked' : '' %>>
<label class="custom-control-label" for="isActive">Активная услуга</label>
</div>
</div>
<div class="form-group">
<div class="custom-control custom-switch">
<input type="checkbox" class="custom-control-input" id="featured" name="featured" <%= service.featured ? 'checked' : '' %>>
<label class="custom-control-label" for="featured">Рекомендуемая услуга</label>
</div>
</div>
<div class="form-group">
<label for="order">Порядок отображения</label>
<input type="number" class="form-control" id="order" name="order" value="<%= service.order || 0 %>" min="0">
</div>
</div>
</div>
<!-- Tags -->
<div class="card card-secondary">
<div class="card-header">
<h3 class="card-title">Теги</h3>
</div>
<div class="card-body">
<div class="form-group">
<label for="tags">Теги (через запятую)</label>
<input type="text" class="form-control" id="tags" name="tags" value="<%= service.tags ? service.tags.join(', ') : '' %>">
<small class="form-text text-muted">Разделите теги запятыми</small>
</div>
</div>
</div>
<!-- SEO -->
<div class="card card-dark">
<div class="card-header">
<h3 class="card-title">SEO настройки</h3>
</div>
<div class="card-body">
<div class="form-group">
<label for="seoTitle">SEO заголовок</label>
<input type="text" class="form-control" id="seoTitle" name="seo[title]" value="<%= service.seo?.title || '' %>">
</div>
<div class="form-group">
<label for="seoDescription">SEO описание</label>
<textarea class="form-control" id="seoDescription" name="seo[description]" rows="3"><%= service.seo?.description || '' %></textarea>
</div>
<div class="form-group">
<label for="seoKeywords">Ключевые слова</label>
<input type="text" class="form-control" id="seoKeywords" name="seo[keywords]" value="<%= service.seo?.keywords || '' %>">
</div>
</div>
</div>
</div>
</div>
<!-- Submit Buttons -->
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-body">
<button type="submit" class="btn btn-success">
<i class="fas fa-save mr-1"></i> Сохранить изменения
</button>
<a href="/admin/services" class="btn btn-secondary ml-2">
<i class="fas fa-times mr-1"></i> Отмена
</a>
<button type="button" class="btn btn-danger ml-2" onclick="deleteService()">
<i class="fas fa-trash mr-1"></i> Удалить услугу
</button>
</div>
</div>
</div>
</div>
</form>
</div>
</section>
<script>
$(document).ready(function() {
const service = <%- JSON.stringify(service) %>;
let featureIndex = service.features ? service.features.length : 1;
// Add feature
$('#addFeature').click(function() {
const featureHtml = `
<div class="feature-item mb-3">
<div class="row">
<div class="col-md-8">
<input type="text" class="form-control" name="features[${featureIndex}][name]" placeholder="Название функции">
</div>
<div class="col-md-3">
<select class="form-control" name="features[${featureIndex}][included]">
<option value="true">Включено</option>
<option value="false">Не включено</option>
</select>
</div>
<div class="col-md-1">
<button type="button" class="btn btn-danger btn-sm remove-feature">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
</div>
`;
$('#featuresContainer').append(featureHtml);
featureIndex++;
});
// Remove feature
$(document).on('click', '.remove-feature', function() {
$(this).closest('.feature-item').remove();
});
// Form submission
$('#serviceForm').submit(function(e) {
e.preventDefault();
const serviceId = $('#serviceId').val();
const formData = new FormData(this);
const data = {};
// Convert FormData to object
for (let [key, value] of formData.entries()) {
if (key.includes('[') && key.includes(']')) {
// Handle nested objects (pricing, seo, features)
const matches = key.match(/(\w+)\[(\w+|\d+)\](?:\[(\w+)\])?/);
if (matches) {
const [, parent, child, grandchild] = matches;
if (!data[parent]) data[parent] = {};
if (grandchild) {
// features array handling
if (!data[parent][child]) data[parent][child] = {};
data[parent][child][grandchild] = value;
} else {
data[parent][child] = value;
}
}
} else {
data[key] = value;
}
}
// Convert features object to array
if (data.features) {
const featuresArray = [];
Object.keys(data.features).forEach(index => {
if (data.features[index].name) {
featuresArray.push({
name: data.features[index].name,
included: data.features[index].included === 'true'
});
}
});
data.features = featuresArray;
}
// Convert checkboxes
data.isActive = $('#isActive').is(':checked');
data.featured = $('#featured').is(':checked');
// Convert tags to array
if (data.tags) {
data.tags = data.tags.split(',').map(tag => tag.trim()).filter(tag => tag);
}
fetch(`/api/admin/services/${serviceId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data)
})
.then(response => response.json())
.then(result => {
if (result.success) {
toastr.success('Услуга успешно обновлена!');
setTimeout(() => {
window.location.href = '/admin/services';
}, 1500);
} else {
toastr.error(result.message || 'Ошибка при обновлении услуги');
}
})
.catch(error => {
console.error('Error:', error);
toastr.error('Произошла ошибка при обновлении услуги');
});
});
});
function deleteService() {
const serviceId = $('#serviceId').val();
Swal.fire({
title: 'Удалить услугу?',
text: 'Это действие невозможно отменить!',
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#d33',
cancelButtonColor: '#3085d6',
confirmButtonText: 'Да, удалить!',
cancelButtonText: 'Отмена'
}).then((result) => {
if (result.isConfirmed) {
fetch(`/api/admin/services/${serviceId}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
Swal.fire('Удалено!', 'Услуга была удалена.', 'success').then(() => {
window.location.href = '/admin/services';
});
} else {
Swal.fire('Ошибка!', data.message || 'Ошибка при удалении услуги', 'error');
}
})
.catch(error => {
console.error('Error:', error);
Swal.fire('Ошибка!', 'Произошла ошибка при удалении услуги', 'error');
});
}
});
}
</script>

View File

@@ -0,0 +1,411 @@
<!-- 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-edit 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"><a href="/admin/services">Услуги</a></li>
<li class="breadcrumb-item active">Редактировать</li>
</ol>
</div>
</div>
</div>
</div>
<!-- Main content -->
<section class="content">
<div class="container-fluid">
<form id="serviceForm">
<input type="hidden" id="serviceId" value="<%= service.id %>">
<div class="row">
<div class="col-md-8">
<!-- Basic Information -->
<div class="card card-primary">
<div class="card-header">
<h3 class="card-title">Основная информация</h3>
</div>
<div class="card-body">
<div class="form-group">
<label for="name">Название услуги *</label>
<input type="text" class="form-control" id="name" name="name" value="<%= service.name %>" required>
</div>
<div class="form-group">
<label for="shortDescription">Краткое описание</label>
<textarea class="form-control" id="shortDescription" name="shortDescription" rows="2"><%= service.shortDescription || '' %></textarea>
</div>
<div class="form-group">
<label for="description">Полное описание</label>
<textarea class="form-control" id="description" name="description" rows="6"><%= service.description || '' %></textarea>
</div>
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label for="category">Категория *</label>
<select class="form-control" id="category" name="category" required>
<option value="">Выберите категорию</option>
<option value="web-development" <%= service.category === 'web-development' ? 'selected' : '' %>>Веб-разработка</option>
<option value="mobile-development" <%= service.category === 'mobile-development' ? 'selected' : '' %>>Мобильная разработка</option>
<option value="ui-ux-design" <%= service.category === 'ui-ux-design' ? 'selected' : '' %>>UI/UX Дизайн</option>
<option value="consulting" <%= service.category === 'consulting' ? 'selected' : '' %>>Консалтинг</option>
<option value="support" <%= service.category === 'support' ? 'selected' : '' %>>Поддержка</option>
<option value="other" <%= service.category === 'other' ? 'selected' : '' %>>Другое</option>
</select>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="icon">Иконка</label>
<input type="text" class="form-control" id="icon" name="icon" value="<%= service.icon || 'fas fa-cog' %>">
<small class="form-text text-muted">FontAwesome класс иконки</small>
</div>
</div>
</div>
<div class="form-group">
<label for="estimatedTime">Время выполнения</label>
<input type="text" class="form-control" id="estimatedTime" name="estimatedTime" value="<%= service.estimatedTime || '' %>">
</div>
</div>
</div>
<!-- Pricing -->
<div class="card card-info">
<div class="card-header">
<h3 class="card-title">Ценообразование</h3>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label for="basePrice">Базовая цена ($)</label>
<input type="number" class="form-control" id="basePrice" name="pricing[basePrice]" value="<%= service.pricing?.basePrice || '' %>" min="0" step="0.01">
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="currency">Валюта</label>
<select class="form-control" id="currency" name="pricing[currency]">
<option value="USD" <%= service.pricing?.currency === 'USD' ? 'selected' : '' %>>USD ($)</option>
<option value="EUR" <%= service.pricing?.currency === 'EUR' ? 'selected' : '' %>>EUR (€)</option>
<option value="KRW" <%= service.pricing?.currency === 'KRW' ? 'selected' : '' %>>KRW (₩)</option>
</select>
</div>
</div>
</div>
<div class="form-group">
<label for="pricingType">Тип ценообразования</label>
<select class="form-control" id="pricingType" name="pricing[type]">
<option value="fixed" <%= service.pricing?.type === 'fixed' ? 'selected' : '' %>>Фиксированная цена</option>
<option value="hourly" <%= service.pricing?.type === 'hourly' ? 'selected' : '' %>>Почасовая оплата</option>
<option value="project" <%= service.pricing?.type === 'project' ? 'selected' : '' %>>За проект</option>
<option value="subscription" <%= service.pricing?.type === 'subscription' ? 'selected' : '' %>>Подписка</option>
</select>
</div>
</div>
</div>
<!-- Features -->
<div class="card card-success">
<div class="card-header">
<h3 class="card-title">Функции и возможности</h3>
</div>
<div class="card-body">
<div id="featuresContainer">
<% if (service.features && service.features.length > 0) { %>
<% service.features.forEach((feature, index) => { %>
<div class="feature-item mb-3">
<div class="row">
<div class="col-md-8">
<input type="text" class="form-control" name="features[<%= index %>][name]" value="<%= feature.name %>">
</div>
<div class="col-md-3">
<select class="form-control" name="features[<%= index %>][included]">
<option value="true" <%= feature.included ? 'selected' : '' %>>Включено</option>
<option value="false" <%= !feature.included ? 'selected' : '' %>>Не включено</option>
</select>
</div>
<div class="col-md-1">
<button type="button" class="btn btn-danger btn-sm remove-feature">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
</div>
<% }) %>
<% } else { %>
<div class="feature-item mb-3">
<div class="row">
<div class="col-md-8">
<input type="text" class="form-control" name="features[0][name]" placeholder="Название функции">
</div>
<div class="col-md-3">
<select class="form-control" name="features[0][included]">
<option value="true">Включено</option>
<option value="false">Не включено</option>
</select>
</div>
<div class="col-md-1">
<button type="button" class="btn btn-danger btn-sm remove-feature">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
</div>
<% } %>
</div>
<button type="button" class="btn btn-sm btn-outline-primary" id="addFeature">
<i class="fas fa-plus mr-1"></i> Добавить функцию
</button>
</div>
</div>
</div>
<div class="col-md-4">
<!-- Status & Settings -->
<div class="card card-warning">
<div class="card-header">
<h3 class="card-title">Настройки</h3>
</div>
<div class="card-body">
<div class="form-group">
<div class="custom-control custom-switch">
<input type="checkbox" class="custom-control-input" id="isActive" name="isActive" <%= service.isActive ? 'checked' : '' %>>
<label class="custom-control-label" for="isActive">Активная услуга</label>
</div>
</div>
<div class="form-group">
<div class="custom-control custom-switch">
<input type="checkbox" class="custom-control-input" id="featured" name="featured" <%= service.featured ? 'checked' : '' %>>
<label class="custom-control-label" for="featured">Рекомендуемая услуга</label>
</div>
</div>
<div class="form-group">
<label for="order">Порядок отображения</label>
<input type="number" class="form-control" id="order" name="order" value="<%= service.order || 0 %>" min="0">
</div>
</div>
</div>
<!-- Tags -->
<div class="card card-secondary">
<div class="card-header">
<h3 class="card-title">Теги</h3>
</div>
<div class="card-body">
<div class="form-group">
<label for="tags">Теги (через запятую)</label>
<input type="text" class="form-control" id="tags" name="tags" value="<%= service.tags ? service.tags.join(', ') : '' %>">
<small class="form-text text-muted">Разделите теги запятыми</small>
</div>
</div>
</div>
<!-- SEO -->
<div class="card card-dark">
<div class="card-header">
<h3 class="card-title">SEO настройки</h3>
</div>
<div class="card-body">
<div class="form-group">
<label for="seoTitle">SEO заголовок</label>
<input type="text" class="form-control" id="seoTitle" name="seo[title]" value="<%= service.seo?.title || '' %>">
</div>
<div class="form-group">
<label for="seoDescription">SEO описание</label>
<textarea class="form-control" id="seoDescription" name="seo[description]" rows="3"><%= service.seo?.description || '' %></textarea>
</div>
<div class="form-group">
<label for="seoKeywords">Ключевые слова</label>
<input type="text" class="form-control" id="seoKeywords" name="seo[keywords]" value="<%= service.seo?.keywords || '' %>">
</div>
</div>
</div>
</div>
</div>
<!-- Submit Buttons -->
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-body">
<button type="submit" class="btn btn-success">
<i class="fas fa-save mr-1"></i> Сохранить изменения
</button>
<a href="/admin/services" class="btn btn-secondary ml-2">
<i class="fas fa-times mr-1"></i> Отмена
</a>
<button type="button" class="btn btn-danger ml-2" onclick="deleteService()">
<i class="fas fa-trash mr-1"></i> Удалить услугу
</button>
</div>
</div>
</div>
</div>
</form>
</div>
</section>
<script>
$(document).ready(function() {
const service = <%- JSON.stringify(service) %>;
let featureIndex = service.features ? service.features.length : 1;
// Add feature
$('#addFeature').click(function() {
const featureHtml = `
<div class="feature-item mb-3">
<div class="row">
<div class="col-md-8">
<input type="text" class="form-control" name="features[${featureIndex}][name]" placeholder="Название функции">
</div>
<div class="col-md-3">
<select class="form-control" name="features[${featureIndex}][included]">
<option value="true">Включено</option>
<option value="false">Не включено</option>
</select>
</div>
<div class="col-md-1">
<button type="button" class="btn btn-danger btn-sm remove-feature">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
</div>
`;
$('#featuresContainer').append(featureHtml);
featureIndex++;
});
// Remove feature
$(document).on('click', '.remove-feature', function() {
$(this).closest('.feature-item').remove();
});
// Form submission
$('#serviceForm').submit(function(e) {
e.preventDefault();
const serviceId = $('#serviceId').val();
const formData = new FormData(this);
const data = {};
// Convert FormData to object
for (let [key, value] of formData.entries()) {
if (key.includes('[') && key.includes(']')) {
// Handle nested objects (pricing, seo, features)
const matches = key.match(/(\w+)\[(\w+|\d+)\](?:\[(\w+)\])?/);
if (matches) {
const [, parent, child, grandchild] = matches;
if (!data[parent]) data[parent] = {};
if (grandchild) {
// features array handling
if (!data[parent][child]) data[parent][child] = {};
data[parent][child][grandchild] = value;
} else {
data[parent][child] = value;
}
}
} else {
data[key] = value;
}
}
// Convert features object to array
if (data.features) {
const featuresArray = [];
Object.keys(data.features).forEach(index => {
if (data.features[index].name) {
featuresArray.push({
name: data.features[index].name,
included: data.features[index].included === 'true'
});
}
});
data.features = featuresArray;
}
// Convert checkboxes
data.isActive = $('#isActive').is(':checked');
data.featured = $('#featured').is(':checked');
// Convert tags to array
if (data.tags) {
data.tags = data.tags.split(',').map(tag => tag.trim()).filter(tag => tag);
}
fetch(`/api/admin/services/${serviceId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data)
})
.then(response => response.json())
.then(result => {
if (result.success) {
toastr.success('Услуга успешно обновлена!');
setTimeout(() => {
window.location.href = '/admin/services';
}, 1500);
} else {
toastr.error(result.message || 'Ошибка при обновлении услуги');
}
})
.catch(error => {
console.error('Error:', error);
toastr.error('Произошла ошибка при обновлении услуги');
});
});
});
function deleteService() {
const serviceId = $('#serviceId').val();
Swal.fire({
title: 'Удалить услугу?',
text: 'Это действие невозможно отменить!',
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#d33',
cancelButtonColor: '#3085d6',
confirmButtonText: 'Да, удалить!',
cancelButtonText: 'Отмена'
}).then((result) => {
if (result.isConfirmed) {
fetch(`/api/admin/services/${serviceId}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
Swal.fire('Удалено!', 'Услуга была удалена.', 'success').then(() => {
window.location.href = '/admin/services';
});
} else {
Swal.fire('Ошибка!', data.message || 'Ошибка при удалении услуги', 'error');
}
})
.catch(error => {
console.error('Error:', error);
Swal.fire('Ошибка!', 'Произошла ошибка при удалении услуги', 'error');
});
}
});
}
</script>

View File

@@ -0,0 +1,206 @@
<!-- 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-cogs mr-2"></i>Управление услугами</h1>
</div>
<div class="col-sm-6">
<div class="float-sm-right">
<a href="/admin/services/add" class="btn btn-primary">
<i class="fas fa-plus mr-1"></i>
Добавить услугу
</a>
</div>
</div>
</div>
</div>
</div>
<!-- Main content -->
<section class="content">
<div class="container-fluid">
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header">
<h3 class="card-title">Список услуг</h3>
<div class="card-tools">
<div class="input-group input-group-sm" style="width: 150px;">
<input type="text" name="table_search" class="form-control float-right" placeholder="Поиск">
<div class="input-group-append">
<button type="submit" class="btn btn-default">
<i class="fas fa-search"></i>
</button>
</div>
</div>
</div>
</div>
<div class="card-body table-responsive p-0">
<% if (services && services.length > 0) { %>
<table class="table table-hover text-nowrap">
<thead>
<tr>
<th>ID</th>
<th>Название</th>
<th>Категория</th>
<th>Статус</th>
<th>Цена от</th>
<th>Время</th>
<th>Рекомендуемая</th>
<th>Создано</th>
<th>Действия</th>
</tr>
</thead>
<tbody>
<% services.forEach(service => { %>
<tr>
<td>
<small class="text-muted">#<%= service.id.slice(-8) %></small>
</td>
<td>
<div class="d-flex align-items-center">
<i class="<%= service.icon || 'fas fa-cog' %> mr-2 text-primary"></i>
<strong><%= service.name %></strong>
</div>
<% if (service.shortDescription) { %>
<small class="text-muted d-block">
<%= service.shortDescription.substring(0, 80) %><%= service.shortDescription.length > 80 ? '...' : '' %>
</small>
<% } %>
</td>
<td>
<span class="badge badge-info"><%= service.category %></span>
</td>
<td>
<% if (service.isActive) { %>
<span class="badge badge-success">Активна</span>
<% } else { %>
<span class="badge badge-secondary">Неактивна</span>
<% } %>
</td>
<td>
<% if (service.pricing && service.pricing.basePrice) { %>
<span class="text-success font-weight-bold">$<%= service.pricing.basePrice %></span>
<% } else { %>
<span class="text-muted">Не указана</span>
<% } %>
</td>
<td>
<% if (service.estimatedTime) { %>
<i class="fas fa-clock mr-1 text-info"></i>
<%= service.estimatedTime %>
<% } else { %>
<span class="text-muted">-</span>
<% } %>
</td>
<td>
<% if (service.featured) { %>
<i class="fas fa-star text-warning" title="Рекомендуемая услуга"></i>
<% } else { %>
<i class="far fa-star text-muted"></i>
<% } %>
</td>
<td>
<small class="text-muted">
<%= new Date(service.createdAt).toLocaleDateString('ru-RU') %>
</small>
</td>
<td>
<div class="btn-group btn-group-sm">
<a href="/services#<%= service.id %>" target="_blank" class="btn btn-info btn-sm" title="Посмотреть на сайте">
<i class="fas fa-eye"></i>
</a>
<a href="/admin/services/edit/<%= service.id %>" class="btn btn-warning btn-sm" title="Редактировать">
<i class="fas fa-edit"></i>
</a>
<button onclick="deleteService('<%= service.id %>')" class="btn btn-danger btn-sm" title="Удалить">
<i class="fas fa-trash"></i>
</button>
</div>
</td>
</tr>
<% }) %>
</tbody>
</table>
<% } else { %>
<div class="p-4 text-center">
<i class="fas fa-cogs text-muted" style="font-size: 4rem;"></i>
<h4 class="mt-3 text-muted">Услуги не найдены</h4>
<p class="text-muted">Добавьте первую услугу для начала работы</p>
<a href="/admin/services/add" class="btn btn-primary">
<i class="fas fa-plus mr-1"></i>
Добавить услугу
</a>
</div>
<% } %>
</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 %>">&laquo;</a>
</li>
<% } %>
<% for (let i = 1; i <= pagination.total; i++) { %>
<li class="page-item <%= i === pagination.current ? '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 %>">&raquo;</a>
</li>
<% } %>
</ul>
</div>
<% } %>
</div>
</div>
</div>
</div>
</section>
<script>
function deleteService(id) {
Swal.fire({
title: 'Удалить услугу?',
text: 'Это действие невозможно отменить!',
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#d33',
cancelButtonColor: '#3085d6',
confirmButtonText: 'Да, удалить!',
cancelButtonText: 'Отмена'
}).then((result) => {
if (result.isConfirmed) {
fetch(`/api/admin/services/${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');
});
}
});
}
</script>

View File

@@ -0,0 +1,206 @@
<!-- 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-cogs mr-2"></i>Управление услугами</h1>
</div>
<div class="col-sm-6">
<div class="float-sm-right">
<a href="/admin/services/add" class="btn btn-primary">
<i class="fas fa-plus mr-1"></i>
Добавить услугу
</a>
</div>
</div>
</div>
</div>
</div>
<!-- Main content -->
<section class="content">
<div class="container-fluid">
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header">
<h3 class="card-title">Список услуг</h3>
<div class="card-tools">
<div class="input-group input-group-sm" style="width: 150px;">
<input type="text" name="table_search" class="form-control float-right" placeholder="Поиск">
<div class="input-group-append">
<button type="submit" class="btn btn-default">
<i class="fas fa-search"></i>
</button>
</div>
</div>
</div>
</div>
<div class="card-body table-responsive p-0">
<% if (services && services.length > 0) { %>
<table class="table table-hover text-nowrap">
<thead>
<tr>
<th>ID</th>
<th>Название</th>
<th>Категория</th>
<th>Статус</th>
<th>Цена от</th>
<th>Время</th>
<th>Рекомендуемая</th>
<th>Создано</th>
<th>Действия</th>
</tr>
</thead>
<tbody>
<% services.forEach(service => { %>
<tr>
<td>
<small class="text-muted">#<%= service.id.slice(-8) %></small>
</td>
<td>
<div class="d-flex align-items-center">
<i class="<%= service.icon || 'fas fa-cog' %> mr-2 text-primary"></i>
<strong><%= service.name %></strong>
</div>
<% if (service.shortDescription) { %>
<small class="text-muted d-block">
<%= service.shortDescription.substring(0, 80) %><%= service.shortDescription.length > 80 ? '...' : '' %>
</small>
<% } %>
</td>
<td>
<span class="badge badge-info"><%= service.category %></span>
</td>
<td>
<% if (service.isActive) { %>
<span class="badge badge-success">Активна</span>
<% } else { %>
<span class="badge badge-secondary">Неактивна</span>
<% } %>
</td>
<td>
<% if (service.pricing && service.pricing.basePrice) { %>
<span class="text-success font-weight-bold">$<%= service.pricing.basePrice %></span>
<% } else { %>
<span class="text-muted">Не указана</span>
<% } %>
</td>
<td>
<% if (service.estimatedTime) { %>
<i class="fas fa-clock mr-1 text-info"></i>
<%= service.estimatedTime %>
<% } else { %>
<span class="text-muted">-</span>
<% } %>
</td>
<td>
<% if (service.featured) { %>
<i class="fas fa-star text-warning" title="Рекомендуемая услуга"></i>
<% } else { %>
<i class="far fa-star text-muted"></i>
<% } %>
</td>
<td>
<small class="text-muted">
<%= new Date(service.createdAt).toLocaleDateString('ru-RU') %>
</small>
</td>
<td>
<div class="btn-group btn-group-sm">
<a href="/services#<%= service.id %>" target="_blank" class="btn btn-info btn-sm" title="Посмотреть на сайте">
<i class="fas fa-eye"></i>
</a>
<a href="/admin/services/edit/<%= service.id %>" class="btn btn-warning btn-sm" title="Редактировать">
<i class="fas fa-edit"></i>
</a>
<button onclick="deleteService('<%= service.id %>')" class="btn btn-danger btn-sm" title="Удалить">
<i class="fas fa-trash"></i>
</button>
</div>
</td>
</tr>
<% }) %>
</tbody>
</table>
<% } else { %>
<div class="p-4 text-center">
<i class="fas fa-cogs text-muted" style="font-size: 4rem;"></i>
<h4 class="mt-3 text-muted">Услуги не найдены</h4>
<p class="text-muted">Добавьте первую услугу для начала работы</p>
<a href="/admin/services/add" class="btn btn-primary">
<i class="fas fa-plus mr-1"></i>
Добавить услугу
</a>
</div>
<% } %>
</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 %>">&laquo;</a>
</li>
<% } %>
<% for (let i = 1; i <= pagination.total; i++) { %>
<li class="page-item <%= i === pagination.current ? '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 %>">&raquo;</a>
</li>
<% } %>
</ul>
</div>
<% } %>
</div>
</div>
</div>
</div>
</section>
<script>
function deleteService(id) {
Swal.fire({
title: 'Удалить услугу?',
text: 'Это действие невозможно отменить!',
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#d33',
cancelButtonColor: '#3085d6',
confirmButtonText: 'Да, удалить!',
cancelButtonText: 'Отмена'
}).then((result) => {
if (result.isConfirmed) {
fetch(`/api/admin/services/${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');
});
}
});
}
</script>

View File

@@ -0,0 +1,362 @@
<!-- Content Header (Page header) -->
<section class="content-header">
<div class="container-fluid">
<div class="row mb-2">
<div class="col-sm-6">
<h1><i class="fas fa-cogs mr-2"></i>Настройки сайта</h1>
</div>
<div class="col-sm-6">
<ol class="breadcrumb float-sm-right">
<li class="breadcrumb-item"><a href="/admin">Админ</a></li>
<li class="breadcrumb-item active">Настройки</li>
</ol>
</div>
</div>
</div>
</section>
<!-- Main content -->
<section class="content">
<div class="container-fluid">
<!-- Site Information Card -->
<div class="card">
<div class="card-header">
<h3 class="card-title">Основная информация</h3>
</div>
<form id="site-settings-form">
<div class="card-body">
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label for="site-title">Название сайта</label>
<input type="text" class="form-control" id="site-title" name="siteTitle" required>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="site-tagline">Слоган</label>
<input type="text" class="form-control" id="site-tagline" name="siteTagline">
</div>
</div>
</div>
<div class="form-group">
<label for="site-description">Описание сайта</label>
<textarea class="form-control" id="site-description" name="siteDescription" rows="3"></textarea>
</div>
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label for="company-name">Название компании</label>
<input type="text" class="form-control" id="company-name" name="companyName" required>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="company-email">Email компании</label>
<input type="email" class="form-control" id="company-email" name="companyEmail" required>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label for="company-phone">Телефон</label>
<input type="tel" class="form-control" id="company-phone" name="companyPhone">
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="company-address">Адрес</label>
<input type="text" class="form-control" id="company-address" name="companyAddress">
</div>
</div>
</div>
</div>
<div class="card-footer">
<button type="submit" class="btn btn-primary">
<i class="fas fa-save mr-1"></i>Сохранить
</button>
</div>
</form>
</div>
<!-- SEO Settings Card -->
<div class="card">
<div class="card-header">
<h3 class="card-title">SEO настройки</h3>
</div>
<form id="seo-settings-form">
<div class="card-body">
<div class="form-group">
<label for="meta-keywords">Ключевые слова</label>
<input type="text" class="form-control" id="meta-keywords" name="metaKeywords"
placeholder="ключевое слово 1, ключевое слово 2, ...">
<small class="form-text text-muted">Разделяйте ключевые слова запятыми</small>
</div>
<div class="form-group">
<label for="meta-description">Meta Description</label>
<textarea class="form-control" id="meta-description" name="metaDescription"
rows="3" maxlength="160"></textarea>
<small class="form-text text-muted">Рекомендуемая длина: до 160 символов</small>
</div>
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label for="google-analytics">Google Analytics ID</label>
<input type="text" class="form-control" id="google-analytics" name="googleAnalytics"
placeholder="G-XXXXXXXXXX">
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="google-tag-manager">Google Tag Manager ID</label>
<input type="text" class="form-control" id="google-tag-manager" name="googleTagManager"
placeholder="GTM-XXXXXXX">
</div>
</div>
</div>
</div>
<div class="card-footer">
<button type="submit" class="btn btn-primary">
<i class="fas fa-save mr-1"></i>Сохранить SEO
</button>
</div>
</form>
</div>
<!-- Social Media Card -->
<div class="card">
<div class="card-header">
<h3 class="card-title">Социальные сети</h3>
</div>
<form id="social-settings-form">
<div class="card-body">
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label for="facebook-url"><i class="fab fa-facebook mr-1"></i>Facebook</label>
<input type="url" class="form-control" id="facebook-url" name="facebookUrl"
placeholder="https://facebook.com/your-page">
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="twitter-url"><i class="fab fa-twitter mr-1"></i>Twitter</label>
<input type="url" class="form-control" id="twitter-url" name="twitterUrl"
placeholder="https://twitter.com/your-account">
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label for="linkedin-url"><i class="fab fa-linkedin mr-1"></i>LinkedIn</label>
<input type="url" class="form-control" id="linkedin-url" name="linkedinUrl"
placeholder="https://linkedin.com/company/your-company">
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="instagram-url"><i class="fab fa-instagram mr-1"></i>Instagram</label>
<input type="url" class="form-control" id="instagram-url" name="instagramUrl"
placeholder="https://instagram.com/your-account">
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label for="youtube-url"><i class="fab fa-youtube mr-1"></i>YouTube</label>
<input type="url" class="form-control" id="youtube-url" name="youtubeUrl"
placeholder="https://youtube.com/channel/your-channel">
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="github-url"><i class="fab fa-github mr-1"></i>GitHub</label>
<input type="url" class="form-control" id="github-url" name="githubUrl"
placeholder="https://github.com/your-account">
</div>
</div>
</div>
</div>
<div class="card-footer">
<button type="submit" class="btn btn-primary">
<i class="fas fa-save mr-1"></i>Сохранить соцсети
</button>
</div>
</form>
</div>
<!-- Maintenance Mode Card -->
<div class="card">
<div class="card-header">
<h3 class="card-title">Режим обслуживания</h3>
</div>
<div class="card-body">
<div class="form-group">
<div class="custom-control custom-switch">
<input type="checkbox" class="custom-control-input" id="maintenance-mode">
<label class="custom-control-label" for="maintenance-mode">
Включить режим обслуживания
</label>
</div>
<small class="form-text text-muted">
В режиме обслуживания сайт будет недоступен для обычных пользователей
</small>
</div>
<div class="form-group">
<label for="maintenance-message">Сообщение для пользователей</label>
<textarea class="form-control" id="maintenance-message" rows="3"
placeholder="Сайт временно недоступен из-за технических работ..."></textarea>
</div>
<button type="button" class="btn btn-warning" onclick="toggleMaintenanceMode()">
<i class="fas fa-tools mr-1"></i>Применить настройки
</button>
</div>
</div>
</div>
</section>
<script>
document.addEventListener('DOMContentLoaded', function() {
loadSettings();
// Setup form submissions
document.getElementById('site-settings-form').addEventListener('submit', saveSiteSettings);
document.getElementById('seo-settings-form').addEventListener('submit', saveSeoSettings);
document.getElementById('social-settings-form').addEventListener('submit', saveSocialSettings);
});
async function loadSettings() {
try {
const response = await fetch('/api/admin/settings');
const data = await response.json();
if (data.success) {
const settings = data.settings;
// Site settings
document.getElementById('site-title').value = settings.siteTitle || '';
document.getElementById('site-tagline').value = settings.siteTagline || '';
document.getElementById('site-description').value = settings.siteDescription || '';
document.getElementById('company-name').value = settings.companyName || '';
document.getElementById('company-email').value = settings.companyEmail || '';
document.getElementById('company-phone').value = settings.companyPhone || '';
document.getElementById('company-address').value = settings.companyAddress || '';
// SEO settings
document.getElementById('meta-keywords').value = settings.metaKeywords || '';
document.getElementById('meta-description').value = settings.metaDescription || '';
document.getElementById('google-analytics').value = settings.googleAnalytics || '';
document.getElementById('google-tag-manager').value = settings.googleTagManager || '';
// Social media
document.getElementById('facebook-url').value = settings.facebookUrl || '';
document.getElementById('twitter-url').value = settings.twitterUrl || '';
document.getElementById('linkedin-url').value = settings.linkedinUrl || '';
document.getElementById('instagram-url').value = settings.instagramUrl || '';
document.getElementById('youtube-url').value = settings.youtubeUrl || '';
document.getElementById('github-url').value = settings.githubUrl || '';
// Maintenance mode
document.getElementById('maintenance-mode').checked = settings.maintenanceMode || false;
document.getElementById('maintenance-message').value = settings.maintenanceMessage || '';
}
} catch (error) {
console.error('Error loading settings:', error);
alert('Ошибка загрузки настроек: ' + error.message);
}
}
async function saveSiteSettings(event) {
event.preventDefault();
const formData = new FormData(event.target);
const settings = {
siteTitle: formData.get('siteTitle'),
siteTagline: formData.get('siteTagline'),
siteDescription: formData.get('siteDescription'),
companyName: formData.get('companyName'),
companyEmail: formData.get('companyEmail'),
companyPhone: formData.get('companyPhone'),
companyAddress: formData.get('companyAddress')
};
await saveSettings(settings, 'Основные настройки сохранены');
}
async function saveSeoSettings(event) {
event.preventDefault();
const formData = new FormData(event.target);
const settings = {
metaKeywords: formData.get('metaKeywords'),
metaDescription: formData.get('metaDescription'),
googleAnalytics: formData.get('googleAnalytics'),
googleTagManager: formData.get('googleTagManager')
};
await saveSettings(settings, 'SEO настройки сохранены');
}
async function saveSocialSettings(event) {
event.preventDefault();
const formData = new FormData(event.target);
const settings = {
facebookUrl: formData.get('facebookUrl'),
twitterUrl: formData.get('twitterUrl'),
linkedinUrl: formData.get('linkedinUrl'),
instagramUrl: formData.get('instagramUrl'),
youtubeUrl: formData.get('youtubeUrl'),
githubUrl: formData.get('githubUrl')
};
await saveSettings(settings, 'Настройки соцсетей сохранены');
}
async function saveSettings(settings, successMessage) {
try {
const response = await fetch('/api/admin/settings', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(settings)
});
const data = await response.json();
if (data.success) {
alert(successMessage);
} else {
throw new Error(data.message);
}
} catch (error) {
console.error('Error saving settings:', error);
alert('Ошибка сохранения настроек: ' + error.message);
}
}
async function toggleMaintenanceMode() {
const isEnabled = document.getElementById('maintenance-mode').checked;
const message = document.getElementById('maintenance-message').value;
const settings = {
maintenanceMode: isEnabled,
maintenanceMessage: message
};
await saveSettings(settings,
isEnabled ? 'Режим обслуживания включен' : 'Режим обслуживания отключен'
);
}
</script>

View File

@@ -0,0 +1,362 @@
<!-- Content Header (Page header) -->
<section class="content-header">
<div class="container-fluid">
<div class="row mb-2">
<div class="col-sm-6">
<h1><i class="fas fa-cogs mr-2"></i>Настройки сайта</h1>
</div>
<div class="col-sm-6">
<ol class="breadcrumb float-sm-right">
<li class="breadcrumb-item"><a href="/admin">Админ</a></li>
<li class="breadcrumb-item active">Настройки</li>
</ol>
</div>
</div>
</div>
</section>
<!-- Main content -->
<section class="content">
<div class="container-fluid">
<!-- Site Information Card -->
<div class="card">
<div class="card-header">
<h3 class="card-title">Основная информация</h3>
</div>
<form id="site-settings-form">
<div class="card-body">
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label for="site-title">Название сайта</label>
<input type="text" class="form-control" id="site-title" name="siteTitle" required>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="site-tagline">Слоган</label>
<input type="text" class="form-control" id="site-tagline" name="siteTagline">
</div>
</div>
</div>
<div class="form-group">
<label for="site-description">Описание сайта</label>
<textarea class="form-control" id="site-description" name="siteDescription" rows="3"></textarea>
</div>
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label for="company-name">Название компании</label>
<input type="text" class="form-control" id="company-name" name="companyName" required>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="company-email">Email компании</label>
<input type="email" class="form-control" id="company-email" name="companyEmail" required>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label for="company-phone">Телефон</label>
<input type="tel" class="form-control" id="company-phone" name="companyPhone">
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="company-address">Адрес</label>
<input type="text" class="form-control" id="company-address" name="companyAddress">
</div>
</div>
</div>
</div>
<div class="card-footer">
<button type="submit" class="btn btn-primary">
<i class="fas fa-save mr-1"></i>Сохранить
</button>
</div>
</form>
</div>
<!-- SEO Settings Card -->
<div class="card">
<div class="card-header">
<h3 class="card-title">SEO настройки</h3>
</div>
<form id="seo-settings-form">
<div class="card-body">
<div class="form-group">
<label for="meta-keywords">Ключевые слова</label>
<input type="text" class="form-control" id="meta-keywords" name="metaKeywords"
placeholder="ключевое слово 1, ключевое слово 2, ...">
<small class="form-text text-muted">Разделяйте ключевые слова запятыми</small>
</div>
<div class="form-group">
<label for="meta-description">Meta Description</label>
<textarea class="form-control" id="meta-description" name="metaDescription"
rows="3" maxlength="160"></textarea>
<small class="form-text text-muted">Рекомендуемая длина: до 160 символов</small>
</div>
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label for="google-analytics">Google Analytics ID</label>
<input type="text" class="form-control" id="google-analytics" name="googleAnalytics"
placeholder="G-XXXXXXXXXX">
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="google-tag-manager">Google Tag Manager ID</label>
<input type="text" class="form-control" id="google-tag-manager" name="googleTagManager"
placeholder="GTM-XXXXXXX">
</div>
</div>
</div>
</div>
<div class="card-footer">
<button type="submit" class="btn btn-primary">
<i class="fas fa-save mr-1"></i>Сохранить SEO
</button>
</div>
</form>
</div>
<!-- Social Media Card -->
<div class="card">
<div class="card-header">
<h3 class="card-title">Социальные сети</h3>
</div>
<form id="social-settings-form">
<div class="card-body">
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label for="facebook-url"><i class="fab fa-facebook mr-1"></i>Facebook</label>
<input type="url" class="form-control" id="facebook-url" name="facebookUrl"
placeholder="https://facebook.com/your-page">
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="twitter-url"><i class="fab fa-twitter mr-1"></i>Twitter</label>
<input type="url" class="form-control" id="twitter-url" name="twitterUrl"
placeholder="https://twitter.com/your-account">
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label for="linkedin-url"><i class="fab fa-linkedin mr-1"></i>LinkedIn</label>
<input type="url" class="form-control" id="linkedin-url" name="linkedinUrl"
placeholder="https://linkedin.com/company/your-company">
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="instagram-url"><i class="fab fa-instagram mr-1"></i>Instagram</label>
<input type="url" class="form-control" id="instagram-url" name="instagramUrl"
placeholder="https://instagram.com/your-account">
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label for="youtube-url"><i class="fab fa-youtube mr-1"></i>YouTube</label>
<input type="url" class="form-control" id="youtube-url" name="youtubeUrl"
placeholder="https://youtube.com/channel/your-channel">
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="github-url"><i class="fab fa-github mr-1"></i>GitHub</label>
<input type="url" class="form-control" id="github-url" name="githubUrl"
placeholder="https://github.com/your-account">
</div>
</div>
</div>
</div>
<div class="card-footer">
<button type="submit" class="btn btn-primary">
<i class="fas fa-save mr-1"></i>Сохранить соцсети
</button>
</div>
</form>
</div>
<!-- Maintenance Mode Card -->
<div class="card">
<div class="card-header">
<h3 class="card-title">Режим обслуживания</h3>
</div>
<div class="card-body">
<div class="form-group">
<div class="custom-control custom-switch">
<input type="checkbox" class="custom-control-input" id="maintenance-mode">
<label class="custom-control-label" for="maintenance-mode">
Включить режим обслуживания
</label>
</div>
<small class="form-text text-muted">
В режиме обслуживания сайт будет недоступен для обычных пользователей
</small>
</div>
<div class="form-group">
<label for="maintenance-message">Сообщение для пользователей</label>
<textarea class="form-control" id="maintenance-message" rows="3"
placeholder="Сайт временно недоступен из-за технических работ..."></textarea>
</div>
<button type="button" class="btn btn-warning" onclick="toggleMaintenanceMode()">
<i class="fas fa-tools mr-1"></i>Применить настройки
</button>
</div>
</div>
</div>
</section>
<script>
document.addEventListener('DOMContentLoaded', function() {
loadSettings();
// Setup form submissions
document.getElementById('site-settings-form').addEventListener('submit', saveSiteSettings);
document.getElementById('seo-settings-form').addEventListener('submit', saveSeoSettings);
document.getElementById('social-settings-form').addEventListener('submit', saveSocialSettings);
});
async function loadSettings() {
try {
const response = await fetch('/api/admin/settings');
const data = await response.json();
if (data.success) {
const settings = data.settings;
// Site settings
document.getElementById('site-title').value = settings.siteTitle || '';
document.getElementById('site-tagline').value = settings.siteTagline || '';
document.getElementById('site-description').value = settings.siteDescription || '';
document.getElementById('company-name').value = settings.companyName || '';
document.getElementById('company-email').value = settings.companyEmail || '';
document.getElementById('company-phone').value = settings.companyPhone || '';
document.getElementById('company-address').value = settings.companyAddress || '';
// SEO settings
document.getElementById('meta-keywords').value = settings.metaKeywords || '';
document.getElementById('meta-description').value = settings.metaDescription || '';
document.getElementById('google-analytics').value = settings.googleAnalytics || '';
document.getElementById('google-tag-manager').value = settings.googleTagManager || '';
// Social media
document.getElementById('facebook-url').value = settings.facebookUrl || '';
document.getElementById('twitter-url').value = settings.twitterUrl || '';
document.getElementById('linkedin-url').value = settings.linkedinUrl || '';
document.getElementById('instagram-url').value = settings.instagramUrl || '';
document.getElementById('youtube-url').value = settings.youtubeUrl || '';
document.getElementById('github-url').value = settings.githubUrl || '';
// Maintenance mode
document.getElementById('maintenance-mode').checked = settings.maintenanceMode || false;
document.getElementById('maintenance-message').value = settings.maintenanceMessage || '';
}
} catch (error) {
console.error('Error loading settings:', error);
alert('Ошибка загрузки настроек: ' + error.message);
}
}
async function saveSiteSettings(event) {
event.preventDefault();
const formData = new FormData(event.target);
const settings = {
siteTitle: formData.get('siteTitle'),
siteTagline: formData.get('siteTagline'),
siteDescription: formData.get('siteDescription'),
companyName: formData.get('companyName'),
companyEmail: formData.get('companyEmail'),
companyPhone: formData.get('companyPhone'),
companyAddress: formData.get('companyAddress')
};
await saveSettings(settings, 'Основные настройки сохранены');
}
async function saveSeoSettings(event) {
event.preventDefault();
const formData = new FormData(event.target);
const settings = {
metaKeywords: formData.get('metaKeywords'),
metaDescription: formData.get('metaDescription'),
googleAnalytics: formData.get('googleAnalytics'),
googleTagManager: formData.get('googleTagManager')
};
await saveSettings(settings, 'SEO настройки сохранены');
}
async function saveSocialSettings(event) {
event.preventDefault();
const formData = new FormData(event.target);
const settings = {
facebookUrl: formData.get('facebookUrl'),
twitterUrl: formData.get('twitterUrl'),
linkedinUrl: formData.get('linkedinUrl'),
instagramUrl: formData.get('instagramUrl'),
youtubeUrl: formData.get('youtubeUrl'),
githubUrl: formData.get('githubUrl')
};
await saveSettings(settings, 'Настройки соцсетей сохранены');
}
async function saveSettings(settings, successMessage) {
try {
const response = await fetch('/api/admin/settings', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(settings)
});
const data = await response.json();
if (data.success) {
alert(successMessage);
} else {
throw new Error(data.message);
}
} catch (error) {
console.error('Error saving settings:', error);
alert('Ошибка сохранения настроек: ' + error.message);
}
}
async function toggleMaintenanceMode() {
const isEnabled = document.getElementById('maintenance-mode').checked;
const message = document.getElementById('maintenance-message').value;
const settings = {
maintenanceMode: isEnabled,
maintenanceMessage: message
};
await saveSettings(settings,
isEnabled ? 'Режим обслуживания включен' : 'Режим обслуживания отключен'
);
}
</script>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,360 @@
<!-- Content Header (Page header) -->
<section class="content-header">
<div class="container-fluid">
<div class="row mb-2">
<div class="col-sm-6">
<h1><i class="fab fa-telegram mr-2 text-info"></i>Telegram Bot</h1>
</div>
<div class="col-sm-6">
<ol class="breadcrumb float-sm-right">
<li class="breadcrumb-item"><a href="/admin">Админ</a></li>
<li class="breadcrumb-item active">Telegram Bot</li>
</ol>
</div>
</div>
</div>
</section>
<!-- Main content -->
<section class="content">
<div class="container-fluid">
<!-- Bot Status Card -->
<div class="card">
<div class="card-header">
<h3 class="card-title">Статус бота</h3>
<div class="card-tools">
<button id="refresh-status" class="btn btn-sm btn-default">
<i class="fas fa-sync-alt"></i>
</button>
</div>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<div class="info-box">
<span class="info-box-icon" id="status-icon">
<i class="fas fa-question"></i>
</span>
<div class="info-box-content">
<span class="info-box-text">Статус подключения</span>
<span class="info-box-number" id="status-text">Проверка...</span>
</div>
</div>
</div>
<div class="col-md-6">
<div class="info-box">
<span class="info-box-icon bg-info">
<i class="fab fa-telegram"></i>
</span>
<div class="info-box-content">
<span class="info-box-text">Имя бота</span>
<span class="info-box-number" id="bot-name">Загрузка...</span>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Configuration Card -->
<div class="card">
<div class="card-header">
<h3 class="card-title">Конфигурация</h3>
</div>
<form id="config-form">
<div class="card-body">
<div class="form-group">
<label for="bot-token">Токен бота</label>
<div class="input-group">
<input type="password" class="form-control" id="bot-token" name="botToken" placeholder="Введите токен бота">
<div class="input-group-append">
<button type="button" class="btn btn-outline-secondary" onclick="toggleTokenVisibility()">
<i class="fas fa-eye" id="token-eye"></i>
</button>
</div>
</div>
<small class="form-text text-muted">
Получите токен у <a href="https://t.me/BotFather" target="_blank">@BotFather</a>
</small>
</div>
<div class="form-group">
<label for="admin-chat-id">ID чата администратора</label>
<input type="text" class="form-control" id="admin-chat-id" name="adminChatId" placeholder="Введите ID чата">
<small class="form-text text-muted">
Узнайте ваш ID чата у <a href="https://t.me/userinfobot" target="_blank">@userinfobot</a>
</small>
</div>
<div class="form-group">
<div class="custom-control custom-switch">
<input type="checkbox" class="custom-control-input" id="notifications-enabled" name="notificationsEnabled">
<label class="custom-control-label" for="notifications-enabled">Включить уведомления</label>
</div>
</div>
<div class="form-group">
<label>Типы уведомлений</label>
<div class="row">
<div class="col-md-6">
<div class="custom-control custom-checkbox">
<input type="checkbox" class="custom-control-input" id="notify-contacts" name="notifyContacts">
<label class="custom-control-label" for="notify-contacts">Новые контакты</label>
</div>
<div class="custom-control custom-checkbox">
<input type="checkbox" class="custom-control-input" id="notify-orders" name="notifyOrders">
<label class="custom-control-label" for="notify-orders">Новые заказы</label>
</div>
</div>
<div class="col-md-6">
<div class="custom-control custom-checkbox">
<input type="checkbox" class="custom-control-input" id="notify-errors" name="notifyErrors">
<label class="custom-control-label" for="notify-errors">Системные ошибки</label>
</div>
<div class="custom-control custom-checkbox">
<input type="checkbox" class="custom-control-input" id="notify-updates" name="notifyUpdates">
<label class="custom-control-label" for="notify-updates">Обновления системы</label>
</div>
</div>
</div>
</div>
</div>
<div class="card-footer">
<button type="submit" class="btn btn-primary">
<i class="fas fa-save mr-1"></i>Сохранить
</button>
<button type="button" class="btn btn-secondary ml-2" onclick="testConnection()">
<i class="fas fa-bolt mr-1"></i>Проверить подключение
</button>
</div>
</form>
</div>
<!-- Test Message Card -->
<div class="card">
<div class="card-header">
<h3 class="card-title">Тестовое сообщение</h3>
</div>
<div class="card-body">
<div class="form-group">
<label for="test-message">Сообщение</label>
<textarea class="form-control" id="test-message" rows="3" placeholder="Введите тестовое сообщение...">Тестовое сообщение от SmartSolTech Admin Panel</textarea>
</div>
<button type="button" class="btn btn-info" onclick="sendTestMessage()">
<i class="fas fa-paper-plane mr-1"></i>Отправить тест
</button>
</div>
</div>
<!-- Activity Log Card -->
<div class="card">
<div class="card-header">
<h3 class="card-title">Журнал активности</h3>
<div class="card-tools">
<button class="btn btn-sm btn-default" onclick="clearLog()">
<i class="fas fa-trash"></i> Очистить
</button>
</div>
</div>
<div class="card-body">
<div id="activity-log" style="max-height: 300px; overflow-y: auto;">
<!-- Log entries will be added here -->
</div>
</div>
</div>
</div>
</section>
<script>
let activityLog = [];
document.addEventListener('DOMContentLoaded', function() {
loadConfig();
checkBotStatus();
initializeLog();
document.getElementById('config-form').addEventListener('submit', saveConfig);
});
function addLog(message, type = 'info') {
const timestamp = new Date().toLocaleString('ru-RU');
const logEntry = { timestamp, message, type };
activityLog.unshift(logEntry);
if (activityLog.length > 100) {
activityLog = activityLog.slice(0, 100);
}
updateLogDisplay();
}
function updateLogDisplay() {
const logContainer = document.getElementById('activity-log');
const typeColors = {
info: 'text-info',
success: 'text-success',
error: 'text-danger',
warning: 'text-warning'
};
logContainer.innerHTML = activityLog.map(entry => `
<div class="mb-2">
<small class="text-muted">[${entry.timestamp}]</small>
<span class="${typeColors[entry.type] || 'text-info'}">${entry.message}</span>
</div>
`).join('');
}
function initializeLog() {
addLog('Telegram Bot панель загружена');
}
function clearLog() {
if (confirm('Очистить журнал активности?')) {
activityLog = [];
updateLogDisplay();
addLog('Журнал активности очищен');
}
}
async function loadConfig() {
try {
const response = await fetch('/api/admin/telegram/config');
const data = await response.json();
if (data.success) {
const config = data.config;
document.getElementById('bot-token').value = config.botToken || '';
document.getElementById('admin-chat-id').value = config.adminChatId || '';
document.getElementById('notifications-enabled').checked = config.notificationsEnabled || false;
document.getElementById('notify-contacts').checked = config.notifyContacts || false;
document.getElementById('notify-orders').checked = config.notifyOrders || false;
document.getElementById('notify-errors').checked = config.notifyErrors || false;
document.getElementById('notify-updates').checked = config.notifyUpdates || false;
addLog('Конфигурация загружена');
}
} catch (error) {
console.error('Error loading config:', error);
addLog('Ошибка загрузки конфигурации: ' + error.message, 'error');
}
}
async function saveConfig(event) {
event.preventDefault();
const formData = new FormData(event.target);
const config = {
botToken: formData.get('botToken'),
adminChatId: formData.get('adminChatId'),
notificationsEnabled: formData.has('notificationsEnabled'),
notifyContacts: formData.has('notifyContacts'),
notifyOrders: formData.has('notifyOrders'),
notifyErrors: formData.has('notifyErrors'),
notifyUpdates: formData.has('notifyUpdates')
};
try {
const response = await fetch('/api/admin/telegram/config', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(config)
});
const data = await response.json();
if (data.success) {
addLog('Конфигурация сохранена', 'success');
setTimeout(checkBotStatus, 1000);
} else {
throw new Error(data.message);
}
} catch (error) {
console.error('Error saving config:', error);
addLog('Ошибка сохранения конфигурации: ' + error.message, 'error');
}
}
async function checkBotStatus() {
try {
const response = await fetch('/api/admin/telegram/status');
const data = await response.json();
const statusIcon = document.getElementById('status-icon');
const statusText = document.getElementById('status-text');
const botName = document.getElementById('bot-name');
if (data.success && data.status.connected) {
statusIcon.className = 'info-box-icon bg-success';
statusIcon.innerHTML = '<i class="fas fa-check"></i>';
statusText.textContent = 'Подключен';
botName.textContent = data.status.botInfo.first_name;
addLog('Бот подключен: ' + data.status.botInfo.first_name, 'success');
} else {
statusIcon.className = 'info-box-icon bg-danger';
statusIcon.innerHTML = '<i class="fas fa-times"></i>';
statusText.textContent = 'Не подключен';
botName.textContent = 'Не подключен';
addLog('Бот не подключен: ' + (data.message || 'Неизвестная ошибка'), 'error');
}
} catch (error) {
console.error('Error checking bot status:', error);
const statusIcon = document.getElementById('status-icon');
const statusText = document.getElementById('status-text');
const botName = document.getElementById('bot-name');
statusIcon.className = 'info-box-icon bg-warning';
statusIcon.innerHTML = '<i class="fas fa-exclamation"></i>';
statusText.textContent = 'Ошибка проверки';
botName.textContent = 'Ошибка';
addLog('Ошибка проверки статуса: ' + error.message, 'error');
}
}
async function testConnection() {
addLog('Проверка подключения...');
await checkBotStatus();
}
async function sendTestMessage() {
const message = document.getElementById('test-message').value;
if (!message.trim()) {
alert('Введите сообщение для отправки');
return;
}
try {
addLog('Отправка тестового сообщения...');
const response = await fetch('/api/admin/telegram/test', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message })
});
const data = await response.json();
if (data.success) {
addLog('Тестовое сообщение отправлено', 'success');
} else {
throw new Error(data.message);
}
} catch (error) {
console.error('Error sending test message:', error);
addLog('Ошибка отправки сообщения: ' + error.message, 'error');
}
}
function toggleTokenVisibility() {
const tokenInput = document.getElementById('bot-token');
const tokenEye = document.getElementById('token-eye');
if (tokenInput.type === 'password') {
tokenInput.type = 'text';
tokenEye.className = 'fas fa-eye-slash';
} else {
tokenInput.type = 'password';
tokenEye.className = 'fas fa-eye';
}
}
</script>

View File

@@ -0,0 +1,360 @@
<!-- Content Header (Page header) -->
<section class="content-header">
<div class="container-fluid">
<div class="row mb-2">
<div class="col-sm-6">
<h1><i class="fab fa-telegram mr-2 text-info"></i>Telegram Bot</h1>
</div>
<div class="col-sm-6">
<ol class="breadcrumb float-sm-right">
<li class="breadcrumb-item"><a href="/admin">Админ</a></li>
<li class="breadcrumb-item active">Telegram Bot</li>
</ol>
</div>
</div>
</div>
</section>
<!-- Main content -->
<section class="content">
<div class="container-fluid">
<!-- Bot Status Card -->
<div class="card">
<div class="card-header">
<h3 class="card-title">Статус бота</h3>
<div class="card-tools">
<button id="refresh-status" class="btn btn-sm btn-default">
<i class="fas fa-sync-alt"></i>
</button>
</div>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<div class="info-box">
<span class="info-box-icon" id="status-icon">
<i class="fas fa-question"></i>
</span>
<div class="info-box-content">
<span class="info-box-text">Статус подключения</span>
<span class="info-box-number" id="status-text">Проверка...</span>
</div>
</div>
</div>
<div class="col-md-6">
<div class="info-box">
<span class="info-box-icon bg-info">
<i class="fab fa-telegram"></i>
</span>
<div class="info-box-content">
<span class="info-box-text">Имя бота</span>
<span class="info-box-number" id="bot-name">Загрузка...</span>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Configuration Card -->
<div class="card">
<div class="card-header">
<h3 class="card-title">Конфигурация</h3>
</div>
<form id="config-form">
<div class="card-body">
<div class="form-group">
<label for="bot-token">Токен бота</label>
<div class="input-group">
<input type="password" class="form-control" id="bot-token" name="botToken" placeholder="Введите токен бота">
<div class="input-group-append">
<button type="button" class="btn btn-outline-secondary" onclick="toggleTokenVisibility()">
<i class="fas fa-eye" id="token-eye"></i>
</button>
</div>
</div>
<small class="form-text text-muted">
Получите токен у <a href="https://t.me/BotFather" target="_blank">@BotFather</a>
</small>
</div>
<div class="form-group">
<label for="admin-chat-id">ID чата администратора</label>
<input type="text" class="form-control" id="admin-chat-id" name="adminChatId" placeholder="Введите ID чата">
<small class="form-text text-muted">
Узнайте ваш ID чата у <a href="https://t.me/userinfobot" target="_blank">@userinfobot</a>
</small>
</div>
<div class="form-group">
<div class="custom-control custom-switch">
<input type="checkbox" class="custom-control-input" id="notifications-enabled" name="notificationsEnabled">
<label class="custom-control-label" for="notifications-enabled">Включить уведомления</label>
</div>
</div>
<div class="form-group">
<label>Типы уведомлений</label>
<div class="row">
<div class="col-md-6">
<div class="custom-control custom-checkbox">
<input type="checkbox" class="custom-control-input" id="notify-contacts" name="notifyContacts">
<label class="custom-control-label" for="notify-contacts">Новые контакты</label>
</div>
<div class="custom-control custom-checkbox">
<input type="checkbox" class="custom-control-input" id="notify-orders" name="notifyOrders">
<label class="custom-control-label" for="notify-orders">Новые заказы</label>
</div>
</div>
<div class="col-md-6">
<div class="custom-control custom-checkbox">
<input type="checkbox" class="custom-control-input" id="notify-errors" name="notifyErrors">
<label class="custom-control-label" for="notify-errors">Системные ошибки</label>
</div>
<div class="custom-control custom-checkbox">
<input type="checkbox" class="custom-control-input" id="notify-updates" name="notifyUpdates">
<label class="custom-control-label" for="notify-updates">Обновления системы</label>
</div>
</div>
</div>
</div>
</div>
<div class="card-footer">
<button type="submit" class="btn btn-primary">
<i class="fas fa-save mr-1"></i>Сохранить
</button>
<button type="button" class="btn btn-secondary ml-2" onclick="testConnection()">
<i class="fas fa-bolt mr-1"></i>Проверить подключение
</button>
</div>
</form>
</div>
<!-- Test Message Card -->
<div class="card">
<div class="card-header">
<h3 class="card-title">Тестовое сообщение</h3>
</div>
<div class="card-body">
<div class="form-group">
<label for="test-message">Сообщение</label>
<textarea class="form-control" id="test-message" rows="3" placeholder="Введите тестовое сообщение...">Тестовое сообщение от SmartSolTech Admin Panel</textarea>
</div>
<button type="button" class="btn btn-info" onclick="sendTestMessage()">
<i class="fas fa-paper-plane mr-1"></i>Отправить тест
</button>
</div>
</div>
<!-- Activity Log Card -->
<div class="card">
<div class="card-header">
<h3 class="card-title">Журнал активности</h3>
<div class="card-tools">
<button class="btn btn-sm btn-default" onclick="clearLog()">
<i class="fas fa-trash"></i> Очистить
</button>
</div>
</div>
<div class="card-body">
<div id="activity-log" style="max-height: 300px; overflow-y: auto;">
<!-- Log entries will be added here -->
</div>
</div>
</div>
</div>
</section>
<script>
let activityLog = [];
document.addEventListener('DOMContentLoaded', function() {
loadConfig();
checkBotStatus();
initializeLog();
document.getElementById('config-form').addEventListener('submit', saveConfig);
});
function addLog(message, type = 'info') {
const timestamp = new Date().toLocaleString('ru-RU');
const logEntry = { timestamp, message, type };
activityLog.unshift(logEntry);
if (activityLog.length > 100) {
activityLog = activityLog.slice(0, 100);
}
updateLogDisplay();
}
function updateLogDisplay() {
const logContainer = document.getElementById('activity-log');
const typeColors = {
info: 'text-info',
success: 'text-success',
error: 'text-danger',
warning: 'text-warning'
};
logContainer.innerHTML = activityLog.map(entry => `
<div class="mb-2">
<small class="text-muted">[${entry.timestamp}]</small>
<span class="${typeColors[entry.type] || 'text-info'}">${entry.message}</span>
</div>
`).join('');
}
function initializeLog() {
addLog('Telegram Bot панель загружена');
}
function clearLog() {
if (confirm('Очистить журнал активности?')) {
activityLog = [];
updateLogDisplay();
addLog('Журнал активности очищен');
}
}
async function loadConfig() {
try {
const response = await fetch('/api/admin/telegram/config');
const data = await response.json();
if (data.success) {
const config = data.config;
document.getElementById('bot-token').value = config.botToken || '';
document.getElementById('admin-chat-id').value = config.adminChatId || '';
document.getElementById('notifications-enabled').checked = config.notificationsEnabled || false;
document.getElementById('notify-contacts').checked = config.notifyContacts || false;
document.getElementById('notify-orders').checked = config.notifyOrders || false;
document.getElementById('notify-errors').checked = config.notifyErrors || false;
document.getElementById('notify-updates').checked = config.notifyUpdates || false;
addLog('Конфигурация загружена');
}
} catch (error) {
console.error('Error loading config:', error);
addLog('Ошибка загрузки конфигурации: ' + error.message, 'error');
}
}
async function saveConfig(event) {
event.preventDefault();
const formData = new FormData(event.target);
const config = {
botToken: formData.get('botToken'),
adminChatId: formData.get('adminChatId'),
notificationsEnabled: formData.has('notificationsEnabled'),
notifyContacts: formData.has('notifyContacts'),
notifyOrders: formData.has('notifyOrders'),
notifyErrors: formData.has('notifyErrors'),
notifyUpdates: formData.has('notifyUpdates')
};
try {
const response = await fetch('/api/admin/telegram/config', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(config)
});
const data = await response.json();
if (data.success) {
addLog('Конфигурация сохранена', 'success');
setTimeout(checkBotStatus, 1000);
} else {
throw new Error(data.message);
}
} catch (error) {
console.error('Error saving config:', error);
addLog('Ошибка сохранения конфигурации: ' + error.message, 'error');
}
}
async function checkBotStatus() {
try {
const response = await fetch('/api/admin/telegram/status');
const data = await response.json();
const statusIcon = document.getElementById('status-icon');
const statusText = document.getElementById('status-text');
const botName = document.getElementById('bot-name');
if (data.success && data.status.connected) {
statusIcon.className = 'info-box-icon bg-success';
statusIcon.innerHTML = '<i class="fas fa-check"></i>';
statusText.textContent = 'Подключен';
botName.textContent = data.status.botInfo.first_name;
addLog('Бот подключен: ' + data.status.botInfo.first_name, 'success');
} else {
statusIcon.className = 'info-box-icon bg-danger';
statusIcon.innerHTML = '<i class="fas fa-times"></i>';
statusText.textContent = 'Не подключен';
botName.textContent = 'Не подключен';
addLog('Бот не подключен: ' + (data.message || 'Неизвестная ошибка'), 'error');
}
} catch (error) {
console.error('Error checking bot status:', error);
const statusIcon = document.getElementById('status-icon');
const statusText = document.getElementById('status-text');
const botName = document.getElementById('bot-name');
statusIcon.className = 'info-box-icon bg-warning';
statusIcon.innerHTML = '<i class="fas fa-exclamation"></i>';
statusText.textContent = 'Ошибка проверки';
botName.textContent = 'Ошибка';
addLog('Ошибка проверки статуса: ' + error.message, 'error');
}
}
async function testConnection() {
addLog('Проверка подключения...');
await checkBotStatus();
}
async function sendTestMessage() {
const message = document.getElementById('test-message').value;
if (!message.trim()) {
alert('Введите сообщение для отправки');
return;
}
try {
addLog('Отправка тестового сообщения...');
const response = await fetch('/api/admin/telegram/test', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message })
});
const data = await response.json();
if (data.success) {
addLog('Тестовое сообщение отправлено', 'success');
} else {
throw new Error(data.message);
}
} catch (error) {
console.error('Error sending test message:', error);
addLog('Ошибка отправки сообщения: ' + error.message, 'error');
}
}
function toggleTokenVisibility() {
const tokenInput = document.getElementById('bot-token');
const tokenEye = document.getElementById('token-eye');
if (tokenInput.type === 'password') {
tokenInput.type = 'text';
tokenEye.className = 'fas fa-eye-slash';
} else {
tokenInput.type = 'password';
tokenEye.className = 'fas fa-eye';
}
}
</script>