🚀 Korea Tourism Agency - Complete implementation
✨ 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
This commit is contained in:
314
public/js/admin-image-selector-fixed.js
Normal file
314
public/js/admin-image-selector-fixed.js
Normal file
@@ -0,0 +1,314 @@
|
||||
// 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);
|
||||
|
||||
})();
|
||||
Reference in New Issue
Block a user