🚀 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:
2025-11-30 00:53:15 +09:00
parent ed871fc4d1
commit b4e513e996
36 changed files with 6894 additions and 239 deletions

View 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}&current=${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>