✨ Features: - Modern tourism website with responsive design - AdminJS admin panel with image editor integration - PostgreSQL database with comprehensive schema - Docker containerization - Image upload and gallery management 🛠 Tech Stack: - Backend: Node.js + Express.js - Database: PostgreSQL 13+ - Frontend: HTML/CSS/JS with responsive design - Admin: AdminJS with custom components - Deployment: Docker + Docker Compose - Image Processing: Sharp with optimization 📱 Admin Features: - Routes/Tours management (city, mountain, fishing) - Guides profiles with specializations - Articles and blog system - Image editor with upload/gallery/URL options - User management and authentication - Responsive admin interface 🎨 Design: - Korean tourism focused branding - Mobile-first responsive design - Custom CSS with modern aesthetics - Image optimization and gallery - SEO-friendly structure 🔒 Security: - Helmet.js security headers - bcrypt password hashing - Input validation and sanitization - CORS protection - Environment variables
314 lines
11 KiB
JavaScript
314 lines
11 KiB
JavaScript
// JavaScript для интеграции редактора изображений в AdminJS
|
||
(function() {
|
||
'use strict';
|
||
|
||
// Функция для открытия редактора изображений
|
||
function openImageEditor(inputField, fieldName) {
|
||
const currentValue = inputField.value || '';
|
||
const editorUrl = `/image-editor-compact.html?field=${fieldName}¤t=${encodeURIComponent(currentValue)}`;
|
||
|
||
// Убираем предыдущие модальные окна
|
||
document.querySelectorAll('.image-editor-modal').forEach(modal => modal.remove());
|
||
|
||
// Создаем модальное окно
|
||
const modal = document.createElement('div');
|
||
modal.className = 'image-editor-modal';
|
||
modal.style.cssText = `
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
background: rgba(0,0,0,0.7);
|
||
z-index: 10000;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
`;
|
||
|
||
const content = document.createElement('div');
|
||
content.style.cssText = `
|
||
background: white;
|
||
border-radius: 8px;
|
||
width: 90%;
|
||
max-width: 700px;
|
||
height: 80%;
|
||
max-height: 600px;
|
||
position: relative;
|
||
box-shadow: 0 8px 32px rgba(0,0,0,0.3);
|
||
`;
|
||
|
||
const closeBtn = document.createElement('button');
|
||
closeBtn.innerHTML = '✕';
|
||
closeBtn.style.cssText = `
|
||
position: absolute;
|
||
top: 10px;
|
||
right: 10px;
|
||
background: #ff4757;
|
||
color: white;
|
||
border: none;
|
||
border-radius: 50%;
|
||
width: 30px;
|
||
height: 30px;
|
||
cursor: pointer;
|
||
z-index: 1;
|
||
font-size: 16px;
|
||
font-weight: bold;
|
||
`;
|
||
|
||
const iframe = document.createElement('iframe');
|
||
iframe.src = editorUrl;
|
||
iframe.style.cssText = `
|
||
width: 100%;
|
||
height: 100%;
|
||
border: none;
|
||
border-radius: 8px;
|
||
`;
|
||
|
||
// Обработчик сообщений от iframe
|
||
const messageHandler = function(event) {
|
||
if (event.data.type === 'imageSelected' && event.data.field === fieldName) {
|
||
console.log('🖼️ Изображение выбрано:', event.data.url);
|
||
inputField.value = event.data.url;
|
||
updateImagePreview(inputField, event.data.url);
|
||
|
||
// Триггерим событие change для обновления формы
|
||
const changeEvent = new Event('change', { bubbles: true });
|
||
inputField.dispatchEvent(changeEvent);
|
||
|
||
// Триггерим input событие
|
||
const inputEvent = new Event('input', { bubbles: true });
|
||
inputField.dispatchEvent(inputEvent);
|
||
|
||
modal.remove();
|
||
window.removeEventListener('message', messageHandler);
|
||
} else if (event.data.type === 'editorClosed') {
|
||
modal.remove();
|
||
window.removeEventListener('message', messageHandler);
|
||
}
|
||
};
|
||
|
||
window.addEventListener('message', messageHandler);
|
||
|
||
closeBtn.onclick = function() {
|
||
modal.remove();
|
||
window.removeEventListener('message', messageHandler);
|
||
};
|
||
|
||
modal.onclick = function(e) {
|
||
if (e.target === modal) {
|
||
modal.remove();
|
||
window.removeEventListener('message', messageHandler);
|
||
}
|
||
};
|
||
|
||
content.appendChild(closeBtn);
|
||
content.appendChild(iframe);
|
||
modal.appendChild(content);
|
||
document.body.appendChild(modal);
|
||
}
|
||
|
||
// Функция обновления превью изображения
|
||
function updateImagePreview(inputField, imagePath) {
|
||
const fieldContainer = inputField.closest('.field, .property-edit, div[data-testid]') || inputField.parentNode;
|
||
if (!fieldContainer) return;
|
||
|
||
// Находим или создаем превью
|
||
let preview = fieldContainer.querySelector('.image-preview');
|
||
|
||
if (!preview) {
|
||
preview = document.createElement('img');
|
||
preview.className = 'image-preview';
|
||
preview.style.cssText = `
|
||
display: block;
|
||
max-width: 180px;
|
||
max-height: 120px;
|
||
object-fit: cover;
|
||
border: 1px solid #ddd;
|
||
border-radius: 4px;
|
||
margin-top: 8px;
|
||
margin-bottom: 8px;
|
||
`;
|
||
|
||
// Вставляем превью после кнопки
|
||
const button = fieldContainer.querySelector('.image-editor-btn');
|
||
if (button) {
|
||
const buttonContainer = button.parentNode;
|
||
buttonContainer.parentNode.insertBefore(preview, buttonContainer.nextSibling);
|
||
} else {
|
||
fieldContainer.appendChild(preview);
|
||
}
|
||
}
|
||
|
||
if (imagePath && imagePath.trim()) {
|
||
preview.src = imagePath + '?t=' + Date.now(); // Добавляем timestamp для обновления
|
||
preview.style.display = 'block';
|
||
preview.onerror = () => {
|
||
preview.style.display = 'none';
|
||
};
|
||
} else {
|
||
preview.style.display = 'none';
|
||
}
|
||
}
|
||
|
||
// Функция добавления кнопки редактора к полю
|
||
function addImageEditorButton(inputField) {
|
||
const fieldName = inputField.name || inputField.id || 'image';
|
||
|
||
// Проверяем, не добавлена ли уже кнопка
|
||
const fieldContainer = inputField.closest('.field, .property-edit, div[data-testid]') || inputField.parentNode;
|
||
if (fieldContainer.querySelector('.image-editor-btn')) {
|
||
return;
|
||
}
|
||
|
||
// Создаем контейнер для кнопки
|
||
const buttonContainer = document.createElement('div');
|
||
buttonContainer.style.cssText = `
|
||
margin-top: 8px;
|
||
display: flex;
|
||
align-items: flex-start;
|
||
gap: 10px;
|
||
`;
|
||
|
||
// Создаем кнопку
|
||
const button = document.createElement('button');
|
||
button.type = 'button';
|
||
button.className = 'image-editor-btn';
|
||
button.innerHTML = '📷 Выбрать';
|
||
button.style.cssText = `
|
||
padding: 6px 12px;
|
||
background: #007bff;
|
||
color: white;
|
||
border: none;
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
font-size: 13px;
|
||
white-space: nowrap;
|
||
`;
|
||
|
||
button.onclick = (e) => {
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
openImageEditor(inputField, fieldName);
|
||
};
|
||
|
||
buttonContainer.appendChild(button);
|
||
|
||
// Добавляем контейнер после поля ввода
|
||
if (inputField.nextSibling) {
|
||
inputField.parentNode.insertBefore(buttonContainer, inputField.nextSibling);
|
||
} else {
|
||
inputField.parentNode.appendChild(buttonContainer);
|
||
}
|
||
|
||
// Добавляем превью если есть значение
|
||
if (inputField.value && inputField.value.trim()) {
|
||
updateImagePreview(inputField, inputField.value);
|
||
}
|
||
|
||
// Слушаем изменения в поле для обновления превью
|
||
inputField.addEventListener('input', () => {
|
||
updateImagePreview(inputField, inputField.value);
|
||
});
|
||
|
||
inputField.addEventListener('change', () => {
|
||
updateImagePreview(inputField, inputField.value);
|
||
});
|
||
}
|
||
|
||
// Функция проверки, является ли поле полем изображения
|
||
function isImageField(inputField) {
|
||
const fieldName = (inputField.name || inputField.id || '').toLowerCase();
|
||
let labelText = '';
|
||
|
||
// Ищем label для поля
|
||
const fieldContainer = inputField.closest('.field, .property-edit, div[data-testid]');
|
||
if (fieldContainer) {
|
||
const label = fieldContainer.querySelector('label, .property-label, h3');
|
||
if (label) {
|
||
labelText = label.textContent.toLowerCase();
|
||
}
|
||
}
|
||
|
||
// Проверяем по имени поля или тексту label
|
||
return fieldName.includes('image') ||
|
||
fieldName.includes('photo') ||
|
||
fieldName.includes('avatar') ||
|
||
fieldName.includes('picture') ||
|
||
fieldName.includes('banner') ||
|
||
fieldName.includes('thumbnail') ||
|
||
(fieldName.includes('url') && (labelText.includes('image') || labelText.includes('изображение'))) ||
|
||
labelText.includes('изображение') ||
|
||
labelText.includes('картинка') ||
|
||
labelText.includes('фото') ||
|
||
labelText.includes('image') ||
|
||
labelText.includes('picture');
|
||
}
|
||
|
||
// Функция сканирования и добавления кнопок к полям изображений
|
||
function scanAndAddImageButtons() {
|
||
console.log('🔍 Сканирование полей для добавления кнопок редактора изображений...');
|
||
|
||
// Более широкий поиск полей ввода
|
||
const inputFields = document.querySelectorAll('input[type="text"], input[type="url"], input:not([type="hidden"]):not([type="submit"]):not([type="button"])');
|
||
|
||
console.log(`📋 Найдено ${inputFields.length} полей ввода`);
|
||
|
||
inputFields.forEach((inputField, index) => {
|
||
const fieldName = inputField.name || inputField.id || `field_${index}`;
|
||
const isImage = isImageField(inputField);
|
||
const fieldContainer = inputField.closest('.field, .property-edit, div[data-testid]') || inputField.parentNode;
|
||
const hasButton = fieldContainer.querySelector('.image-editor-btn');
|
||
|
||
console.log(`🔸 Поле "${fieldName}": isImage=${isImage}, hasButton=${!!hasButton}`);
|
||
|
||
if (isImage && !hasButton) {
|
||
console.log(`➕ Добавляем кнопку для поля "${fieldName}"`);
|
||
addImageEditorButton(inputField);
|
||
}
|
||
});
|
||
}
|
||
|
||
// Инициализация при загрузке DOM
|
||
function initialize() {
|
||
console.log('🚀 Инициализация селектора изображений AdminJS');
|
||
|
||
scanAndAddImageButtons();
|
||
|
||
// Наблюдаем за изменениями в DOM для динамически добавляемых полей
|
||
const observer = new MutationObserver((mutations) => {
|
||
let shouldScan = false;
|
||
mutations.forEach(mutation => {
|
||
if (mutation.type === 'childList') {
|
||
mutation.addedNodes.forEach(node => {
|
||
if (node.nodeType === 1 && (node.tagName === 'INPUT' || node.querySelector('input'))) {
|
||
shouldScan = true;
|
||
}
|
||
});
|
||
}
|
||
});
|
||
|
||
if (shouldScan) {
|
||
setTimeout(scanAndAddImageButtons, 100);
|
||
}
|
||
});
|
||
|
||
observer.observe(document.body, {
|
||
childList: true,
|
||
subtree: true
|
||
});
|
||
}
|
||
|
||
// Ждем загрузки DOM
|
||
if (document.readyState === 'loading') {
|
||
document.addEventListener('DOMContentLoaded', initialize);
|
||
} else {
|
||
initialize();
|
||
}
|
||
|
||
// Также запускаем через задержки для AdminJS
|
||
setTimeout(initialize, 1000);
|
||
setTimeout(initialize, 3000);
|
||
setTimeout(initialize, 5000);
|
||
|
||
})(); |