✨ 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
191 lines
9.4 KiB
HTML
191 lines
9.4 KiB
HTML
<!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> |