✨ 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
271 lines
8.6 KiB
JavaScript
271 lines
8.6 KiB
JavaScript
import React, { useState, useEffect } from 'react';
|
|
|
|
const ImageEditor = ({ record, property, onChange }) => {
|
|
const [currentValue, setCurrentValue] = useState(record.params[property.name] || '');
|
|
const [showEditor, setShowEditor] = useState(false);
|
|
const [images, setImages] = useState([]);
|
|
const [loading, setLoading] = useState(false);
|
|
const [activeTab, setActiveTab] = useState('gallery');
|
|
const [uploadFile, setUploadFile] = useState(null);
|
|
|
|
// Загрузка галереи изображений
|
|
useEffect(() => {
|
|
if (showEditor) {
|
|
loadGallery();
|
|
}
|
|
}, [showEditor]);
|
|
|
|
const loadGallery = async () => {
|
|
setLoading(true);
|
|
try {
|
|
const response = await fetch('/api/images/gallery?folder=all');
|
|
const result = await response.json();
|
|
if (result.success) {
|
|
setImages(result.data);
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading gallery:', error);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleImageSelect = (imagePath) => {
|
|
setCurrentValue(imagePath);
|
|
onChange(property.name, imagePath);
|
|
setShowEditor(false);
|
|
};
|
|
|
|
const handleFileUpload = async () => {
|
|
if (!uploadFile) return;
|
|
|
|
const formData = new FormData();
|
|
formData.append('image', uploadFile);
|
|
|
|
setLoading(true);
|
|
try {
|
|
const response = await fetch('/api/images/upload', {
|
|
method: 'POST',
|
|
body: formData
|
|
});
|
|
|
|
const result = await response.json();
|
|
if (result.success) {
|
|
handleImageSelect(result.data.path);
|
|
await loadGallery(); // Обновляем галерею
|
|
} else {
|
|
alert('Ошибка загрузки: ' + result.error);
|
|
}
|
|
} catch (error) {
|
|
alert('Ошибка загрузки: ' + error.message);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const getFolderFromPropertyName = () => {
|
|
const name = property.name.toLowerCase();
|
|
if (name.includes('route')) return 'routes';
|
|
if (name.includes('guide')) return 'guides';
|
|
if (name.includes('article')) return 'articles';
|
|
return 'general';
|
|
};
|
|
|
|
return (
|
|
<div>
|
|
<label style={{ fontWeight: 'bold', marginBottom: '8px', display: 'block' }}>
|
|
{property.label || property.name}
|
|
</label>
|
|
|
|
{/* Поле ввода и кнопка */}
|
|
<div style={{ display: 'flex', alignItems: 'center', marginBottom: '12px' }}>
|
|
<input
|
|
type="text"
|
|
value={currentValue}
|
|
onChange={(e) => {
|
|
setCurrentValue(e.target.value);
|
|
onChange(property.name, e.target.value);
|
|
}}
|
|
placeholder="Путь к изображению"
|
|
style={{
|
|
flex: 1,
|
|
padding: '8px 12px',
|
|
border: '1px solid #ddd',
|
|
borderRadius: '4px',
|
|
marginRight: '8px'
|
|
}}
|
|
/>
|
|
<button
|
|
type="button"
|
|
onClick={() => setShowEditor(!showEditor)}
|
|
style={{
|
|
padding: '8px 16px',
|
|
backgroundColor: '#007bff',
|
|
color: 'white',
|
|
border: 'none',
|
|
borderRadius: '4px',
|
|
cursor: 'pointer'
|
|
}}
|
|
>
|
|
{showEditor ? 'Закрыть' : 'Выбрать'}
|
|
</button>
|
|
</div>
|
|
|
|
{/* Предварительный просмотр */}
|
|
{currentValue && (
|
|
<div style={{ marginBottom: '12px' }}>
|
|
<img
|
|
src={currentValue}
|
|
alt="Preview"
|
|
style={{
|
|
maxWidth: '200px',
|
|
maxHeight: '200px',
|
|
objectFit: 'cover',
|
|
border: '1px solid #ddd',
|
|
borderRadius: '4px'
|
|
}}
|
|
onError={(e) => {
|
|
e.target.style.display = 'none';
|
|
}}
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
{/* Редактор изображений */}
|
|
{showEditor && (
|
|
<div style={{
|
|
border: '1px solid #ddd',
|
|
borderRadius: '4px',
|
|
padding: '16px',
|
|
backgroundColor: '#f9f9f9'
|
|
}}>
|
|
{/* Вкладки */}
|
|
<div style={{ marginBottom: '16px' }}>
|
|
<button
|
|
type="button"
|
|
onClick={() => setActiveTab('gallery')}
|
|
style={{
|
|
padding: '8px 16px',
|
|
marginRight: '8px',
|
|
backgroundColor: activeTab === 'gallery' ? '#007bff' : '#f8f9fa',
|
|
color: activeTab === 'gallery' ? 'white' : '#495057',
|
|
border: '1px solid #dee2e6',
|
|
borderRadius: '4px',
|
|
cursor: 'pointer'
|
|
}}
|
|
>
|
|
Галерея
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => setActiveTab('upload')}
|
|
style={{
|
|
padding: '8px 16px',
|
|
backgroundColor: activeTab === 'upload' ? '#007bff' : '#f8f9fa',
|
|
color: activeTab === 'upload' ? 'white' : '#495057',
|
|
border: '1px solid #dee2e6',
|
|
borderRadius: '4px',
|
|
cursor: 'pointer'
|
|
}}
|
|
>
|
|
Загрузить
|
|
</button>
|
|
</div>
|
|
|
|
{/* Контент вкладки Галерея */}
|
|
{activeTab === 'gallery' && (
|
|
<div>
|
|
{loading ? (
|
|
<div style={{ textAlign: 'center', padding: '20px' }}>
|
|
Загрузка...
|
|
</div>
|
|
) : (
|
|
<div style={{
|
|
display: 'grid',
|
|
gridTemplateColumns: 'repeat(auto-fill, minmax(150px, 1fr))',
|
|
gap: '12px',
|
|
maxHeight: '400px',
|
|
overflowY: 'auto'
|
|
}}>
|
|
{images.map((image) => (
|
|
<div
|
|
key={image.path}
|
|
onClick={() => handleImageSelect(image.path)}
|
|
style={{
|
|
cursor: 'pointer',
|
|
border: currentValue === image.path ? '2px solid #007bff' : '1px solid #ddd',
|
|
borderRadius: '4px',
|
|
overflow: 'hidden',
|
|
backgroundColor: 'white'
|
|
}}
|
|
>
|
|
<img
|
|
src={image.path}
|
|
alt={image.name}
|
|
style={{
|
|
width: '100%',
|
|
height: '120px',
|
|
objectFit: 'cover'
|
|
}}
|
|
/>
|
|
<div style={{ padding: '8px', fontSize: '12px' }}>
|
|
<div style={{ fontWeight: 'bold', marginBottom: '4px' }}>
|
|
{image.name}
|
|
</div>
|
|
<div style={{ color: '#666' }}>
|
|
{image.folder}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* Контент вкладки Загрузить */}
|
|
{activeTab === 'upload' && (
|
|
<div>
|
|
<div style={{ marginBottom: '16px' }}>
|
|
<input
|
|
type="file"
|
|
accept="image/*"
|
|
onChange={(e) => setUploadFile(e.target.files[0])}
|
|
style={{ marginBottom: '12px' }}
|
|
/>
|
|
<div style={{ fontSize: '14px', color: '#666' }}>
|
|
Поддерживаются: JPG, PNG, GIF (макс. 5МБ)
|
|
</div>
|
|
</div>
|
|
|
|
{uploadFile && (
|
|
<div style={{ marginBottom: '16px' }}>
|
|
<div style={{ fontSize: '14px', marginBottom: '8px' }}>
|
|
Выбран файл: {uploadFile.name}
|
|
</div>
|
|
<button
|
|
type="button"
|
|
onClick={handleFileUpload}
|
|
disabled={loading}
|
|
style={{
|
|
padding: '8px 16px',
|
|
backgroundColor: '#28a745',
|
|
color: 'white',
|
|
border: 'none',
|
|
borderRadius: '4px',
|
|
cursor: loading ? 'not-allowed' : 'pointer'
|
|
}}
|
|
>
|
|
{loading ? 'Загрузка...' : 'Загрузить'}
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default ImageEditor; |