🚀 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:
300
public/js/admin-image-selector.js
Normal file
300
public/js/admin-image-selector.js
Normal file
@@ -0,0 +1,300 @@
|
||||
// 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: 15px;
|
||||
right: 15px;
|
||||
background: #ff4757;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
width: 35px;
|
||||
height: 35px;
|
||||
cursor: pointer;
|
||||
z-index: 1;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
`;
|
||||
|
||||
const iframe = document.createElement('iframe');
|
||||
iframe.src = editorUrl;
|
||||
iframe.style.cssText = `
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
`;
|
||||
|
||||
// Обработчик закрытия
|
||||
const closeModal = () => {
|
||||
document.body.removeChild(modal);
|
||||
window.removeEventListener('message', handleMessage);
|
||||
};
|
||||
|
||||
closeBtn.onclick = closeModal;
|
||||
|
||||
// Обработчик сообщений от редактора
|
||||
const handleMessage = (event) => {
|
||||
if (event.origin !== window.location.origin) return;
|
||||
|
||||
if (event.data.type === 'imageSelected' && event.data.targetField === fieldName) {
|
||||
inputField.value = event.data.path;
|
||||
|
||||
// Триггерим события изменения
|
||||
inputField.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
inputField.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
|
||||
// Обновляем превью если есть
|
||||
updateImagePreview(inputField, event.data.path);
|
||||
|
||||
closeModal();
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('message', handleMessage);
|
||||
|
||||
content.appendChild(closeBtn);
|
||||
content.appendChild(iframe);
|
||||
modal.appendChild(content);
|
||||
document.body.appendChild(modal);
|
||||
|
||||
// Закрытие по клику на фон
|
||||
modal.addEventListener('click', (e) => {
|
||||
if (e.target === modal) {
|
||||
closeModal();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Функция обновления превью изображения
|
||||
function updateImagePreview(inputField, imagePath) {
|
||||
const fieldContainer = inputField.closest('.field, .property-edit, div[data-testid]');
|
||||
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 && button.nextSibling) {
|
||||
button.parentNode.insertBefore(preview, button.nextSibling);
|
||||
} else {
|
||||
inputField.parentNode.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 hasButton = inputField.parentNode.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(() => {
|
||||
scanAndAddImageButtons();
|
||||
});
|
||||
|
||||
observer.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true
|
||||
});
|
||||
|
||||
// Периодическое сканирование для надежности
|
||||
setInterval(scanAndAddImageButtons, 2000);
|
||||
}
|
||||
|
||||
// Ждем загрузки 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