✨ 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
130 lines
3.6 KiB
JavaScript
130 lines
3.6 KiB
JavaScript
// Простой компонент для выбора изображений, который работает с 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}¤t=${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; |