Files
sst_site/.history/views/admin/banner-editor_20251020225453.ejs
2025-10-22 21:22:44 +09:00

596 lines
28 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="<%= locale || 'ko' %>" class="<%= theme === 'dark' ? 'dark' : '' %>">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Редактор Баннеров - SmartSolTech Admin</title>
<!-- Styles -->
<link href="https://cdn.tailwindcss.com" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" rel="stylesheet">
<link href="/css/main.css" rel="stylesheet">
<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 dark:bg-gray-900">
<div class="flex">
<!-- Sidebar -->
<div class="w-64 bg-white dark:bg-gray-800 shadow-lg h-screen fixed">
<div class="p-6">
<h2 class="text-xl font-bold text-gray-800 dark:text-white mb-6">Админ Панель</h2>
<nav class="space-y-2">
<a href="/admin" class="flex items-center px-4 py-2 text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded">
<i class="fas fa-tachometer-alt mr-3"></i>
Дашборд
</a>
<a href="/admin/banner-editor" class="flex items-center px-4 py-2 bg-blue-500 text-white rounded">
<i class="fas fa-image mr-3"></i>
Редактор Баннеров
</a>
<a href="/admin/portfolio" class="flex items-center px-4 py-2 text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded">
<i class="fas fa-briefcase mr-3"></i>
Портфолио
</a>
</nav>
</div>
</div>
<!-- Main Content -->
<div class="flex-1 ml-64 p-8">
<div class="max-w-6xl mx-auto">
<div class="flex justify-between items-center mb-8">
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">
<i class="fas fa-image text-blue-500 mr-3"></i>
Редактор Баннеров
</h1>
<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>
<!-- 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>
</body>
</html>