- Portfolio CRUD: добавление, редактирование, удаление, переключение публикации - Services CRUD: полное управление услугами с возможностью активации/деактивации - Banner system: новая модель Banner с CRUD операциями и аналитикой кликов - Telegram integration: расширенные настройки бота, обнаружение чатов, отправка сообщений - Media management: улучшенная загрузка файлов с оптимизацией изображений и превью - UI improvements: обновлённые админ-панели с rich-text редактором и drag&drop загрузкой - Database: добавлена таблица banners с полями для баннеров и аналитики
664 lines
32 KiB
Plaintext
664 lines
32 KiB
Plaintext
<!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> |