Files
tourrism_site/public/image-editor.html
Andrey K. Choi b4e513e996 🚀 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
2025-11-30 00:53:15 +09:00

448 lines
21 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Редактор изображений - Korea Tourism</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
<style>
.image-preview {
max-width: 200px;
max-height: 200px;
object-fit: cover;
border: 2px dashed #ddd;
border-radius: 8px;
}
.image-gallery {
max-height: 400px;
overflow-y: auto;
}
.gallery-item {
cursor: pointer;
transition: transform 0.2s;
}
.gallery-item:hover {
transform: scale(1.05);
}
.gallery-item.selected {
border: 3px solid #007bff;
border-radius: 8px;
}
.upload-area {
border: 2px dashed #ccc;
border-radius: 10px;
padding: 2rem;
text-align: center;
background: #f8f9fa;
transition: border-color 0.3s;
}
.upload-area:hover, .upload-area.drag-over {
border-color: #007bff;
background: #e3f2fd;
}
</style>
</head>
<body>
<div class="container-fluid py-4">
<div class="row">
<div class="col-md-12">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">
<i class="fas fa-images me-2"></i>
Редактор изображений
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="card-body">
<!-- Вкладки -->
<ul class="nav nav-tabs" id="imageEditorTabs" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="upload-tab" data-bs-toggle="tab" data-bs-target="#upload" type="button">
<i class="fas fa-upload me-2"></i>Загрузить
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="gallery-tab" data-bs-toggle="tab" data-bs-target="#gallery" type="button">
<i class="fas fa-images me-2"></i>Галерея
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="url-tab" data-bs-toggle="tab" data-bs-target="#url" type="button">
<i class="fas fa-link me-2"></i>По ссылке
</button>
</li>
</ul>
<div class="tab-content" id="imageEditorTabsContent">
<!-- Вкладка загрузки -->
<div class="tab-pane fade show active" id="upload" role="tabpanel">
<div class="row mt-3">
<div class="col-md-6">
<div class="upload-area" id="uploadArea">
<i class="fas fa-cloud-upload-alt fa-3x text-muted mb-3"></i>
<p class="mb-2">Перетащите файлы сюда или нажмите для выбора</p>
<small class="text-muted">Поддерживаются: JPG, PNG, GIF (макс. 5МБ)</small>
<input type="file" id="fileInput" class="d-none" multiple accept="image/*">
</div>
<!-- Прогресс загрузки -->
<div id="uploadProgress" class="mt-3" style="display: none;">
<div class="progress">
<div class="progress-bar" role="progressbar" style="width: 0%"></div>
</div>
</div>
</div>
<div class="col-md-6">
<div class="text-center">
<h6>Предварительный просмотр</h6>
<img id="uploadPreview" class="image-preview" src="/images/placeholders/no-image.png" alt="Предварительный просмотр">
<div class="mt-2">
<small id="imageInfo" class="text-muted"></small>
</div>
</div>
</div>
</div>
</div>
<!-- Вкладка галереи -->
<div class="tab-pane fade" id="gallery" role="tabpanel">
<div class="row mt-3">
<div class="col-md-12">
<div class="d-flex justify-content-between align-items-center mb-3">
<h6>Загруженные изображения</h6>
<div class="btn-group">
<button type="button" class="btn btn-outline-secondary btn-sm" data-folder="routes">
<i class="fas fa-route me-1"></i>Маршруты
</button>
<button type="button" class="btn btn-outline-secondary btn-sm" data-folder="guides">
<i class="fas fa-user-tie me-1"></i>Гиды
</button>
<button type="button" class="btn btn-outline-secondary btn-sm" data-folder="articles">
<i class="fas fa-newspaper me-1"></i>Статьи
</button>
<button type="button" class="btn btn-outline-secondary btn-sm active" data-folder="all">
<i class="fas fa-images me-1"></i>Все
</button>
</div>
</div>
<div class="image-gallery">
<div class="row" id="galleryImages">
<!-- Изображения будут загружены динамически -->
<div class="col-12 text-center py-4">
<i class="fas fa-spinner fa-spin fa-2x text-muted"></i>
<p class="mt-2 text-muted">Загрузка галереи...</p>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Вкладка URL -->
<div class="tab-pane fade" id="url" role="tabpanel">
<div class="row mt-3">
<div class="col-md-8">
<label for="imageUrl" class="form-label">URL изображения</label>
<div class="input-group">
<input type="url" class="form-control" id="imageUrl" placeholder="https://example.com/image.jpg">
<button class="btn btn-primary" type="button" id="loadUrlImage">
<i class="fas fa-download me-1"></i>Загрузить
</button>
</div>
<small class="text-muted">Введите прямую ссылку на изображение</small>
</div>
<div class="col-md-4">
<div class="text-center">
<h6>Предварительный просмотр</h6>
<img id="urlPreview" class="image-preview" src="/images/placeholders/no-image.png" alt="Предварительный просмотр">
</div>
</div>
</div>
</div>
</div>
</div>
<div class="card-footer">
<div class="d-flex justify-content-between">
<div>
<strong>Выбранное изображение:</strong>
<span id="selectedImagePath" class="text-muted ms-2">Не выбрано</span>
</div>
<div>
<button type="button" class="btn btn-secondary me-2" onclick="closeImageEditor()">
<i class="fas fa-times me-1"></i>Отмена
</button>
<button type="button" class="btn btn-primary" id="confirmSelection" disabled>
<i class="fas fa-check me-1"></i>Выбрать
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Bootstrap JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
let selectedImagePath = null;
let targetField = null;
// Инициализация редактора
function initImageEditor() {
setupUploadEvents();
setupGalleryEvents();
setupUrlEvents();
loadGallery('all');
}
// Настройка событий загрузки
function setupUploadEvents() {
const uploadArea = document.getElementById('uploadArea');
const fileInput = document.getElementById('fileInput');
uploadArea.addEventListener('click', () => fileInput.click());
uploadArea.addEventListener('dragover', (e) => {
e.preventDefault();
uploadArea.classList.add('drag-over');
});
uploadArea.addEventListener('dragleave', () => {
uploadArea.classList.remove('drag-over');
});
uploadArea.addEventListener('drop', (e) => {
e.preventDefault();
uploadArea.classList.remove('drag-over');
handleFiles(e.dataTransfer.files);
});
fileInput.addEventListener('change', (e) => {
handleFiles(e.target.files);
});
}
// Обработка файлов
function handleFiles(files) {
if (files.length > 0) {
const file = files[0];
if (validateFile(file)) {
showPreview(file);
uploadFile(file);
}
}
}
// Валидация файла
function validateFile(file) {
const maxSize = 5 * 1024 * 1024; // 5MB
const allowedTypes = ['image/jpeg', 'image/png', 'image/gif'];
if (!allowedTypes.includes(file.type)) {
alert('Неподдерживаемый тип файла. Используйте JPG, PNG или GIF.');
return false;
}
if (file.size > maxSize) {
alert('Файл слишком большой. Максимальный размер: 5МБ.');
return false;
}
return true;
}
// Показ предварительного просмотра
function showPreview(file) {
const reader = new FileReader();
const preview = document.getElementById('uploadPreview');
const info = document.getElementById('imageInfo');
reader.onload = (e) => {
preview.src = e.target.result;
info.textContent = `${file.name} (${(file.size / 1024).toFixed(1)} KB)`;
};
reader.readAsDataURL(file);
}
// Загрузка файла на сервер
async function uploadFile(file) {
const formData = new FormData();
formData.append('image', file);
const progress = document.getElementById('uploadProgress');
const progressBar = progress.querySelector('.progress-bar');
progress.style.display = 'block';
try {
const response = await fetch('/api/images/upload', {
method: 'POST',
body: formData
});
const result = await response.json();
if (result.success) {
selectedImagePath = result.data.path;
updateSelectedImage(selectedImagePath);
showSuccessMessage('Изображение успешно загружено!');
// Обновляем галерею
setTimeout(() => loadGallery('all'), 500);
} else {
alert('Ошибка загрузки: ' + result.error);
}
} catch (error) {
alert('Ошибка загрузки: ' + error.message);
} finally {
progress.style.display = 'none';
progressBar.style.width = '0%';
}
}
// Настройка событий галереи
function setupGalleryEvents() {
document.querySelectorAll('[data-folder]').forEach(btn => {
btn.addEventListener('click', (e) => {
document.querySelectorAll('[data-folder]').forEach(b => b.classList.remove('active'));
e.target.classList.add('active');
loadGallery(e.target.dataset.folder);
});
});
}
// Загрузка галереи
async function loadGallery(folder) {
const galleryContainer = document.getElementById('galleryImages');
galleryContainer.innerHTML = '<div class="col-12 text-center py-4"><i class="fas fa-spinner fa-spin fa-2x text-muted"></i><p class="mt-2 text-muted">Загрузка галереи...</p></div>';
try {
const response = await fetch(`/api/images/gallery?folder=${folder}`);
const result = await response.json();
if (result.success) {
renderGallery(result.data);
} else {
galleryContainer.innerHTML = '<div class="col-12 text-center py-4"><p class="text-muted">Ошибка загрузки галереи</p></div>';
}
} catch (error) {
galleryContainer.innerHTML = '<div class="col-12 text-center py-4"><p class="text-muted">Ошибка: ' + error.message + '</p></div>';
}
}
// Отображение галереи
function renderGallery(images) {
const galleryContainer = document.getElementById('galleryImages');
if (images.length === 0) {
galleryContainer.innerHTML = '<div class="col-12 text-center py-4"><p class="text-muted">Изображения не найдены</p></div>';
return;
}
galleryContainer.innerHTML = images.map(img => `
<div class="col-md-2 col-sm-3 col-4 mb-3">
<div class="gallery-item" data-path="${img.path}" onclick="selectGalleryImage('${img.path}')">
<img src="${img.path}" class="img-fluid rounded" alt="${img.name}" title="${img.name}">
<small class="d-block text-center text-muted mt-1">${img.name}</small>
</div>
</div>
`).join('');
}
// Выбор изображения из галереи
function selectGalleryImage(path) {
document.querySelectorAll('.gallery-item').forEach(item => {
item.classList.remove('selected');
});
document.querySelector(`[data-path="${path}"]`).classList.add('selected');
selectedImagePath = path;
updateSelectedImage(path);
}
// Настройка событий URL
function setupUrlEvents() {
const urlInput = document.getElementById('imageUrl');
const loadBtn = document.getElementById('loadUrlImage');
const urlPreview = document.getElementById('urlPreview');
loadBtn.addEventListener('click', () => {
const url = urlInput.value.trim();
if (url) {
urlPreview.src = url;
selectedImagePath = url;
updateSelectedImage(url);
}
});
urlInput.addEventListener('input', (e) => {
const url = e.target.value.trim();
if (url && isValidUrl(url)) {
urlPreview.src = url;
}
});
}
// Проверка валидности URL
function isValidUrl(string) {
try {
new URL(string);
return true;
} catch (_) {
return false;
}
}
// Обновление выбранного изображения
function updateSelectedImage(path) {
document.getElementById('selectedImagePath').textContent = path;
document.getElementById('confirmSelection').disabled = false;
}
// Подтверждение выбора
document.getElementById('confirmSelection').addEventListener('click', () => {
if (selectedImagePath && window.opener) {
// Передаем путь в родительское окно
window.opener.postMessage({
type: 'imageSelected',
path: selectedImagePath,
targetField: new URLSearchParams(window.location.search).get('field')
}, '*');
window.close();
}
});
// Закрытие редактора
function closeImageEditor() {
window.close();
}
// Показ сообщения об успехе
function showSuccessMessage(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);
}
// Инициализация при загрузке страницы
document.addEventListener('DOMContentLoaded', initImageEditor);
</script>
</body>
</html>