🚀 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,130 @@
// Простой компонент для выбора изображений, который работает с AdminJS
import React from 'react';
const ImageSelector = ({ record, property, onChange }) => {
const currentValue = record.params[property.name] || '';
const openImagePicker = () => {
// Создаем модальное окно с iframe для редактора изображений
const modal = document.createElement('div');
modal.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.8);
z-index: 10000;
display: flex;
align-items: center;
justify-content: center;
`;
const content = document.createElement('div');
content.style.cssText = `
background: white;
border-radius: 8px;
width: 90%;
height: 90%;
position: relative;
`;
const closeBtn = document.createElement('button');
closeBtn.textContent = '✕';
closeBtn.style.cssText = `
position: absolute;
top: 10px;
right: 10px;
background: #ff4757;
color: white;
border: none;
border-radius: 50%;
width: 30px;
height: 30px;
cursor: pointer;
z-index: 1;
`;
const iframe = document.createElement('iframe');
iframe.src = `/image-editor.html?field=${property.name}&current=${encodeURIComponent(currentValue)}`;
iframe.style.cssText = `
width: 100%;
height: 100%;
border: none;
border-radius: 8px;
`;
closeBtn.onclick = () => document.body.removeChild(modal);
// Слушаем сообщения от iframe
const handleMessage = (event) => {
if (event.origin !== window.location.origin) return;
if (event.data.type === 'imageSelected' && event.data.targetField === property.name) {
onChange(property.name, event.data.path);
document.body.removeChild(modal);
window.removeEventListener('message', handleMessage);
}
};
window.addEventListener('message', handleMessage);
content.appendChild(closeBtn);
content.appendChild(iframe);
modal.appendChild(content);
document.body.appendChild(modal);
};
return React.createElement('div', null,
React.createElement('label', {
style: { fontWeight: 'bold', marginBottom: '8px', display: 'block' }
}, property.label || property.name),
React.createElement('div', {
style: { display: 'flex', alignItems: 'center', marginBottom: '12px' }
},
React.createElement('input', {
type: 'text',
value: currentValue,
onChange: (e) => onChange(property.name, e.target.value),
placeholder: 'Путь к изображению',
style: {
flex: 1,
padding: '8px 12px',
border: '1px solid #ddd',
borderRadius: '4px',
marginRight: '8px'
}
}),
React.createElement('button', {
type: 'button',
onClick: openImagePicker,
style: {
padding: '8px 16px',
backgroundColor: '#007bff',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer'
}
}, 'Выбрать')
),
currentValue && React.createElement('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';
}
})
);
};
export default ImageSelector;