Files
sst_site/views/admin/banner-editor.ejs
Andrey K. Choi 9477ff6de0 feat: Реализован полный CRUD для админ-панели и улучшена функциональность
- Portfolio CRUD: добавление, редактирование, удаление, переключение публикации
- Services CRUD: полное управление услугами с возможностью активации/деактивации
- Banner system: новая модель Banner с CRUD операциями и аналитикой кликов
- Telegram integration: расширенные настройки бота, обнаружение чатов, отправка сообщений
- Media management: улучшенная загрузка файлов с оптимизацией изображений и превью
- UI improvements: обновлённые админ-панели с rich-text редактором и drag&drop загрузкой
- Database: добавлена таблица banners с полями для баннеров и аналитики
2025-10-22 20:32:16 +09:00

664 lines
32 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Редактор Баннеров - SmartSolTech Admin</title>
<!-- Tailwind CSS -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- Font Awesome -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<!-- Custom CSS -->
<link rel="stylesheet" href="/css/main.css">
<link rel="stylesheet" href="/css/fixes.css">
<style>
.upload-zone {
border: 2px dashed #d1d5db;
transition: all 0.3s ease;
}
.upload-zone.dragover {
border-color: #3b82f6;
background-color: #eff6ff;
}
.image-preview {
position: relative;
overflow: hidden;
border-radius: 8px;
}
.image-preview:hover .overlay {
opacity: 1;
}
.overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.7);
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: opacity 0.3s ease;
}
</style>
</head>
<body class="bg-gray-100">
<!-- Admin Header -->
<header class="bg-white shadow-sm border-b">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between items-center h-16">
<div class="flex items-center">
<h1 class="text-xl font-semibold text-gray-900">
<i class="fas fa-cogs mr-2"></i>
SmartSolTech Admin
</h1>
</div>
<div class="flex items-center space-x-4">
<span class="text-sm text-gray-600">
Добро пожаловать, <%= user ? user.name : 'Admin' %>!
</span>
<a href="/" class="text-gray-500 hover:text-gray-700">
<i class="fas fa-external-link-alt mr-1"></i>
Посмотреть сайт
</a>
<form action="/admin/logout" method="post" class="inline">
<button type="submit" class="text-red-600 hover:text-red-800">
<i class="fas fa-sign-out-alt mr-1"></i>
Выход
</button>
</form>
</div>
</div>
</div>
</header>
<div class="flex">
<!-- Admin Sidebar -->
<aside class="w-64 bg-white shadow-sm admin-sidebar min-h-screen">
<nav class="mt-5 px-2">
<div class="space-y-1">
<a href="/admin/dashboard" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-600 hover:bg-gray-50 hover:text-gray-900">
<i class="fas fa-tachometer-alt mr-3"></i>
Панель управления
</a>
<a href="/admin/portfolio" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-600 hover:bg-gray-50 hover:text-gray-900">
<i class="fas fa-briefcase mr-3"></i>
Портфолио
</a>
<a href="/admin/services" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-600 hover:bg-gray-50 hover:text-gray-900">
<i class="fas fa-cog mr-3"></i>
Услуги
</a>
<a href="/admin/contacts" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-600 hover:bg-gray-50 hover:text-gray-900">
<i class="fas fa-envelope mr-3"></i>
Сообщения
</a>
<a href="/admin/media" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-600 hover:bg-gray-50 hover:text-gray-900">
<i class="fas fa-images mr-3"></i>
Медиа
</a>
<a href="/admin/settings" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-600 hover:bg-gray-50 hover:text-gray-900">
<i class="fas fa-cogs mr-3"></i>
Настройки
</a>
<a href="/admin/telegram" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-600 hover:bg-gray-50 hover:text-gray-900">
<i class="fab fa-telegram mr-3"></i>
Telegram Bot
</a>
<a href="/admin/banner-editor" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md bg-blue-100 text-blue-700">
<i class="fas fa-paint-brush mr-3"></i>
Редактор баннеров
</a>
</div>
</nav>
</aside>
<!-- Main Content -->
<main class="flex-1 p-8">
<div class="space-y-6">
<!-- Header -->
<div class="bg-white shadow rounded-lg p-6">
<h1 class="text-2xl font-bold text-gray-900 flex items-center">
<i class="fas fa-paint-brush mr-3 text-blue-600"></i>
Редактор Баннеров
</h1>
<p class="mt-2 text-gray-600">Создание и редактирование баннеров для сайта</p>
</div>
<!-- Controls -->
<div class="bg-white shadow rounded-lg p-6">
<div class="flex justify-between items-center">
<h3 class="text-lg font-medium text-gray-900">Инструменты</h3>
<div class="flex space-x-4">
<button id="refresh-images" class="px-4 py-2 bg-gray-500 text-white rounded hover:bg-gray-600 transition-colors">
<i class="fas fa-sync-alt mr-2"></i>
Обновить
</button>
<button id="upload-modal-btn" class="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors">
<i class="fas fa-plus mr-2"></i>
Загрузить Изображения
</button>
</div>
</div>
</div>
<!-- Banner Editor -->
<div class="bg-white shadow rounded-lg p-6">
<!-- Banner Types Tabs -->
<div class="mb-8">
<div class="border-b border-gray-200 dark:border-gray-700">
<nav class="-mb-px flex space-x-8">
<button class="banner-tab active py-2 px-1 border-b-2 border-blue-500 font-medium text-sm text-blue-600" data-page="home">
Главная страница
</button>
<button class="banner-tab py-2 px-1 border-b-2 border-transparent font-medium text-sm text-gray-500 hover:text-gray-700 hover:border-gray-300" data-page="about">
О нас
</button>
<button class="banner-tab py-2 px-1 border-b-2 border-transparent font-medium text-sm text-gray-500 hover:text-gray-700 hover:border-gray-300" data-page="services">
Услуги
</button>
<button class="banner-tab py-2 px-1 border-b-2 border-transparent font-medium text-sm text-gray-500 hover:text-gray-700 hover:border-gray-300" data-page="portfolio">
Портфолио
</button>
</nav>
</div>
</div>
<!-- Current Banner Display -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-lg p-6 mb-8">
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-4">
Текущий баннер: <span id="current-page">Главная страница</span>
</h2>
<div id="current-banner" class="relative">
<div class="w-full h-64 bg-gradient-to-br from-blue-900 via-purple-900 to-indigo-900 rounded-lg flex items-center justify-center">
<div class="text-center text-white">
<h3 class="text-4xl font-bold mb-2">Текущий Баннер</h3>
<p class="text-xl opacity-90">Нажмите на изображение ниже, чтобы заменить</p>
</div>
</div>
<div class="mt-4 flex justify-between items-center">
<div class="text-sm text-gray-600 dark:text-gray-400">
<span id="banner-info">Используется CSS градиент</span>
</div>
<button id="remove-banner" class="px-3 py-1 bg-red-500 text-white rounded hover:bg-red-600 transition-colors text-sm" style="display: none;">
<i class="fas fa-trash mr-1"></i>
Удалить
</button>
</div>
</div>
</div>
<!-- Image Gallery -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-lg p-6">
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-6">
Галерея изображений
</h2>
<div id="loading" class="text-center py-8" style="display: none;">
<div class="inline-block animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500"></div>
<p class="mt-2 text-gray-600 dark:text-gray-400">Загрузка...</p>
</div>
<div id="images-grid" class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
<!-- Изображения будут загружены динамически -->
</div>
<div id="no-images" class="text-center py-8" style="display: none;">
<i class="fas fa-images text-4xl text-gray-400 mb-4"></i>
<p class="text-gray-600 dark:text-gray-400">Нет загруженных изображений</p>
<button class="mt-4 px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors" onclick="document.getElementById('upload-modal-btn').click()">
Загрузить первое изображение
</button>
</div>
</div>
</div>
</div>
</div>
<!-- Upload Modal -->
<div id="upload-modal" class="fixed inset-0 bg-black bg-opacity-50 z-50 hidden">
<div class="flex items-center justify-center min-h-screen p-4">
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-lg max-w-2xl w-full max-h-[90vh] overflow-y-auto">
<div class="p-6">
<div class="flex justify-between items-center mb-6">
<h3 class="text-xl font-semibold text-gray-900 dark:text-white">Загрузить изображения</h3>
<button id="close-modal" class="text-gray-400 hover:text-gray-600">
<i class="fas fa-times text-xl"></i>
</button>
</div>
<!-- Upload Zone -->
<div id="upload-zone" class="upload-zone rounded-lg p-8 text-center mb-6">
<div class="mb-4">
<i class="fas fa-cloud-upload-alt text-4xl text-gray-400 mb-2"></i>
<p class="text-lg text-gray-600 dark:text-gray-400 mb-2">
Перетащите изображения сюда или нажмите для выбора
</p>
<p class="text-sm text-gray-500">
Поддерживаются: JPG, PNG, GIF, WebP (максимум 10MB каждое)
</p>
</div>
<input type="file" id="file-input" multiple accept="image/*" class="hidden">
<button type="button" onclick="document.getElementById('file-input').click()" class="px-6 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors">
Выбрать файлы
</button>
</div>
<!-- Upload Progress -->
<div id="upload-progress" class="hidden mb-6">
<div class="bg-gray-200 rounded-full h-2 mb-2">
<div id="progress-bar" class="bg-blue-500 h-2 rounded-full transition-all duration-300" style="width: 0%"></div>
</div>
<p id="progress-text" class="text-sm text-gray-600 dark:text-gray-400 text-center">Загрузка...</p>
</div>
<!-- Preview Area -->
<div id="preview-area" class="grid grid-cols-2 md:grid-cols-3 gap-4 mb-6" style="display: none;">
<!-- Previews will be added here -->
</div>
<!-- Upload Button -->
<div class="flex justify-end">
<button id="upload-btn" class="px-6 py-2 bg-green-500 text-white rounded hover:bg-green-600 transition-colors disabled:opacity-50 disabled:cursor-not-allowed" disabled>
<i class="fas fa-upload mr-2"></i>
Загрузить
</button>
</div>
</div>
</div>
</div>
</div>
<!-- Scripts -->
<script>
class BannerEditor {
constructor() {
this.currentPage = 'home';
this.selectedFiles = [];
this.bannerSettings = {
home: { type: 'gradient', image: null },
about: { type: 'gradient', image: null },
services: { type: 'gradient', image: null },
portfolio: { type: 'gradient', image: null }
};
this.init();
}
init() {
this.loadImages();
this.setupEventListeners();
this.loadBannerSettings();
}
setupEventListeners() {
// Tab switching
document.querySelectorAll('.banner-tab').forEach(tab => {
tab.addEventListener('click', (e) => {
const page = e.target.dataset.page;
this.switchPage(page);
});
});
// Upload modal
document.getElementById('upload-modal-btn').addEventListener('click', () => {
document.getElementById('upload-modal').classList.remove('hidden');
});
document.getElementById('close-modal').addEventListener('click', () => {
this.closeModal();
});
// File upload
const fileInput = document.getElementById('file-input');
const uploadZone = document.getElementById('upload-zone');
fileInput.addEventListener('change', (e) => {
this.handleFiles(e.target.files);
});
// Drag and drop
uploadZone.addEventListener('dragover', (e) => {
e.preventDefault();
uploadZone.classList.add('dragover');
});
uploadZone.addEventListener('dragleave', () => {
uploadZone.classList.remove('dragover');
});
uploadZone.addEventListener('drop', (e) => {
e.preventDefault();
uploadZone.classList.remove('dragover');
this.handleFiles(e.dataTransfer.files);
});
// Upload button
document.getElementById('upload-btn').addEventListener('click', () => {
this.uploadFiles();
});
// Refresh images
document.getElementById('refresh-images').addEventListener('click', () => {
this.loadImages();
});
}
switchPage(page) {
this.currentPage = page;
// Update active tab
document.querySelectorAll('.banner-tab').forEach(tab => {
tab.classList.remove('active', 'border-blue-500', 'text-blue-600');
tab.classList.add('border-transparent', 'text-gray-500');
});
document.querySelector(`[data-page="${page}"]`).classList.add('active', 'border-blue-500', 'text-blue-600');
document.querySelector(`[data-page="${page}"]`).classList.remove('border-transparent', 'text-gray-500');
// Update current page display
const pageNames = {
home: 'Главная страница',
about: 'О нас',
services: 'Услуги',
portfolio: 'Портфолио'
};
document.getElementById('current-page').textContent = pageNames[page];
this.updateCurrentBanner();
}
updateCurrentBanner() {
const banner = this.bannerSettings[this.currentPage];
const bannerElement = document.getElementById('current-banner').querySelector('div');
const infoElement = document.getElementById('banner-info');
const removeBtn = document.getElementById('remove-banner');
if (banner.image) {
bannerElement.style.backgroundImage = `url(${banner.image})`;
bannerElement.style.backgroundSize = 'cover';
bannerElement.style.backgroundPosition = 'center';
bannerElement.innerHTML = '';
infoElement.textContent = `Изображение: ${banner.image.split('/').pop()}`;
removeBtn.style.display = 'block';
} else {
bannerElement.style.backgroundImage = '';
bannerElement.className = 'w-full h-64 bg-gradient-to-br from-blue-900 via-purple-900 to-indigo-900 rounded-lg flex items-center justify-center';
bannerElement.innerHTML = `
<div class="text-center text-white">
<h3 class="text-4xl font-bold mb-2">Текущий Баннер</h3>
<p class="text-xl opacity-90">Нажмите на изображение ниже, чтобы заменить</p>
</div>
`;
infoElement.textContent = 'Используется CSS градиент';
removeBtn.style.display = 'none';
}
}
async loadImages() {
const loading = document.getElementById('loading');
const grid = document.getElementById('images-grid');
const noImages = document.getElementById('no-images');
loading.style.display = 'block';
grid.innerHTML = '';
noImages.style.display = 'none';
try {
const response = await fetch('/media/list');
const data = await response.json();
if (data.success && data.images.length > 0) {
grid.innerHTML = '';
data.images.forEach(image => {
this.addImageToGrid(image);
});
} else {
noImages.style.display = 'block';
}
} catch (error) {
console.error('Error loading images:', error);
this.showNotification('Ошибка загрузки изображений', 'error');
} finally {
loading.style.display = 'none';
}
}
addImageToGrid(image) {
const grid = document.getElementById('images-grid');
const imageElement = document.createElement('div');
imageElement.className = 'image-preview bg-gray-200 rounded-lg cursor-pointer';
imageElement.innerHTML = `
<img src="${image.url}" alt="${image.filename}" class="w-full h-32 object-cover rounded-lg">
<div class="overlay">
<div class="flex space-x-2">
<button class="use-image px-3 py-1 bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors text-sm" data-url="${image.url}">
<i class="fas fa-check mr-1"></i>
Использовать
</button>
<button class="delete-image px-3 py-1 bg-red-500 text-white rounded hover:bg-red-600 transition-colors text-sm" data-filename="${image.filename}">
<i class="fas fa-trash mr-1"></i>
Удалить
</button>
</div>
</div>
<div class="p-2">
<p class="text-xs text-gray-600 truncate">${image.filename}</p>
<p class="text-xs text-gray-500">${this.formatFileSize(image.size)}</p>
</div>
`;
// Add event listeners
imageElement.querySelector('.use-image').addEventListener('click', (e) => {
e.stopPropagation();
this.setImageAsBanner(image.url);
});
imageElement.querySelector('.delete-image').addEventListener('click', (e) => {
e.stopPropagation();
this.deleteImage(image.filename);
});
grid.appendChild(imageElement);
}
setImageAsBanner(imageUrl) {
this.bannerSettings[this.currentPage] = {
type: 'image',
image: imageUrl
};
this.updateCurrentBanner();
this.saveBannerSettings();
this.showNotification('Баннер обновлен!', 'success');
}
async deleteImage(filename) {
if (!confirm('Вы уверены, что хотите удалить это изображение?')) {
return;
}
try {
const response = await fetch(`/media/${filename}`, {
method: 'DELETE'
});
if (response.ok) {
this.loadImages();
this.showNotification('Изображение удалено', 'success');
} else {
throw new Error('Failed to delete image');
}
} catch (error) {
console.error('Error deleting image:', error);
this.showNotification('Ошибка удаления изображения', 'error');
}
}
handleFiles(files) {
this.selectedFiles = Array.from(files).filter(file => {
if (file.type.startsWith('image/')) {
if (file.size <= 10 * 1024 * 1024) { // 10MB
return true;
} else {
this.showNotification(`Файл ${file.name} слишком большой (максимум 10MB)`, 'error');
}
} else {
this.showNotification(`Файл ${file.name} не является изображением`, 'error');
}
return false;
});
this.showPreviews();
document.getElementById('upload-btn').disabled = this.selectedFiles.length === 0;
}
showPreviews() {
const previewArea = document.getElementById('preview-area');
previewArea.innerHTML = '';
previewArea.style.display = this.selectedFiles.length > 0 ? 'grid' : 'none';
this.selectedFiles.forEach((file, index) => {
const reader = new FileReader();
reader.onload = (e) => {
const preview = document.createElement('div');
preview.className = 'relative';
preview.innerHTML = `
<img src="${e.target.result}" alt="${file.name}" class="w-full h-24 object-cover rounded">
<button class="absolute top-1 right-1 bg-red-500 text-white rounded-full w-6 h-6 flex items-center justify-center text-xs hover:bg-red-600" onclick="bannerEditor.removePreview(${index})">
<i class="fas fa-times"></i>
</button>
<p class="text-xs text-gray-600 mt-1 truncate">${file.name}</p>
`;
previewArea.appendChild(preview);
};
reader.readAsDataURL(file);
});
}
removePreview(index) {
this.selectedFiles.splice(index, 1);
this.showPreviews();
document.getElementById('upload-btn').disabled = this.selectedFiles.length === 0;
}
async uploadFiles() {
if (this.selectedFiles.length === 0) return;
const progressContainer = document.getElementById('upload-progress');
const progressBar = document.getElementById('progress-bar');
const progressText = document.getElementById('progress-text');
const uploadBtn = document.getElementById('upload-btn');
progressContainer.classList.remove('hidden');
uploadBtn.disabled = true;
const formData = new FormData();
this.selectedFiles.forEach(file => {
formData.append('images', file);
});
try {
const xhr = new XMLHttpRequest();
xhr.upload.addEventListener('progress', (e) => {
if (e.lengthComputable) {
const percentComplete = (e.loaded / e.total) * 100;
progressBar.style.width = percentComplete + '%';
progressText.textContent = `Загрузка... ${Math.round(percentComplete)}%`;
}
});
xhr.addEventListener('load', () => {
if (xhr.status === 200) {
const response = JSON.parse(xhr.responseText);
if (response.success) {
this.showNotification('Изображения загружены успешно!', 'success');
this.closeModal();
this.loadImages();
} else {
throw new Error(response.message);
}
} else {
throw new Error('Upload failed');
}
});
xhr.addEventListener('error', () => {
throw new Error('Network error');
});
xhr.open('POST', '/media/upload-multiple');
xhr.send(formData);
} catch (error) {
console.error('Upload error:', error);
this.showNotification('Ошибка загрузки изображений', 'error');
} finally {
progressContainer.classList.add('hidden');
uploadBtn.disabled = false;
progressBar.style.width = '0%';
}
}
closeModal() {
document.getElementById('upload-modal').classList.add('hidden');
this.selectedFiles = [];
document.getElementById('preview-area').style.display = 'none';
document.getElementById('upload-btn').disabled = true;
document.getElementById('upload-progress').classList.add('hidden');
}
loadBannerSettings() {
const saved = localStorage.getItem('bannerSettings');
if (saved) {
this.bannerSettings = JSON.parse(saved);
}
this.updateCurrentBanner();
}
saveBannerSettings() {
localStorage.setItem('bannerSettings', JSON.stringify(this.bannerSettings));
}
formatFileSize(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
showNotification(message, type = 'info') {
// Simple notification - you can enhance this with a proper notification system
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);
}
}
// Initialize when page loads
let bannerEditor;
document.addEventListener('DOMContentLoaded', () => {
bannerEditor = new BannerEditor();
});
</script>
</div>
</main>
</div>
</div>
</body>
</html>