🚀 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,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;