🚀 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

@@ -1,9 +1,16 @@
import AdminJS from 'adminjs';
import AdminJSExpress from '@adminjs/express';
import AdminJSSequelize from '@adminjs/sequelize';
import uploadFeature from '@adminjs/upload';
import bcrypt from 'bcryptjs';
import pkg from 'pg';
import { Sequelize, DataTypes } from 'sequelize';
import path from 'path';
import { fileURLToPath } from 'url';
import { dirname } from 'path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const { Pool } = pkg;
// Регистрируем адаптер Sequelize
@@ -61,6 +68,7 @@ const Guides = sequelize.define('guides', {
specialization: { type: DataTypes.ENUM('city', 'mountain', 'fishing', 'general') },
bio: { type: DataTypes.TEXT },
experience: { type: DataTypes.INTEGER },
image_url: { type: DataTypes.STRING },
hourly_rate: { type: DataTypes.DECIMAL(10, 2) },
is_active: { type: DataTypes.BOOLEAN, defaultValue: true },
created_at: { type: DataTypes.DATE, defaultValue: DataTypes.NOW }
@@ -280,7 +288,7 @@ const adminJsOptions = {
},
image_url: {
type: 'string',
description: 'URL изображения тура (например: /images/tours/seoul-1.jpg)'
description: 'Изображение тура. Кнопка "Выбрать" будет добавлена автоматически'
},
is_featured: { type: 'boolean' },
is_active: { type: 'boolean' },
@@ -298,8 +306,8 @@ const adminJsOptions = {
options: {
parent: { name: 'Персонал', icon: 'Users' },
listProperties: ['id', 'name', 'email', 'specialization', 'experience', 'hourly_rate', 'is_active'],
editProperties: ['name', 'email', 'phone', 'languages', 'specialization', 'bio', 'experience', 'hourly_rate', 'is_active'],
showProperties: ['id', 'name', 'email', 'phone', 'languages', 'specialization', 'bio', 'experience', 'hourly_rate', 'is_active', 'created_at'],
editProperties: ['name', 'email', 'phone', 'languages', 'specialization', 'bio', 'experience', 'image_url', 'hourly_rate', 'is_active'],
showProperties: ['id', 'name', 'email', 'phone', 'languages', 'specialization', 'bio', 'experience', 'image_url', 'hourly_rate', 'is_active', 'created_at'],
filterProperties: ['name', 'specialization', 'is_active'],
properties: {
name: {
@@ -328,6 +336,10 @@ const adminJsOptions = {
type: 'number',
description: 'Опыт работы в годах',
},
image_url: {
type: 'string',
description: 'Фотография гида. Кнопка "Выбрать" будет добавлена автоматически'
},
hourly_rate: {
type: 'number',
description: 'Ставка за час в вонах',
@@ -344,7 +356,7 @@ const adminJsOptions = {
options: {
parent: { name: 'Контент', icon: 'DocumentText' },
listProperties: ['id', 'title', 'category', 'is_published', 'views', 'created_at'],
editProperties: ['title', 'excerpt', 'content', 'category', 'is_published'],
editProperties: ['title', 'excerpt', 'content', 'category', 'image_url', 'is_published'],
showProperties: ['id', 'title', 'excerpt', 'content', 'category', 'is_published', 'views', 'created_at', 'updated_at'],
filterProperties: ['title', 'category', 'is_published'],
properties: {
@@ -369,6 +381,10 @@ const adminJsOptions = {
{ value: 'history', label: 'История' }
],
},
image_url: {
type: 'string',
description: 'Изображение статьи. Кнопка "Выбрать" будет добавлена автоматически'
},
is_published: { type: 'boolean' },
views: {
type: 'number',
@@ -730,10 +746,15 @@ const adminJsOptions = {
},
dashboard: {
component: false
},
assets: {
styles: ['/css/admin-custom.css'],
scripts: ['/js/admin-image-selector-fixed.js']
}
};
// Создаем экземпляр AdminJS
// Создаем экземпляр AdminJS с componentLoader
// Создание AdminJS с конфигурацией
const adminJs = new AdminJS(adminJsOptions);
// Настраиваем аутентификацию