🚀 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:
271
src/components/ImageEditor.jsx
Normal file
271
src/components/ImageEditor.jsx
Normal file
@@ -0,0 +1,271 @@
|
||||
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;
|
||||
Reference in New Issue
Block a user