🚀 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:
191
public/test-image-editor.html
Normal file
191
public/test-image-editor.html
Normal file
@@ -0,0 +1,191 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Тест редактора изображений</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container mt-5">
|
||||
<h1>Тест интеграции редактора изображений</h1>
|
||||
<p>Этот файл демонстрирует, как редактор изображений будет работать с AdminJS.</p>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5>Поле изображения маршрута</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<label for="route_image_url" class="form-label">Изображение маршрута</label>
|
||||
<div class="input-group">
|
||||
<input type="text" id="route_image_url" name="route_image_url" class="form-control"
|
||||
placeholder="/uploads/routes/example.jpg" value="/uploads/routes/seoul-city-tour.jpg">
|
||||
<button type="button" class="btn btn-outline-secondary" onclick="openImageEditor('route_image_url', document.getElementById('route_image_url').value)">
|
||||
<i class="fas fa-images"></i> Выбрать
|
||||
</button>
|
||||
</div>
|
||||
<img id="route_image_url_preview" src="/uploads/routes/seoul-city-tour.jpg"
|
||||
class="img-thumbnail mt-2" style="max-width: 200px; max-height: 200px;">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5>Поле изображения гида</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<label for="guide_image_url" class="form-label">Фотография гида</label>
|
||||
<div class="input-group">
|
||||
<input type="text" id="guide_image_url" name="guide_image_url" class="form-control"
|
||||
placeholder="/uploads/guides/example.jpg" value="/uploads/guides/guide-profile.jpg">
|
||||
<button type="button" class="btn btn-outline-secondary" onclick="openImageEditor('guide_image_url', document.getElementById('guide_image_url').value)">
|
||||
<i class="fas fa-images"></i> Выбрать
|
||||
</button>
|
||||
</div>
|
||||
<img id="guide_image_url_preview" src="/uploads/guides/guide-profile.jpg"
|
||||
class="img-thumbnail mt-2" style="max-width: 200px; max-height: 200px;">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mt-4">
|
||||
<div class="col-md-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5>Доступные изображения</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="imageList" class="row">
|
||||
<!-- Будет заполнено динамически -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mt-4">
|
||||
<div class="col-md-12">
|
||||
<div class="alert alert-info">
|
||||
<h6>Как использовать:</h6>
|
||||
<ol>
|
||||
<li>Нажмите кнопку "Выбрать" рядом с полем изображения</li>
|
||||
<li>Откроется редактор изображений в новом окне</li>
|
||||
<li>Выберите изображение из галереи, загрузите новое или укажите URL</li>
|
||||
<li>Нажмите "Выбрать" в редакторе</li>
|
||||
<li>Поле автоматически обновится с выбранным путем</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/js/all.min.js"></script>
|
||||
|
||||
<script>
|
||||
// Функция для открытия редактора изображений
|
||||
function openImageEditor(fieldName, currentValue) {
|
||||
const editorUrl = `/image-editor.html?field=${fieldName}¤t=${encodeURIComponent(currentValue || '')}`;
|
||||
const editorWindow = window.open(editorUrl, 'imageEditor', 'width=1200,height=800,scrollbars=yes,resizable=yes');
|
||||
|
||||
// Слушаем сообщения от редактора
|
||||
const messageHandler = (event) => {
|
||||
if (event.origin !== window.location.origin) return;
|
||||
|
||||
if (event.data.type === 'imageSelected' && event.data.targetField === fieldName) {
|
||||
const field = document.getElementById(fieldName);
|
||||
const preview = document.getElementById(fieldName + '_preview');
|
||||
|
||||
if (field) {
|
||||
field.value = event.data.path;
|
||||
field.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
}
|
||||
|
||||
if (preview) {
|
||||
preview.src = event.data.path;
|
||||
}
|
||||
|
||||
window.removeEventListener('message', messageHandler);
|
||||
editorWindow.close();
|
||||
|
||||
showSuccess(`Изображение обновлено: ${event.data.path}`);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('message', messageHandler);
|
||||
|
||||
// Очистка обработчика при закрытии окна
|
||||
const checkClosed = setInterval(() => {
|
||||
if (editorWindow.closed) {
|
||||
window.removeEventListener('message', messageHandler);
|
||||
clearInterval(checkClosed);
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
// Функция показа уведомления об успехе
|
||||
function showSuccess(message) {
|
||||
const alert = document.createElement('div');
|
||||
alert.className = 'alert alert-success alert-dismissible fade show position-fixed top-0 start-50 translate-middle-x mt-3';
|
||||
alert.style.zIndex = '9999';
|
||||
alert.innerHTML = `
|
||||
${message}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
`;
|
||||
document.body.appendChild(alert);
|
||||
|
||||
setTimeout(() => {
|
||||
if (alert.parentNode) {
|
||||
alert.parentNode.removeChild(alert);
|
||||
}
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
// Загрузка списка изображений
|
||||
async function loadImageList() {
|
||||
try {
|
||||
const response = await fetch('/api/images/gallery?folder=all');
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
renderImageList(result.data);
|
||||
} else {
|
||||
document.getElementById('imageList').innerHTML = '<p class="text-muted">Ошибка загрузки изображений</p>';
|
||||
}
|
||||
} catch (error) {
|
||||
document.getElementById('imageList').innerHTML = '<p class="text-muted">Ошибка: ' + error.message + '</p>';
|
||||
}
|
||||
}
|
||||
|
||||
// Отображение списка изображений
|
||||
function renderImageList(images) {
|
||||
const container = document.getElementById('imageList');
|
||||
|
||||
if (images.length === 0) {
|
||||
container.innerHTML = '<p class="text-muted">Изображения не найдены</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = images.slice(0, 12).map(img => `
|
||||
<div class="col-md-2 col-sm-3 col-4 mb-3">
|
||||
<div class="card h-100">
|
||||
<img src="${img.path}" class="card-img-top" style="height: 100px; object-fit: cover;" alt="${img.name}">
|
||||
<div class="card-body p-2">
|
||||
<small class="card-title text-truncate d-block">${img.name}</small>
|
||||
<small class="text-muted">${img.folder}</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
// Загружаем изображения при загрузке страницы
|
||||
document.addEventListener('DOMContentLoaded', loadImageList);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user