diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..c35af6b --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,151 @@ +# SmartSolTech Website - AI Coding Agent Instructions + +## Project Overview +SmartSolTech is a **Korean tech services company** PWA with an admin panel, portfolio management, Telegram integration, and service calculator. Built with Node.js/Express backend, EJS templating, MongoDB, and modern PWA features. + +## Architecture & Key Patterns + +### 🏗️ Backend Architecture +- **Main server**: `server.js` - Express app with comprehensive middleware stack +- **Models**: MongoDB with Mongoose ODM - all in `/models/` (User, Portfolio, Service, Contact, SiteSettings) +- **Routes**: RESTful API patterns in `/routes/` - separate files per domain +- **Authentication**: Dual auth system - JWT tokens for API, sessions for web pages +- **Validation**: express-validator middleware in `/middleware/validation.js` with Korean error messages + +### 🔑 Authentication Pattern +```javascript +// Two authentication strategies: +// 1. JWT for API endpoints (authenticateToken) +// 2. Session-based for web pages (authenticateSession) +// See middleware/auth.js for implementations +``` + +### 📊 Data Models Key Features +- **Portfolio**: Has virtual `primaryImage` getter, text search indexes, and SEO fields +- **Service**: Complex pricing structure with features array (included/not included) +- **User**: bcrypt password hashing with pre-save hook and comparePassword method +- **All models**: Use timestamps and have soft-delete patterns where applicable + +### 🛣️ Route Organization +- **Admin routes** (`/admin`): Session-based auth, server-side rendered EJS views +- **API routes** (`/api/*`): JWT auth, JSON responses, rate-limited +- **Public routes** (`/`): Mixed auth (optional), EJS templates with PWA support + +## Development Workflow + +### 🚀 Essential Commands +```bash +# Development with hot reload and file watching +npm run dev + +# Initialize database with admin user and sample data +npm run init-db + +# Production build with webpack optimization +npm run build + +# Database setup creates: +# - Admin user (from .env: ADMIN_EMAIL/ADMIN_PASSWORD) +# - Sample services, portfolio items, site settings +``` + +### 🔧 Environment Setup +**Critical `.env` variables**: +- `MONGODB_URI` - Database connection +- `SESSION_SECRET`, `JWT_SECRET` - Security keys +- `ADMIN_EMAIL`, `ADMIN_PASSWORD` - Initial admin account +- `TELEGRAM_BOT_TOKEN` - Optional Telegram integration +- `NODE_ENV` - Controls error verbosity and security settings + +## Frontend Architecture + +### 🎨 View Layer (EJS Templates) +- **Layout system**: `views/layout.ejs` is main wrapper +- **Partials**: `views/partials/` for reusable components (navigation, footer) +- **Admin panel**: Separate layout `admin/layout.ejs` with different styling +- **Internationalization**: Content in Korean, supports locales in `/locales/` + +### 📱 PWA Implementation +- **Service Worker**: `public/sw.js` - caches static files, dynamic routes, and API responses +- **Manifest**: `public/manifest.json` - app metadata and icons +- **Offline-first**: Service worker handles network failures gracefully +- **Cache strategy**: Static files cached immediately, dynamic content cached on demand + +### 🎯 Frontend JavaScript Patterns +- **Main script**: `public/js/main.js` - handles navigation, scroll effects, form interactions +- **Calculator**: `public/js/calculator.js` - complex service pricing calculations +- **Libraries**: AOS animations, Tailwind CSS, Socket.io for real-time features + +## Key Integration Points + +### 📧 Communication Systems +- **Telegram Bot**: Optional integration for admin notifications (contact forms, orders) +- **Email**: Nodemailer for SMTP email sending (contact forms, notifications) +- **Real-time**: Socket.io setup for live updates (dashboard, notifications) + +### 🔒 Security Patterns +- **Helmet**: CSP headers allowing specific domains (fonts.googleapis.com, cdnjs.cloudflare.com) +- **Rate limiting**: Applied to `/api/*` routes (100 requests/15min per IP) +- **Input validation**: Korean-language error messages, comprehensive field validation +- **File uploads**: Multer with Sharp image processing, stored in `public/uploads/` + +### 🗄️ Database Patterns +- **Connection**: Single MongoDB connection with connection pooling +- **Indexing**: Text search on Portfolio, compound indexes for performance +- **Session storage**: MongoDB session store for persistence + +## Business Logic Specifics + +### 💰 Service Calculator +- **Complex pricing**: Base price + features + timeline modifiers +- **Multi-step form**: Service selection → customization → contact info → quote +- **Integration**: Calculator data feeds into Contact model for lead tracking + +### 🎨 Portfolio Management +- **Image handling**: Multiple images per project, primary image logic +- **Categories**: Predefined categories (web-development, mobile-app, ui-ux-design, etc.) +- **SEO optimization**: Meta fields, structured data for portfolio items + +### 👑 Admin Panel Features +- **Dashboard**: Statistics, recent activity, quick actions +- **Content management**: CRUD for Portfolio, Services, Site Settings +- **Media gallery**: File upload with image optimization +- **Contact management**: Lead tracking, status updates, response handling + +## Common Tasks & Patterns + +### 🔧 Adding New Features +1. **Model**: Create in `/models/` with proper validation and indexes +2. **Routes**: Add to `/routes/` with appropriate auth middleware +3. **Views**: EJS templates in `/views/` using layout system +4. **API**: JSON endpoints with validation middleware +5. **Frontend**: Add to `public/js/` with proper event handling + +### 🐛 Debugging Workflow +- **Development**: Use `npm run dev` for auto-restart and detailed logging +- **Database**: Check MongoDB connection and sample data with `npm run init-db` +- **Authentication**: Test both JWT (API) and session (web) auth flows +- **PWA**: Check service worker registration and cache behavior in dev tools + +### 📦 Deployment Considerations +- **Environment**: Production requires secure cookies, CSP, and rate limiting +- **Database**: Ensure MongoDB indexes are created for performance +- **Assets**: Static files served by Express, consider CDN for production +- **Monitoring**: Error handling sends different messages based on NODE_ENV + +## File Structure Quick Reference +``` +├── models/ # MongoDB schemas with business logic +├── routes/ # Express routes (API + web pages) +├── middleware/ # Auth, validation, error handling +├── views/ # EJS templates with Korean content +├── public/ # Static assets + PWA files +├── scripts/ # Database init, dev server, build tools +└── server.js # Main Express application +``` + +## Korean Language Notes +- **UI Text**: All user-facing content in Korean +- **Error Messages**: Validation errors in Korean (`middleware/validation.js`) +- **Admin Interface**: Korean labels and messages throughout admin panel +- **SEO Content**: Korean meta descriptions and structured data \ No newline at end of file diff --git a/REPORT.md b/REPORT.md new file mode 100644 index 0000000..584e50e --- /dev/null +++ b/REPORT.md @@ -0,0 +1,132 @@ +# 🎯 SmartSolTech - Отчет о Выполненных Исправлениях + +## 📋 Задачи которые были выполнены: + +### 1. 🏠 **Исправление главной страницы** +- ✅ **Проблема**: Стили на главной странице были поломаны +- ✅ **Решение**: + - Полностью переписан `base.css` с принудительными стилями (!important) + - Улучшен градиент hero-секции с многослойными эффектами + - Добавлены анимации и hover-эффекты для кнопок + - Исправлена навигация с backdrop-filter и webkit префиксами + - Оптимизирована отзывчивость для мобильных устройств + +### 2. 📐 **Компактные баннеры для внутренних страниц** +- ✅ **Проблема**: Hero-баннеры на всех страницах были полноэкранными +- ✅ **Решение**: + - Создан класс `.hero-section-compact` для внутренних страниц + - Уменьшена высота с 100vh до 40vh (50vh максимум) + - Обновлены страницы: "О нас", "Услуги" + - Сохранен полноэкранный баннер только на главной странице + +### 3. 🖼️ **Система редактирования изображений баннеров** + +#### **A. Backend API (/routes/media.js)** +- ✅ Загрузка одного изображения: `POST /media/upload` +- ✅ Загрузка нескольких изображений: `POST /media/upload-multiple` +- ✅ Удаление изображений: `DELETE /media/:filename` +- ✅ Список изображений: `GET /media/list` +- ✅ Автоматическая оптимизация изображений с Sharp +- ✅ Создание thumbnails (300x200, 800x600, 1200x900) +- ✅ Конвертация в WebP для лучшего сжатия +- ✅ Безопасность: аутентификация и валидация файлов + +#### **B. Frontend Редактор (/views/admin/banner-editor.ejs)** +- ✅ Интуитивный интерфейс с табами для разных страниц +- ✅ Drag & Drop загрузка изображений +- ✅ Превью изображений с возможностью удаления +- ✅ Индикатор прогресса загрузки +- ✅ Галерея загруженных изображений +- ✅ Мгновенная смена баннеров одним кликом +- ✅ Локальное сохранение настроек в localStorage + +#### **C. Интеграция с админкой** +- ✅ Новый маршрут: `/admin/banner-editor` +- ✅ Аутентификация через админ-сессии +- ✅ Интеграция с существующей админ панелью + +## 🔧 Технические улучшения: + +### **CSS архитектура:** +```css +base.css (16KB) - Принудительные стили и reset +main.css (11KB) - Компоненты и анимации + компактные баннеры +fixes.css (6KB) - Дополнительные исправления +``` + +### **Оптимизация изображений:** +- Автоматическое создание 4 размеров (thumbnail, medium, large, original) +- Конвертация в WebP (экономия до 50% размера) +- Максимальный размер файла: 10MB +- Поддержка: JPG, PNG, GIF, WebP + +### **Безопасность:** +- Валидация типов файлов +- Защита от path traversal атак +- Аутентификация для всех операций +- Автоматическая очистка при ошибках + +## 📊 Результаты тестирования: + +``` +🏠 ГЛАВНАЯ СТРАНИЦА: ✅ 200 OK (0.015s) +🎨 О НАС (компактный): ✅ 200 OK (0.007s) +🎨 УСЛУГИ (компактный): ✅ 200 OK (0.009s) +📱 ПОРТФОЛИО: ✅ 200 OK (0.012s) +🧮 КАЛЬКУЛЯТОР: ✅ 200 OK (0.008s) +📸 РЕДАКТОР БАННЕРОВ: ✅ 302 (требует авторизации) +🎨 CSS ФАЙЛЫ: ✅ Все загружаются корректно +📁 UPLOADS ПАПКА: ✅ Создана и готова +``` + +## 🚀 Как использовать редактор баннеров: + +### **Шаг 1: Доступ** +``` +URL: http://localhost:3000/admin/banner-editor +Требуется: Авторизация в админ панели +``` + +### **Шаг 2: Загрузка изображений** +1. Нажать "Загрузить Изображения" +2. Перетащить файлы или выбрать через кнопку +3. Посмотреть превью и нажать "Загрузить" +4. Система автоматически создаст оптимизированные версии + +### **Шаг 3: Установка баннера** +1. Выбрать страницу во вкладках (Главная, О нас, Услуги, Портфолио) +2. Нажать "Использовать" на нужном изображении +3. Баннер мгновенно обновится +4. Настройки сохраняются автоматически + +### **Шаг 4: Управление** +- Удаление: кнопка "Удалить" в галерее +- Обновление: кнопка "Обновить" +- Сброс к градиенту: кнопка "Удалить" на текущем баннере + +## 🎯 Итоговое состояние: + +### ✅ **Что работает:** +- Главная страница с исправленными стилями +- Компактные баннеры на внутренних страницах +- Полнофункциональный редактор изображений +- API для загрузки и управления медиа +- Автоматическая оптимизация изображений +- Безопасная система аутентификации + +### 🔄 **Готово к использованию:** +- Сервер: `http://localhost:3000` (PID: 24059) +- Админка: `http://localhost:3000/admin` +- Редактор: `http://localhost:3000/admin/banner-editor` +- Папка загрузок: `/public/uploads/` (готова к использованию) + +### 📈 **Преимущества реализации:** +1. **Производительность**: WebP формат + множественные размеры +2. **UX**: Drag & Drop + мгновенные превью +3. **Безопасность**: Полная валидация + аутентификация +4. **Масштабируемость**: Готова к добавлению новых страниц +5. **Мобильность**: Отзывчивый дизайн на всех устройствах + +--- + +**🎉 Все задачи выполнены успешно! Система готова к продуктивному использованию.** \ No newline at end of file diff --git a/config/database.js b/config/database.js new file mode 100644 index 0000000..10b6a8f --- /dev/null +++ b/config/database.js @@ -0,0 +1,30 @@ +const { Sequelize } = require('sequelize'); +require('dotenv').config(); + +const sequelize = new Sequelize(process.env.DATABASE_URL || { + host: process.env.DB_HOST || 'localhost', + port: process.env.DB_PORT || 5432, + database: process.env.DB_NAME || 'smartsoltech', + username: process.env.DB_USER || 'postgres', + password: process.env.DB_PASSWORD || 'password', + dialect: 'postgres', + logging: process.env.NODE_ENV === 'development' ? console.log : false, + pool: { + max: 5, + min: 0, + acquire: 30000, + idle: 10000 + } +}); + +// Test the connection +async function testConnection() { + try { + await sequelize.authenticate(); + console.log('✓ PostgreSQL connected successfully'); + } catch (error) { + console.error('✗ PostgreSQL connection error:', error); + } +} + +module.exports = { sequelize, testConnection }; \ No newline at end of file diff --git a/cookies.txt b/cookies.txt new file mode 100644 index 0000000..c800e2f --- /dev/null +++ b/cookies.txt @@ -0,0 +1,5 @@ +# Netscape HTTP Cookie File +# https://curl.se/docs/http-cookies.html +# This file was generated by libcurl! Edit at your own risk. + +#HttpOnly_localhost FALSE / FALSE 1761640548 connect.sid s%3Avv-twFS8-lM9AQEsCor7mjMMTS7bSAIi.D1LM3WdMrHiBcGv175fp8L3ORi4gsGQ3RSm9a3H59to diff --git a/locales/en.json b/locales/en.json index 5f08cf5..538c0a2 100644 --- a/locales/en.json +++ b/locales/en.json @@ -1,318 +1,331 @@ { - "navigation": { - "home": "Home", - "about": "About", - "services": "Services", - "portfolio": "Portfolio", - "contact": "Contact", - "calculator": "Calculator", - "admin": "Admin", - "home - SmartSolTech": "navigation.home - SmartSolTech" - }, - "hero": { - "title": { - "smart": "hero.title.smart", - "solutions": "hero.title.solutions" - }, - "subtitle": "Solutions", - "description": "Innovative web development, mobile apps, UI/UX design leading your business digital transformation", - "cta_primary": "Start Project", - "cta_secondary": "View Portfolio", - "cta": { - "start": "hero.cta.start", - "portfolio": "hero.cta.portfolio" - } - }, - "services": { - "title": { - "our": "services.title.our", - "services": "services.title.services" - }, - "title_highlight": "Services", - "description": "Digital solutions completed with cutting-edge technology and creative ideas", - "web_development": { - "title": "Web Development", - "description": "Modern and responsive websites and web applications development", - "price": "$5,000~" - }, - "mobile_app": { - "title": "Mobile App", - "description": "Native and cross-platform apps for iOS and Android", - "price": "$8,000~" - }, - "ui_ux_design": { - "title": "UI/UX Design", - "description": "User-centered intuitive and beautiful interface design", - "price": "$3,000~" - }, - "digital_marketing": { - "title": "Digital Marketing", - "description": "Digital marketing through SEO, social media, online advertising", - "price": "$2,000~" - }, - "view_all": "View All Services", - "subtitle": "services.subtitle", - "web": { - "title": "services.web.title", - "description": "services.web.description", - "price": "services.web.price" - }, - "mobile": { - "title": "services.mobile.title", - "description": "services.mobile.description", - "price": "services.mobile.price" - }, - "design": { - "title": "services.design.title", - "description": "services.design.description", - "price": "services.design.price" - }, - "marketing": { - "title": "services.marketing.title", - "description": "services.marketing.description", - "price": "services.marketing.price" - } - }, - "portfolio": { - "title": { - "recent": "portfolio.title.recent", - "projects": "portfolio.title.projects" - }, - "title_highlight": "Projects", - "description": "Check out the projects completed for customer success", - "view_details": "View Details", - "view_all": "View All Portfolio", - "subtitle": "portfolio.subtitle" - }, - "calculator": { - "title": "Project Cost Calculator", - "subtitle": "Select your desired services and requirements to get accurate cost estimates in real time", - "meta": { - "title": "Project Cost Calculator", - "description": "Calculate the cost of your web development, mobile app, or design project with our interactive calculator" - }, - "cta": { - "title": "Check Your Project Estimate", - "subtitle": "Select your desired services and requirements to calculate costs in real time", - "button": "Use Cost Calculator" - }, - "step1": { - "title": "Step 1: Service Selection", - "subtitle": "Please select the services you need (multiple selection allowed)" - }, - "step2": { - "title": "Step 2: Project Details", - "subtitle": "Select project complexity and timeline" - }, - "complexity": { - "title": "Project Complexity", - "simple": "Simple", - "simple_desc": "Basic features, standard design", - "medium": "Medium", - "medium_desc": "Additional features, custom design", - "complex": "Complex", - "complex_desc": "Advanced features, complex integrations" - }, - "timeline": { - "title": "Development Timeline", - "standard": "Standard", - "standard_desc": "Normal development timeframe", - "rush": "Rush", - "rush_desc": "Fast development (+50%)", - "extended": "Extended", - "extended_desc": "Flexible development timeline (-20%)" - }, - "result": { - "title": "Estimate Results", - "subtitle": "Here's your preliminary project cost estimate", - "estimated_price": "Estimated Price", - "price_note": "* Final cost may vary based on project details", - "summary": "Project Summary", - "selected_services": "Selected Services", - "complexity": "Complexity", - "timeline": "Timeline", - "get_quote": "Get Accurate Quote", - "recalculate": "Recalculate", - "contact_note": "Contact us for an accurate quote and to discuss project details" - }, - "next_step": "Next Step", - "prev_step": "Previous", - "calculate": "Calculate" - }, - "contact": { - "ready_title": "Ready to Start Your Project?", - "ready_description": "Turn your ideas into reality. Experts provide the best solutions.", - "phone_consultation": "Phone Consultation", - "email_inquiry": "Email Inquiry", - "telegram_chat": "Telegram Chat", - "instant_response": "Instant response available", - "free_consultation": "Free Consultation Application", - "form": { - "name": "Name", - "email": "Email", - "phone": "Phone", - "service_interest": "Service Interest", - "service_options": { - "select": "Select Service Interest", - "web_development": "Web Development", - "mobile_app": "Mobile App", - "ui_ux_design": "UI/UX Design", - "branding": "Branding", - "consulting": "Consulting", - "other": "Other" - }, - "message": "Please briefly describe your project", - "submit": "Apply for Consultation", - "title": "contact.form.title", - "service": { - "select": "contact.form.service.select", - "web": "contact.form.service.web", - "mobile": "contact.form.service.mobile", - "design": "contact.form.service.design", - "branding": "contact.form.service.branding", - "consulting": "contact.form.service.consulting", - "other": "contact.form.service.other" - }, - "success": "contact.form.success", - "error": "contact.form.error" - }, - "cta": { - "ready": "contact.cta.ready", - "start": "contact.cta.start", - "question": "contact.cta.question", - "subtitle": "contact.cta.subtitle" - }, - "phone": { - "title": "contact.phone.title", - "number": "contact.phone.number" - }, - "email": { - "title": "contact.email.title", - "address": "contact.email.address" - }, - "telegram": { - "title": "contact.telegram.title", - "subtitle": "contact.telegram.subtitle" - } - }, - "about": { - "hero_title": "About", - "hero_highlight": "SmartSolTech", - "hero_description": "Digital solution specialist leading customer success with innovative technology", - "overview": { - "title": "Creating Future with Innovation and Creativity", - "description_1": "SmartSolTech is a digital solution specialist established in 2020, supporting customer business success with innovative technology and creative ideas in web development, mobile apps, and UI/UX design.", - "description_2": "We don't just provide technology, but understand customer goals and propose optimal solutions to become partners growing together.", - "stats": { - "projects": "100+", - "projects_label": "Completed Projects", - "clients": "50+", - "clients_label": "Satisfied Customers", - "experience": "4 years", - "experience_label": "Industry Experience" - }, - "mission": "Our Mission", - "mission_text": "Helping all businesses succeed in the digital age through technology", - "vision": "Our Vision", - "vision_text": "Growing as a global digital solution company representing Korea to lead digital innovation for customers worldwide" - }, - "values": { - "title": "Core", - "title_highlight": "Values", - "description": "Core values pursued by SmartSolTech", - "innovation": { - "title": "Innovation", - "description": "We provide innovative solutions through continuous R&D and adoption of cutting-edge technology." - }, - "collaboration": { - "title": "Collaboration", - "description": "We create the best results through close communication and collaboration with customers." - }, - "quality": { - "title": "Quality", - "description": "We maintain high quality standards and provide high-quality products that customers can be satisfied with." - }, - "growth": { - "title": "Growth", - "description": "We grow together with customers and pursue continuous learning and development." - } - }, - "team": { - "title": "Our", - "title_highlight": "Team", - "description": "Introducing the SmartSolTech team with expertise and passion" - }, - "tech_stack": { - "title": "Technology", - "title_highlight": "Stack", - "description": "We provide the best solutions with cutting-edge technology and proven tools", - "frontend": "Frontend", - "backend": "Backend", - "mobile": "Mobile" - }, - "cta": { - "title": "Become a Partner for Success Together", - "description": "Take your business to the next level with SmartSolTech", - "partnership": "Partnership Inquiry", - "portfolio": "View Portfolio" - } - }, - "footer": { - "company": { - "description": "footer.company.description" - }, - "description": "Digital solution specialist leading innovation", - "quick_links": "Quick Links", - "services": "Services", - "contact_info": "Contact Information", - "follow_us": "Follow Us", - "rights": "All rights reserved.", - "links": { - "title": "footer.links.title" - }, - "contact": { - "title": "footer.contact.title", - "email": "footer.contact.email", - "phone": "footer.contact.phone", - "address": "footer.contact.address" - }, - "copyright": "footer.copyright", - "privacy": "footer.privacy", - "terms": "footer.terms" - }, - "theme": { - "light": "Light Theme", - "dark": "Dark Theme", - "toggle": "Toggle Theme" - }, - "language": { - "english": "English", - "korean": "한국어", - "russian": "Русский", - "kazakh": "Қазақша", - "ko": "language.ko" - }, - "common": { - "loading": "Loading...", - "error": "Error occurred", - "success": "Success", - "view_more": "View More", - "back": "Back", - "next": "Next", - "previous": "Previous", - "view_details": "common.view_details" - }, - "undefined - SmartSolTech": "undefined - SmartSolTech", - "meta": { - "description": "meta.description", - "keywords": "meta.keywords", - "title": "meta.title" - }, - "nav": { - "home": "nav.home", - "about": "nav.about", - "services": "nav.services", - "portfolio": "nav.portfolio", - "calculator": "nav.calculator" - } + "navigation": { + "home": "Home", + "about": "About", + "services": "Services", + "portfolio": "Portfolio", + "contact": "Contact", + "calculator": "Calculator", + "admin": "Admin" + }, + "hero": { + "title": { + "smart": "Smart", + "solutions": "Solutions" + }, + "subtitle": "Grow your business with innovative technology", + "description": "Innovative web development, mobile apps, UI/UX design leading your business digital transformation", + "cta": { + "start": "Get Started", + "portfolio": "View Portfolio" + } + }, + "services": { + "title": { + "our": "Our", + "services": "Services" + }, + "subtitle": "Professional development services to turn your ideas into reality", + "description": "Digital solutions completed with cutting-edge technology and creative ideas", + "view_all": "View All Services", + "web": { + "title": "Web Development", + "description": "Responsive websites and web application development", + "price": "From $500" + }, + "mobile": { + "title": "Mobile Apps", + "description": "iOS and Android native app development", + "price": "From $1,000" + }, + "design": { + "title": "UI/UX Design", + "description": "User-centered interface and experience design", + "price": "From $300" + }, + "marketing": { + "title": "Digital Marketing", + "description": "SEO, social media marketing, advertising management", + "price": "From $200" + }, + "meta": { + "title": "Services", + "description": "Check out SmartSolTech's professional services. Web development, mobile apps, UI/UX design, digital marketing and other technology solutions.", + "keywords": "web development, mobile apps, UI/UX design, digital marketing, technology solutions, SmartSolTech" + }, + "hero": { + "title": "Our", + "title_highlight": "Services", + "subtitle": "Support business growth with innovative technology" + }, + "cards": { + "starting_price": "Starting Price", + "consultation": "consultation", + "contact": "Contact", + "calculate_cost": "Calculate Cost", + "popular": "Popular", + "coming_soon": "Services Coming Soon", + "coming_soon_desc": "We'll soon offer various services!" + }, + "process": { + "title": "Project Implementation Process", + "subtitle": "We conduct projects with systematic and professional processes", + "consultation": { + "title": "Consultation and Planning", + "description": "Accurately understand customer requirements" + } + } + }, + "portfolio": { + "title": { + "recent": "Recent", + "projects": "Projects" + }, + "subtitle": "Check out successfully completed projects", + "description": "Check out the projects completed for customer success", + "view_details": "View Details", + "view_all": "View All Portfolio", + "categories": { + "all": "All", + "web": "Web Development", + "mobile": "Mobile Apps", + "uiux": "UI/UX Design" + }, + "project_details": "Project Details", + "default": { + "ecommerce": "E-commerce", + "title": "E-commerce Platform", + "description": "Modern online commerce solution with intuitive interface" + }, + "meta": { + "title": "Portfolio", + "description": "Check out SmartSolTech's diverse projects and success stories. Web development, mobile apps, UI/UX design portfolio.", + "keywords": "portfolio, web development, mobile apps, UI/UX design, projects, SmartSolTech" + } + }, + "calculator": { + "title": "Project Cost Calculator", + "subtitle": "Select your desired services and requirements to get accurate cost estimates in real time", + "meta": { + "title": "Project Cost Calculator", + "description": "Calculate the cost of your web development, mobile app, or design project with our interactive calculator" + }, + "cta": { + "title": "Check Your Project Estimate", + "subtitle": "Select your desired services and requirements to calculate costs in real time", + "button": "Use Cost Calculator" + } + }, + "contact": { + "hero": { + "title": "Contact Us", + "subtitle": "We're here to help bring your ideas to life" + }, + "ready_title": "Ready to Start Your Project?", + "ready_description": "Turn your ideas into reality. Experts provide the best solutions.", + "form": { + "title": "Project Inquiry", + "name": "Name", + "email": "Email", + "phone": "Phone", + "message": "Message", + "submit": "Send Inquiry", + "success": "Inquiry sent successfully", + "error": "Error occurred while sending inquiry", + "service": { + "title": "Service Interest", + "select": "Select service of interest", + "web": "Web Development", + "mobile": "Mobile App", + "design": "UI/UX Design", + "branding": "Branding", + "consulting": "Consulting", + "other": "Other" + } + }, + "info": { + "title": "Contact Information" + }, + "phone": { + "title": "Phone Inquiry", + "number": "+82-2-1234-5678", + "hours": "Mon-Fri 9:00-18:00" + }, + "email": { + "title": "Email Inquiry", + "address": "info@smartsoltech.co.kr", + "response": "Response within 24 hours" + }, + "telegram": { + "title": "Telegram", + "subtitle": "For quick response" + }, + "address": { + "title": "Office Address", + "line1": "123 Teheran-ro, Gangnam-gu", + "line2": "Seoul, South Korea" + }, + "cta": { + "ready": "Ready?", + "start": "Get Started", + "question": "Have questions?", + "subtitle": "We provide consultation on projects" + }, + "meta": { + "title": "Contact", + "description": "Contact us anytime for project inquiries or consultation" + } + }, + "about": { + "hero": { + "title": "About SmartSolTech", + "subtitle": "Creating the future with innovation and technology" + }, + "company": { + "title": "Company Information", + "description1": "SmartSolTech is a technology company established in 2020, recognized for expertise in web development, mobile app development, and UI/UX design.", + "description2": "We accurately understand customer needs and provide innovative solutions using the latest technology." + }, + "stats": { + "projects": "Completed Projects", + "experience": "Years Experience", + "clients": "Satisfied Customers" + }, + "mission": { + "title": "Our Mission", + "description": "Our mission is to support customer business growth through technology and lead digital innovation." + }, + "values": { + "innovation": { + "title": "Innovation", + "description": "We provide innovative solutions through continuous R&D and adoption of cutting-edge technology." + }, + "quality": { + "title": "Quality", + "description": "We maintain high quality standards and provide high-quality products that customers can be satisfied with." + }, + "partnership": { + "title": "Partnership", + "description": "We create the best results through close communication and collaboration with customers." + } + }, + "cta": { + "title": "We'll Grow Together", + "subtitle": "Turn your ideas into reality", + "button": "Contact Us" + }, + "meta": { + "title": "About Us", + "description": "SmartSolTech is a professional development company that supports customer business growth with innovative technology" + } + }, + "footer": { + "description": "Digital solution specialist leading innovation", + "links": { + "title": "Quick Links" + }, + "contact": { + "title": "Contact", + "email": "info@smartsoltech.co.kr", + "phone": "+82-2-1234-5678", + "address": "123 Teheran-ro, Gangnam-gu, Seoul" + }, + "copyright": "© 2024 SmartSolTech. All rights reserved." + }, + "theme": { + "light": "Light Theme", + "dark": "Dark Theme", + "toggle": "Toggle Theme" + }, + "language": { + "english": "English", + "korean": "한국어", + "russian": "Русский", + "kazakh": "Қазақша" + }, + "common": { + "loading": "Loading...", + "error": "Error occurred", + "success": "Success", + "view_more": "View More", + "back": "Back", + "next": "Next", + "previous": "Previous", + "view_details": "View Details" + }, + "meta": { + "description": "SmartSolTech - Innovative web development, mobile app development, UI/UX design services", + "keywords": "web development, mobile apps, UI/UX design, Korea", + "title": "SmartSolTech" + }, + "nav": { + "home": "Home", + "about": "About", + "services": "Services", + "portfolio": "Portfolio", + "calculator": "Calculator" + }, + "admin": { + "login": "Admin Panel Login", + "dashboard": "Dashboard", + "title": "SmartSolTech Admin" + }, + "company": { + "name": "SmartSolTech", + "description": "Digital solution specialist leading innovation", + "email": "info@smartsoltech.kr", + "phone": "+82-10-1234-5678" + }, + "errors": { + "page_not_found": "Page not found", + "error_occurred": "Error occurred", + "title": "Error - SmartSolTech", + "default_title": "An Error Occurred", + "default_message": "A problem occurred while processing the request.", + "back_home": "Back to Home", + "go_back": "Go Back", + "need_help": "Need Help?", + "help_message": "If the problem persists, please contact us anytime.", + "contact_support": "Contact Support" + }, + "pages": { + "home": "Home page", + "about": "About us", + "services": "Services", + "portfolio": "Portfolio", + "contact": "Contact", + "calculator": "Calculator" + }, + "portfolio_page": { + "title": "Our Portfolio", + "subtitle": "Discover innovative projects and creative solutions", + "categories": { + "all": "All", + "web-development": "Web Development", + "mobile-app": "Mobile App", + "ui-ux-design": "UI/UX Design", + "branding": "Branding", + "marketing": "Digital Marketing" + }, + "buttons": { + "details": "View Details", + "projectDetails": "Project Details", + "loadMore": "Load More Projects", + "contact": "Request Project", + "calculate": "Calculate Cost" + }, + "empty": { + "title": "No portfolio yet", + "subtitle": "We'll be showcasing amazing projects soon!" + }, + "cta": { + "title": "Be the star of the next project", + "subtitle": "Create innovative digital solutions with us" + }, + "labels": { + "featured": "FEATURED", + "views": "views", + "likes": "likes" + } + } } \ No newline at end of file diff --git a/locales/kk.json b/locales/kk.json index b607320..e868476 100644 --- a/locales/kk.json +++ b/locales/kk.json @@ -1,318 +1,525 @@ { - "navigation": { - "home": "Басты бет", - "about": "Біз туралы", - "services": "Қызметтер", - "portfolio": "Портфолио", - "contact": "Байланыс", - "calculator": "Калькулятор", - "admin": "Админ", - "home - SmartSolTech": "navigation.home - SmartSolTech" - }, - "hero": { - "title": { - "smart": "hero.title.smart", - "solutions": "hero.title.solutions" - }, - "subtitle": "Шешімдер", - "description": "Инновациялық веб-әзірлеу, мобильді қосымшалар, UI/UX дизайн арқылы бизнестің цифрлық трансформациясын жүргіземіз", - "cta_primary": "Жобаны бастау", - "cta_secondary": "Портфолионы көру", - "cta": { - "start": "hero.cta.start", - "portfolio": "hero.cta.portfolio" - } - }, - "services": { - "title": { - "our": "services.title.our", - "services": "services.title.services" - }, - "title_highlight": "Қызметтер", - "description": "Заманауи технология және шығармашылық идеялармен жасалған цифрлық шешімдер", - "web_development": { - "title": "Веб-әзірлеу", - "description": "Заманауи және бейімделгіш веб-сайттар мен веб-қосымшаларды әзірлеу", - "price": "$5,000~" - }, - "mobile_app": { - "title": "Мобильді қосымшалар", - "description": "iOS және Android үшін нативті және кросс-платформалық қосымшалар", - "price": "$8,000~" - }, - "ui_ux_design": { - "title": "UI/UX дизайн", - "description": "Пайдаланушы-орталықты интуитивті және әдемі интерфейс дизайны", - "price": "$3,000~" - }, - "digital_marketing": { - "title": "Цифрлық маркетинг", - "description": "SEO, әлеуметтік медиа, онлайн жарнама арқылы цифрлық маркетинг", - "price": "$2,000~" - }, - "view_all": "Барлық қызметтерді көру", - "subtitle": "services.subtitle", - "web": { - "title": "services.web.title", - "description": "services.web.description", - "price": "services.web.price" - }, - "mobile": { - "title": "services.mobile.title", - "description": "services.mobile.description", - "price": "services.mobile.price" - }, - "design": { - "title": "services.design.title", - "description": "services.design.description", - "price": "services.design.price" - }, - "marketing": { - "title": "services.marketing.title", - "description": "services.marketing.description", - "price": "services.marketing.price" - } - }, - "portfolio": { - "title": { - "recent": "portfolio.title.recent", - "projects": "portfolio.title.projects" - }, - "title_highlight": "Жобалар", - "description": "Тұтынушылардың табысы үшін аяқталған жобаларды тексеріңіз", - "view_details": "Толығырақ", - "view_all": "Барлық портфолионы көру", - "subtitle": "portfolio.subtitle" - }, - "calculator": { - "title": "Жоба Құнының Калькуляторы", - "subtitle": "Қажетті қызметтер мен талаптарды таңдап, нақты уақытта дәл бағаны алыңыз", - "meta": { - "title": "Жоба құнының калькуляторы", - "description": "Веб-әзірлеу, мобильді қосымша немесе дизайн жобасының құнын біздің интерактивті калькулятормен есептеңіз" - }, - "cta": { - "title": "Жобаның бағасын тексеріңіз", - "subtitle": "Қажетті қызметтер мен талаптарды таңдап, нақты уақытта бағаны есептейміз", - "button": "Құн калькуляторын пайдалану" - }, - "step1": { - "title": "1-қадам: Қызмет таңдау", - "subtitle": "Қажетті қызметтерді таңдаңыз (бірнеше таңдауға болады)" - }, - "step2": { - "title": "2-қадам: Жоба мәліметтері", - "subtitle": "Жобаның күрделілігі мен мерзімін таңдаңыз" - }, - "complexity": { - "title": "Жобаның күрделілігі", - "simple": "Қарапайым", - "simple_desc": "Негізгі функциялар, стандартты дизайн", - "medium": "Орташа", - "medium_desc": "Қосымша функциялар, жеке дизайн", - "complex": "Күрделі", - "complex_desc": "Кеңейтілген функциялар, күрделі интеграциялар" - }, - "timeline": { - "title": "Әзірлеу мерзімі", - "standard": "Стандартты", - "standard_desc": "Қалыпты әзірлеу мерзімі", - "rush": "Асығыс", - "rush_desc": "Жылдам әзірлеу (+50%)", - "extended": "Кеңейтілген", - "extended_desc": "Икемді әзірлеу мерзімі (-20%)" - }, - "result": { - "title": "Есептеу нәтижесі", - "subtitle": "Міне, сіздің алдын ала жоба құнының бағасы", - "estimated_price": "Алдын ала баға", - "price_note": "* Соңғы құн жоба мәліметтеріне байланысты өзгеруі мүмкін", - "summary": "Жоба қорытындысы", - "selected_services": "Таңдалған қызметтер", - "complexity": "Күрделілік", - "timeline": "Мерзім", - "get_quote": "Дәл ұсыныс алу", - "recalculate": "Қайта есептеу", - "contact_note": "Дәл ұсыныс алу және жоба мәліметтерін талқылау үшін бізбен байланысыңыз" - }, - "next_step": "Келесі қадам", - "prev_step": "Артқа", - "calculate": "Есептеу" - }, - "contact": { - "ready_title": "Жобаңызды бастауға дайынсыз ба?", - "ready_description": "Идеяларыңызды шындыққа айналдырыңыз. Сарапшылар ең жақсы шешімдерді ұсынады.", - "phone_consultation": "Телефон кеңесі", - "email_inquiry": "Электрондық пошта сұрауы", - "telegram_chat": "Telegram чаты", - "instant_response": "Лезде жауап беру мүмкін", - "free_consultation": "Тегін кеңес беру өтініші", - "form": { - "name": "Аты", - "email": "Электрондық пошта", - "phone": "Телефон", - "service_interest": "Қызығатын қызмет", - "service_options": { - "select": "Қызығатын қызметті таңдаңыз", - "web_development": "Веб-әзірлеу", - "mobile_app": "Мобильді қосымша", - "ui_ux_design": "UI/UX дизайн", - "branding": "Брендинг", - "consulting": "Кеңес беру", - "other": "Басқа" - }, - "message": "Жобаңыз туралы қысқаша сипаттаңыз", - "submit": "Кеңес беру үшін өтініш беру", - "title": "contact.form.title", - "service": { - "select": "contact.form.service.select", - "web": "contact.form.service.web", - "mobile": "contact.form.service.mobile", - "design": "contact.form.service.design", - "branding": "contact.form.service.branding", - "consulting": "contact.form.service.consulting", - "other": "contact.form.service.other" - }, - "success": "contact.form.success", - "error": "contact.form.error" - }, - "cta": { - "ready": "contact.cta.ready", - "start": "contact.cta.start", - "question": "contact.cta.question", - "subtitle": "contact.cta.subtitle" - }, - "phone": { - "title": "contact.phone.title", - "number": "contact.phone.number" - }, - "email": { - "title": "contact.email.title", - "address": "contact.email.address" - }, - "telegram": { - "title": "contact.telegram.title", - "subtitle": "contact.telegram.subtitle" - } - }, - "about": { - "hero_title": "Туралы", - "hero_highlight": "SmartSolTech", - "hero_description": "Инновациялық технологиямен тұтынушылардың табысына жетелейтін цифрлық шешімдер маманы", - "overview": { - "title": "Инновация мен шығармашылықпен болашақты құру", - "description_1": "SmartSolTech - 2020 жылы құрылған цифрлық шешімдер маманы, веб-әзірлеу, мобильді қосымшалар, UI/UX дизайн салаларында инновациялық технология мен шығармашылық идеялар негізінде тұтынушылардың бизнес табысын қолдайды.", - "description_2": "Біз жай ғана технологияны ұсынып қана қоймаймыз, тұтынушылардың мақсаттарын түсініп, оларға сәйкес оңтайлы шешімдерді ұсынып, бірге өсетін серіктестер болуды мақсат етеміз.", - "stats": { - "projects": "100+", - "projects_label": "Аяқталған жобалар", - "clients": "50+", - "clients_label": "Қанағаттанған тұтынушылар", - "experience": "4 жыл", - "experience_label": "Саладағы тәжірибе" - }, - "mission": "Біздің миссия", - "mission_text": "Технология арқылы барлық бизнестердің цифрлық дәуірде табысқа жетуіне көмектесу", - "vision": "Біздің көзқарас", - "vision_text": "Қазақстанды білдіретін жаһандық цифрлық шешімдер компаниясы ретінде өсіп, бүкіл әлемдегі тұтынушылардың цифрлық инновацияларын басқару" - }, - "values": { - "title": "Негізгі", - "title_highlight": "Құндылықтар", - "description": "SmartSolTech ұстанатын негізгі құндылықтар", - "innovation": { - "title": "Инновация", - "description": "Үздіксіз зерттеу-әзірлеу және заманауи технологияларды енгізу арқылы инновациялық шешімдерді ұсынамыз." - }, - "collaboration": { - "title": "Ынтымақтастық", - "description": "Тұтынушылармен тығыз қарым-қатынас пен ынтымақтастық арқылы ең жақсы нәтижелерді жасаймыз." - }, - "quality": { - "title": "Сапа", - "description": "Жоғары сапа стандарттарын сақтап, тұтынушылар қанағаттана алатын жоғары сапалы өнімдерді ұсынамыз." - }, - "growth": { - "title": "Өсу", - "description": "Тұтынушылармен бірге өсіп, үздіксіз оқу мен дамуды мақсат етеміз." - } - }, - "team": { - "title": "Біздің", - "title_highlight": "Команда", - "description": "Сараптама мен құлшынысты SmartSolTech командасын таныстырамыз" - }, - "tech_stack": { - "title": "Технологиялық", - "title_highlight": "Стек", - "description": "Заманауи технология мен дәлелденген құралдармен ең жақсы шешімдерді ұсынамыз", - "frontend": "Frontend", - "backend": "Backend", - "mobile": "Мобильді" - }, - "cta": { - "title": "Бірге табысқа жететін серіктес болыңыз", - "description": "SmartSolTech-пен бизнесіңізді келесі деңгейге дамытыңыз", - "partnership": "Серіктестік сұрауы", - "portfolio": "Портфолионы көру" - } - }, - "footer": { - "company": { - "description": "footer.company.description" - }, - "description": "Инновацияны басқаратын цифрлық шешімдер маманы", - "quick_links": "Жылдам сілтемелер", - "services": "Қызметтер", - "contact_info": "Байланыс ақпараты", - "follow_us": "Бізді іздеңіз", - "rights": "Барлық құқықтар сақталған.", - "links": { - "title": "footer.links.title" - }, - "contact": { - "title": "footer.contact.title", - "email": "footer.contact.email", - "phone": "footer.contact.phone", - "address": "footer.contact.address" - }, - "copyright": "footer.copyright", - "privacy": "footer.privacy", - "terms": "footer.terms" - }, - "theme": { - "light": "Ашық тема", - "dark": "Қараңғы тема", - "toggle": "Теманы ауыстыру" - }, - "language": { - "english": "English", - "korean": "한국어", - "russian": "Русский", - "kazakh": "Қазақша", - "ko": "language.ko" - }, - "common": { - "loading": "Жүктелуде...", - "error": "Қате орын алды", - "success": "Сәтті", - "view_more": "Көбірек көру", - "back": "Артқа", - "next": "Келесі", - "previous": "Алдыңғы", - "view_details": "common.view_details" - }, - "undefined - SmartSolTech": "undefined - SmartSolTech", - "meta": { - "description": "meta.description", - "keywords": "meta.keywords", - "title": "meta.title" - }, - "nav": { - "home": "nav.home", - "about": "nav.about", - "services": "nav.services", - "portfolio": "nav.portfolio", - "calculator": "nav.calculator" - } + "navigation": { + "home": "Басты бет", + "about": "Біз туралы", + "services": "Қызметтер", + "portfolio": "Портфолио", + "contact": "Байланыс", + "calculator": "Калькулятор", + "admin": "Админ", + "home - SmartSolTech": "navigation.home - SmartSolTech" + }, + "hero": { + "title": { + "smart": "hero.title.smart", + "solutions": "hero.title.solutions" + }, + "subtitle": "Шешімдер", + "description": "Инновациялық веб-әзірлеу, мобильді қосымшалар, UI/UX дизайн арқылы бизнестің цифрлық трансформациясын жүргіземіз", + "cta": { + "start": "hero.cta.start", + "portfolio": "hero.cta.portfolio" + }, + "cta_primary": "Жобаны бастау", + "cta_secondary": "Портфолионы көру" + }, + "services": { + "title": { + "our": "services.title.our", + "services": "services.title.services" + }, + "subtitle": "services.subtitle", + "description": "Заманауи технология және шығармашылық идеялармен жасалған цифрлық шешімдер", + "view_all": "Барлық қызметтерді көру", + "web": { + "title": "services.web.title", + "description": "services.web.description", + "price": "services.web.price" + }, + "mobile": { + "title": "services.mobile.title", + "description": "services.mobile.description", + "price": "services.mobile.price" + }, + "design": { + "title": "services.design.title", + "description": "services.design.description", + "price": "services.design.price" + }, + "marketing": { + "title": "services.marketing.title", + "description": "services.marketing.description", + "price": "services.marketing.price" + }, + "meta": { + "title": "Services", + "description": "Check out SmartSolTech's professional services. Web development, mobile apps, UI/UX design, digital marketing and other technology solutions.", + "keywords": "web development, mobile apps, UI/UX design, digital marketing, technology solutions, SmartSolTech" + }, + "hero": { + "title": "Our", + "title_highlight": "Services", + "subtitle": "Support business growth with innovative technology" + }, + "cards": { + "starting_price": "Starting Price", + "consultation": "consultation", + "contact": "Contact", + "calculate_cost": "Calculate Cost", + "popular": "Popular", + "coming_soon": "Services Coming Soon", + "coming_soon_desc": "We'll soon offer various services!" + }, + "process": { + "title": "Project Implementation Process", + "subtitle": "We conduct projects with systematic and professional processes", + "consultation": { + "title": "Consultation and Planning", + "description": "Accurately understand customer requirements and" + } + }, + "title_highlight": "Қызметтер", + "web_development": { + "title": "Веб-әзірлеу", + "description": "Заманауи және бейімделгіш веб-сайттар мен веб-қосымшаларды әзірлеу", + "price": "$5,000~" + }, + "mobile_app": { + "title": "Мобильді қосымшалар", + "description": "iOS және Android үшін нативті және кросс-платформалық қосымшалар", + "price": "$8,000~" + }, + "ui_ux_design": { + "title": "UI/UX дизайн", + "description": "Пайдаланушы-орталықты интуитивті және әдемі интерфейс дизайны", + "price": "$3,000~" + }, + "digital_marketing": { + "title": "Цифрлық маркетинг", + "description": "SEO, әлеуметтік медиа, онлайн жарнама арқылы цифрлық маркетинг", + "price": "$2,000~" + } + }, + "portfolio": { + "title": { + "recent": "portfolio.title.recent", + "projects": "portfolio.title.projects", + "our": "Our", + "portfolio": "Portfolio" + }, + "subtitle": "portfolio.subtitle", + "description": "Тұтынушылардың табысы үшін аяқталған жобаларды тексеріңіз", + "view_details": "Толығырақ", + "view_all": "Барлық портфолионы көру", + "categories": { + "all": "All", + "web": "Web Development", + "mobile": "Mobile Apps", + "uiux": "UI/UX Design" + }, + "project_details": "Project Details", + "default": { + "ecommerce": "E-commerce", + "title": "E-commerce Platform", + "description": "Modern online commerce solution with intuitive interface" + }, + "meta": { + "title": "Portfolio", + "description": "Check out SmartSolTech's diverse projects and success stories. Web development, mobile apps, UI/UX design portfolio.", + "keywords": "portfolio, web development, mobile apps, UI/UX design, projects, SmartSolTech", + "og_title": "Portfolio - SmartSolTech", + "og_description": "SmartSolTech's diverse projects and success stories" + }, + "title_highlight": "Жобалар", + "view_project": "View Project" + }, + "calculator": { + "title": "Жоба Құнының Калькуляторы", + "subtitle": "Қажетті қызметтер мен талаптарды таңдап, нақты уақытта дәл бағаны алыңыз", + "meta": { + "title": "Жоба құнының калькуляторы", + "description": "Веб-әзірлеу, мобильді қосымша немесе дизайн жобасының құнын біздің интерактивті калькулятормен есептеңіз" + }, + "cta": { + "title": "Жобаның бағасын тексеріңіз", + "subtitle": "Қажетті қызметтер мен талаптарды таңдап, нақты уақытта бағаны есептейміз", + "button": "Құн калькуляторын пайдалану" + }, + "step1": { + "title": "1-қадам: Қызмет таңдау", + "subtitle": "Қажетті қызметтерді таңдаңыз (бірнеше таңдауға болады)" + }, + "step2": { + "title": "2-қадам: Жоба мәліметтері", + "subtitle": "Жобаның күрделілігі мен мерзімін таңдаңыз" + }, + "complexity": { + "title": "Жобаның күрделілігі", + "simple": "Қарапайым", + "simple_desc": "Негізгі функциялар, стандартты дизайн", + "medium": "Орташа", + "medium_desc": "Қосымша функциялар, жеке дизайн", + "complex": "Күрделі", + "complex_desc": "Кеңейтілген функциялар, күрделі интеграциялар" + }, + "timeline": { + "title": "Әзірлеу мерзімі", + "standard": "Стандартты", + "standard_desc": "Қалыпты әзірлеу мерзімі", + "rush": "Асығыс", + "rush_desc": "Жылдам әзірлеу (+50%)", + "extended": "Кеңейтілген", + "extended_desc": "Икемді әзірлеу мерзімі (-20%)" + }, + "result": { + "title": "Есептеу нәтижесі", + "subtitle": "Міне, сіздің алдын ала жоба құнының бағасы", + "estimated_price": "Алдын ала баға", + "price_note": "* Соңғы құн жоба мәліметтеріне байланысты өзгеруі мүмкін", + "summary": "Жоба қорытындысы", + "selected_services": "Таңдалған қызметтер", + "complexity": "Күрделілік", + "timeline": "Мерзім", + "get_quote": "Дәл ұсыныс алу", + "recalculate": "Қайта есептеу", + "contact_note": "Дәл ұсыныс алу және жоба мәліметтерін талқылау үшін бізбен байланысыңыз" + }, + "next_step": "Келесі қадам", + "prev_step": "Артқа", + "calculate": "Есептеу" + }, + "contact": { + "hero": { + "title": "Contact Us", + "subtitle": "We're here to help bring your ideas to life" + }, + "ready_title": "Жобаңызды бастауға дайынсыз ба?", + "ready_description": "Идеяларыңызды шындыққа айналдырыңыз. Сарапшылар ең жақсы шешімдерді ұсынады.", + "form": { + "title": "contact.form.title", + "name": "Аты", + "email": "Электрондық пошта", + "phone": "Телефон", + "message": "Жобаңыз туралы қысқаша сипаттаңыз", + "submit": "Кеңес беру үшін өтініш беру", + "success": "contact.form.success", + "error": "contact.form.error", + "service": { + "title": "Service Interest", + "select": "contact.form.service.select", + "web": "contact.form.service.web", + "mobile": "contact.form.service.mobile", + "design": "contact.form.service.design", + "branding": "contact.form.service.branding", + "consulting": "contact.form.service.consulting", + "other": "contact.form.service.other" + }, + "service_interest": "Қызығатын қызмет", + "service_options": { + "select": "Қызығатын қызметті таңдаңыз", + "web_development": "Веб-әзірлеу", + "mobile_app": "Мобильді қосымша", + "ui_ux_design": "UI/UX дизайн", + "branding": "Брендинг", + "consulting": "Кеңес беру", + "other": "Басқа" + } + }, + "info": { + "title": "Contact Information" + }, + "phone": { + "title": "contact.phone.title", + "number": "contact.phone.number", + "hours": "Mon-Fri 9:00-18:00" + }, + "email": { + "title": "contact.email.title", + "address": "contact.email.address", + "response": "Response within 24 hours" + }, + "telegram": { + "title": "contact.telegram.title", + "subtitle": "contact.telegram.subtitle" + }, + "address": { + "title": "Office Address", + "line1": "123 Teheran-ro, Gangnam-gu", + "line2": "Seoul, South Korea" + }, + "cta": { + "ready": "contact.cta.ready", + "start": "contact.cta.start", + "question": "contact.cta.question", + "subtitle": "contact.cta.subtitle" + }, + "meta": { + "title": "Contact", + "description": "Contact us anytime for project inquiries or consultation" + }, + "phone_consultation": "Телефон кеңесі", + "email_inquiry": "Электрондық пошта сұрауы", + "telegram_chat": "Telegram чаты", + "instant_response": "Лезде жауап беру мүмкін", + "free_consultation": "Тегін кеңес беру өтініші" + }, + "about": { + "hero": { + "title": "About SmartSolTech", + "subtitle": "Creating the future with innovation and technology" + }, + "company": { + "title": "Company Information", + "description1": "SmartSolTech is a technology company established in 2020, recognized for expertise in web development, mobile app development, and UI/UX design.", + "description2": "We accurately understand customer needs and provide innovative solutions using the latest technology." + }, + "stats": { + "projects": "Completed Projects", + "experience": "Years Experience", + "clients": "Satisfied Customers" + }, + "mission": { + "title": "Our Mission", + "description": "Our mission is to support customer business growth through technology and lead digital innovation." + }, + "values": { + "innovation": { + "title": "Инновация", + "description": "Үздіксіз зерттеу-әзірлеу және заманауи технологияларды енгізу арқылы инновациялық шешімдерді ұсынамыз." + }, + "quality": { + "title": "Сапа", + "description": "Жоғары сапа стандарттарын сақтап, тұтынушылар қанағаттана алатын жоғары сапалы өнімдерді ұсынамыз." + }, + "partnership": { + "title": "Partnership", + "description": "We create the best results through close communication and collaboration with customers." + }, + "title": "Негізгі", + "title_highlight": "Құндылықтар", + "description": "SmartSolTech ұстанатын негізгі құндылықтар", + "collaboration": { + "title": "Ынтымақтастық", + "description": "Тұтынушылармен тығыз қарым-қатынас пен ынтымақтастық арқылы ең жақсы нәтижелерді жасаймыз." + }, + "growth": { + "title": "Өсу", + "description": "Тұтынушылармен бірге өсіп, үздіксіз оқу мен дамуды мақсат етеміз." + } + }, + "cta": { + "title": "Бірге табысқа жететін серіктес болыңыз", + "subtitle": "Turn your ideas into reality", + "button": "Contact Us", + "description": "SmartSolTech-пен бизнесіңізді келесі деңгейге дамытыңыз", + "partnership": "Серіктестік сұрауы", + "portfolio": "Портфолионы көру" + }, + "meta": { + "title": "About Us", + "description": "SmartSolTech is a professional development company that supports customer business growth with innovative technology" + }, + "hero_title": "Туралы", + "hero_highlight": "SmartSolTech", + "hero_description": "Инновациялық технологиямен тұтынушылардың табысына жетелейтін цифрлық шешімдер маманы", + "overview": { + "title": "Инновация мен шығармашылықпен болашақты құру", + "description_1": "SmartSolTech - 2020 жылы құрылған цифрлық шешімдер маманы, веб-әзірлеу, мобильді қосымшалар, UI/UX дизайн салаларында инновациялық технология мен шығармашылық идеялар негізінде тұтынушылардың бизнес табысын қолдайды.", + "description_2": "Біз жай ғана технологияны ұсынып қана қоймаймыз, тұтынушылардың мақсаттарын түсініп, оларға сәйкес оңтайлы шешімдерді ұсынып, бірге өсетін серіктестер болуды мақсат етеміз.", + "stats": { + "projects": "100+", + "projects_label": "Аяқталған жобалар", + "clients": "50+", + "clients_label": "Қанағаттанған тұтынушылар", + "experience": "4 жыл", + "experience_label": "Саладағы тәжірибе" + }, + "mission": "Біздің миссия", + "mission_text": "Технология арқылы барлық бизнестердің цифрлық дәуірде табысқа жетуіне көмектесу", + "vision": "Біздің көзқарас", + "vision_text": "Қазақстанды білдіретін жаһандық цифрлық шешімдер компаниясы ретінде өсіп, бүкіл әлемдегі тұтынушылардың цифрлық инновацияларын басқару" + }, + "team": { + "title": "Біздің", + "title_highlight": "Команда", + "description": "Сараптама мен құлшынысты SmartSolTech командасын таныстырамыз" + }, + "tech_stack": { + "title": "Технологиялық", + "title_highlight": "Стек", + "description": "Заманауи технология мен дәлелденген құралдармен ең жақсы шешімдерді ұсынамыз", + "frontend": "Frontend", + "backend": "Backend", + "mobile": "Мобильді" + } + }, + "footer": { + "description": "Инновацияны басқаратын цифрлық шешімдер маманы", + "links": { + "title": "footer.links.title", + "privacy": "Privacy Policy", + "terms": "Terms of Service", + "sitemap": "Sitemap" + }, + "contact": { + "title": "footer.contact.title", + "email": "footer.contact.email", + "phone": "footer.contact.phone", + "address": "footer.contact.address" + }, + "copyright": "footer.copyright", + "company": { + "description": "footer.company.description" + }, + "quick_links": "Жылдам сілтемелер", + "services": "Қызметтер", + "contact_info": "Байланыс ақпараты", + "follow_us": "Бізді іздеңіз", + "rights": "Барлық құқықтар сақталған.", + "privacy": "footer.privacy", + "terms": "footer.terms", + "social": { + "follow": "Follow Us" + } + }, + "theme": { + "light": "Ашық тема", + "dark": "Қараңғы тема", + "toggle": "Теманы ауыстыру" + }, + "language": { + "english": "English", + "korean": "한국어", + "russian": "Русский", + "kazakh": "Қазақша", + "ko": "language.ko" + }, + "common": { + "loading": "Жүктелуде...", + "error": "Қате орын алды", + "success": "Сәтті", + "view_more": "Көбірек көру", + "back": "Артқа", + "next": "Келесі", + "previous": "Алдыңғы", + "view_details": "common.view_details" + }, + "meta": { + "description": "meta.description", + "keywords": "meta.keywords", + "title": "meta.title" + }, + "nav": { + "home": "nav.home", + "about": "nav.about", + "services": "nav.services", + "portfolio": "nav.portfolio", + "calculator": "nav.calculator" + }, + "admin": { + "login": "Admin Panel Login", + "dashboard": "Dashboard", + "title": "SmartSolTech Admin", + "login_title": "Admin Panel Login", + "login_subtitle": "Login to your account to manage the site", + "login_button": "Login", + "email": "Email", + "password": "Password", + "email_placeholder": "admin@smartsoltech.com", + "password_placeholder": "Enter password", + "back_to_site": "Back to site", + "dashboard_subtitle": "Overview of main site metrics", + "portfolio": "Portfolio", + "services": "Services", + "contacts": "Messages", + "settings": "Settings", + "users": "Users", + "logout": "Logout", + "view_site": "View site", + "view_all": "View all", + "portfolio_projects": "Projects", + "contact_messages": "Messages", + "recent_portfolio": "Recent projects", + "recent_contacts": "Recent messages", + "no_recent_portfolio": "No recent projects", + "no_recent_contacts": "No recent messages", + "quick_actions": "Quick actions", + "add_portfolio": "Add project", + "add_service": "Add service", + "site_settings": "Site settings", + "banner_editor": "Banner Editor", + "current_banner": "Current banner", + "pages": { + "home": "Home page", + "about": "About us", + "services": "Services", + "portfolio": "Portfolio" + } + }, + "company": { + "name": "SmartSolTech", + "description": "Digital solution specialist leading innovation", + "email": "info@smartsoltech.kr", + "phone": "+82-10-1234-5678", + "full_name": "SmartSolTech - Innovative Technology Solutions", + "tagline": "Future begins here", + "address": "Seoul, South Korea", + "social": { + "telegram": "@smartsoltech" + } + }, + "errors": { + "page_not_found": "Page not found", + "error_occurred": "Error occurred", + "title": "Error - SmartSolTech", + "default_title": "An Error Occurred", + "default_message": "A problem occurred while processing the request.", + "back_home": "Back to Home", + "go_back": "Go Back", + "need_help": "Need Help?", + "help_message": "If the problem persists, please contact us anytime.", + "contact_support": "Contact Support", + "contact_us": "Contact us" + }, + "pages": { + "home": "Home page", + "about": "About us", + "services": "Services", + "portfolio": "Portfolio", + "contact": "Contact", + "calculator": "Calculator" + }, + "portfolio_page": { + "title": "Our Portfolio", + "subtitle": "Discover innovative projects and creative solutions", + "categories": { + "all": "All", + "web-development": "Web Development", + "mobile-app": "Mobile App", + "ui-ux-design": "UI/UX Design", + "branding": "Branding", + "marketing": "Digital Marketing" + }, + "buttons": { + "details": "View Details", + "projectDetails": "Project Details", + "loadMore": "Load More Projects", + "contact": "Request Project", + "calculate": "Calculate Cost" + }, + "empty": { + "title": "No portfolio yet", + "subtitle": "We'll be showcasing amazing projects soon!" + }, + "cta": { + "title": "Be the star of the next project", + "subtitle": "Create innovative digital solutions with us" + }, + "labels": { + "featured": "FEATURED", + "views": "views", + "likes": "likes" + } + }, + "undefined - SmartSolTech": "undefined - SmartSolTech" } \ No newline at end of file diff --git a/locales/ko.json b/locales/ko.json index 2d12a27..ea46c6e 100644 --- a/locales/ko.json +++ b/locales/ko.json @@ -1,196 +1,331 @@ { - "undefined - SmartSolTech": "undefined - SmartSolTech", - "meta": { - "description": "meta.description", - "keywords": "meta.keywords", - "title": "meta.title" - }, - "navigation": { - "home": "navigation.home", - "about": "navigation.about", - "services": "navigation.services", - "portfolio": "navigation.portfolio", - "calculator": "navigation.calculator", - "contact": "navigation.contact", - "home - SmartSolTech": "navigation.home - SmartSolTech" - }, - "language": { - "ko": "language.ko", - "korean": "language.korean", - "english": "language.english", - "russian": "language.russian", - "kazakh": "language.kazakh" - }, - "theme": { - "toggle": "theme.toggle" - }, - "hero": { - "cta_primary": "hero.cta_primary", - "title": { - "smart": "hero.title.smart", - "solutions": "hero.title.solutions" - }, - "subtitle": "hero.subtitle", - "cta": { - "start": "hero.cta.start", - "portfolio": "hero.cta.portfolio" - } - }, - "services": { - "title": { - "our": "services.title.our", - "services": "services.title.services" - }, - "subtitle": "services.subtitle", - "web": { - "title": "services.web.title", - "description": "services.web.description", - "price": "services.web.price" - }, - "mobile": { - "title": "services.mobile.title", - "description": "services.mobile.description", - "price": "services.mobile.price" - }, - "design": { - "title": "services.design.title", - "description": "services.design.description", - "price": "services.design.price" - }, - "marketing": { - "title": "services.marketing.title", - "description": "services.marketing.description", - "price": "services.marketing.price" - }, - "view_all": "services.view_all" - }, - "portfolio": { - "title": { - "recent": "portfolio.title.recent", - "projects": "portfolio.title.projects" - }, - "subtitle": "portfolio.subtitle", - "view_all": "portfolio.view_all" - }, - "common": { - "view_details": "common.view_details" - }, - "calculator": { - "cta": { - "title": "calculator.cta.title", - "subtitle": "calculator.cta.subtitle", - "button": "calculator.cta.button" - }, - "meta": { - "title": "calculator.meta.title", - "description": "calculator.meta.description" - }, - "title": "calculator.title", - "subtitle": "calculator.subtitle", - "step1": { - "title": "calculator.step1.title", - "subtitle": "calculator.step1.subtitle" - }, - "next_step": "calculator.next_step", - "step2": { - "title": "calculator.step2.title", - "subtitle": "calculator.step2.subtitle" - }, - "complexity": { - "title": "calculator.complexity.title", - "simple": "calculator.complexity.simple", - "simple_desc": "calculator.complexity.simple_desc", - "medium": "calculator.complexity.medium", - "medium_desc": "calculator.complexity.medium_desc", - "complex": "calculator.complexity.complex", - "complex_desc": "calculator.complexity.complex_desc" - }, - "timeline": { - "title": "calculator.timeline.title", - "standard": "calculator.timeline.standard", - "standard_desc": "calculator.timeline.standard_desc", - "rush": "calculator.timeline.rush", - "rush_desc": "calculator.timeline.rush_desc", - "extended": "calculator.timeline.extended", - "extended_desc": "calculator.timeline.extended_desc" - }, - "prev_step": "calculator.prev_step", - "calculate": "calculator.calculate", - "result": { - "title": "calculator.result.title", - "subtitle": "calculator.result.subtitle", - "estimated_price": "calculator.result.estimated_price", - "price_note": "calculator.result.price_note", - "summary": "calculator.result.summary", - "get_quote": "calculator.result.get_quote", - "recalculate": "calculator.result.recalculate", - "contact_note": "calculator.result.contact_note", - "selected_services": "선택된 서비스", - "complexity": "복잡도", - "timeline": "개발 기간" - } - }, - "contact": { - "cta": { - "ready": "contact.cta.ready", - "start": "contact.cta.start", - "question": "contact.cta.question", - "subtitle": "contact.cta.subtitle" - }, - "phone": { - "title": "contact.phone.title", - "number": "contact.phone.number" - }, - "email": { - "title": "contact.email.title", - "address": "contact.email.address" - }, - "telegram": { - "title": "contact.telegram.title", - "subtitle": "contact.telegram.subtitle" - }, - "form": { - "title": "contact.form.title", - "name": "contact.form.name", - "email": "contact.form.email", - "phone": "contact.form.phone", - "service": { - "select": "contact.form.service.select", - "web": "contact.form.service.web", - "mobile": "contact.form.service.mobile", - "design": "contact.form.service.design", - "branding": "contact.form.service.branding", - "consulting": "contact.form.service.consulting", - "other": "contact.form.service.other" - }, - "message": "contact.form.message", - "submit": "contact.form.submit", - "success": "contact.form.success", - "error": "contact.form.error" - } - }, - "footer": { - "company": { - "description": "footer.company.description" - }, - "links": { - "title": "footer.links.title" - }, - "contact": { - "title": "footer.contact.title", - "email": "footer.contact.email", - "phone": "footer.contact.phone", - "address": "footer.contact.address" - }, - "copyright": "footer.copyright", - "privacy": "footer.privacy", - "terms": "footer.terms" - }, - "nav": { - "home": "nav.home", - "about": "nav.about", - "services": "nav.services", - "portfolio": "nav.portfolio", - "calculator": "nav.calculator" - } + "navigation": { + "home": "Home", + "about": "About", + "services": "Services", + "portfolio": "Portfolio", + "contact": "Contact", + "calculator": "Calculator", + "admin": "Admin" + }, + "hero": { + "title": { + "smart": "Smart", + "solutions": "Solutions" + }, + "subtitle": "Grow your business with innovative technology", + "description": "Innovative web development, mobile apps, UI/UX design leading your business digital transformation", + "cta": { + "start": "Get Started", + "portfolio": "View Portfolio" + } + }, + "services": { + "title": { + "our": "Our", + "services": "Services" + }, + "subtitle": "Professional development services to turn your ideas into reality", + "description": "Digital solutions completed with cutting-edge technology and creative ideas", + "view_all": "View All Services", + "web": { + "title": "Web Development", + "description": "Responsive websites and web application development", + "price": "From $500" + }, + "mobile": { + "title": "Mobile Apps", + "description": "iOS and Android native app development", + "price": "From $1,000" + }, + "design": { + "title": "UI/UX Design", + "description": "User-centered interface and experience design", + "price": "From $300" + }, + "marketing": { + "title": "Digital Marketing", + "description": "SEO, social media marketing, advertising management", + "price": "From $200" + }, + "meta": { + "title": "Services", + "description": "Check out SmartSolTech's professional services. Web development, mobile apps, UI/UX design, digital marketing and other technology solutions.", + "keywords": "web development, mobile apps, UI/UX design, digital marketing, technology solutions, SmartSolTech" + }, + "hero": { + "title": "Our", + "title_highlight": "Services", + "subtitle": "Support business growth with innovative technology" + }, + "cards": { + "starting_price": "Starting Price", + "consultation": "consultation", + "contact": "Contact", + "calculate_cost": "Calculate Cost", + "popular": "Popular", + "coming_soon": "Services Coming Soon", + "coming_soon_desc": "We'll soon offer various services!" + }, + "process": { + "title": "Project Implementation Process", + "subtitle": "We conduct projects with systematic and professional processes", + "consultation": { + "title": "Consultation and Planning", + "description": "Accurately understand customer requirements" + } + } + }, + "portfolio": { + "title": { + "recent": "Recent", + "projects": "Projects" + }, + "subtitle": "Check out successfully completed projects", + "description": "Check out the projects completed for customer success", + "view_details": "View Details", + "view_all": "View All Portfolio", + "categories": { + "all": "All", + "web": "Web Development", + "mobile": "Mobile Apps", + "uiux": "UI/UX Design" + }, + "project_details": "Project Details", + "default": { + "ecommerce": "E-commerce", + "title": "E-commerce Platform", + "description": "Modern online commerce solution with intuitive interface" + }, + "meta": { + "title": "Portfolio", + "description": "Check out SmartSolTech's diverse projects and success stories. Web development, mobile apps, UI/UX design portfolio.", + "keywords": "portfolio, web development, mobile apps, UI/UX design, projects, SmartSolTech" + } + }, + "calculator": { + "title": "Project Cost Calculator", + "subtitle": "Select your desired services and requirements to get accurate cost estimates in real time", + "meta": { + "title": "Project Cost Calculator", + "description": "Calculate the cost of your web development, mobile app, or design project with our interactive calculator" + }, + "cta": { + "title": "Check Your Project Estimate", + "subtitle": "Select your desired services and requirements to calculate costs in real time", + "button": "Use Cost Calculator" + } + }, + "contact": { + "hero": { + "title": "Contact Us", + "subtitle": "We're here to help bring your ideas to life" + }, + "ready_title": "Ready to Start Your Project?", + "ready_description": "Turn your ideas into reality. Experts provide the best solutions.", + "form": { + "title": "Project Inquiry", + "name": "Name", + "email": "Email", + "phone": "Phone", + "message": "Message", + "submit": "Send Inquiry", + "success": "Inquiry sent successfully", + "error": "Error occurred while sending inquiry", + "service": { + "title": "Service Interest", + "select": "Select service of interest", + "web": "Web Development", + "mobile": "Mobile App", + "design": "UI/UX Design", + "branding": "Branding", + "consulting": "Consulting", + "other": "Other" + } + }, + "info": { + "title": "Contact Information" + }, + "phone": { + "title": "Phone Inquiry", + "number": "+82-2-1234-5678", + "hours": "Mon-Fri 9:00-18:00" + }, + "email": { + "title": "Email Inquiry", + "address": "info@smartsoltech.co.kr", + "response": "Response within 24 hours" + }, + "telegram": { + "title": "Telegram", + "subtitle": "For quick response" + }, + "address": { + "title": "Office Address", + "line1": "123 Teheran-ro, Gangnam-gu", + "line2": "Seoul, South Korea" + }, + "cta": { + "ready": "Ready?", + "start": "Get Started", + "question": "Have questions?", + "subtitle": "We provide consultation on projects" + }, + "meta": { + "title": "Contact", + "description": "Contact us anytime for project inquiries or consultation" + } + }, + "about": { + "hero": { + "title": "About SmartSolTech", + "subtitle": "Creating the future with innovation and technology" + }, + "company": { + "title": "Company Information", + "description1": "SmartSolTech is a technology company established in 2020, recognized for expertise in web development, mobile app development, and UI/UX design.", + "description2": "We accurately understand customer needs and provide innovative solutions using the latest technology." + }, + "stats": { + "projects": "Completed Projects", + "experience": "Years Experience", + "clients": "Satisfied Customers" + }, + "mission": { + "title": "Our Mission", + "description": "Our mission is to support customer business growth through technology and lead digital innovation." + }, + "values": { + "innovation": { + "title": "Innovation", + "description": "We provide innovative solutions through continuous R&D and adoption of cutting-edge technology." + }, + "quality": { + "title": "Quality", + "description": "We maintain high quality standards and provide high-quality products that customers can be satisfied with." + }, + "partnership": { + "title": "Partnership", + "description": "We create the best results through close communication and collaboration with customers." + } + }, + "cta": { + "title": "We'll Grow Together", + "subtitle": "Turn your ideas into reality", + "button": "Contact Us" + }, + "meta": { + "title": "About Us", + "description": "SmartSolTech is a professional development company that supports customer business growth with innovative technology" + } + }, + "footer": { + "description": "Digital solution specialist leading innovation", + "links": { + "title": "Quick Links" + }, + "contact": { + "title": "Contact", + "email": "info@smartsoltech.co.kr", + "phone": "+82-2-1234-5678", + "address": "123 Teheran-ro, Gangnam-gu, Seoul" + }, + "copyright": "© 2024 SmartSolTech. All rights reserved." + }, + "theme": { + "light": "Light Theme", + "dark": "Dark Theme", + "toggle": "Toggle Theme" + }, + "language": { + "english": "English", + "korean": "한국어", + "russian": "Русский", + "kazakh": "Қазақша" + }, + "common": { + "loading": "Loading...", + "error": "Error occurred", + "success": "Success", + "view_more": "View More", + "back": "Back", + "next": "Next", + "previous": "Previous", + "view_details": "View Details" + }, + "meta": { + "description": "SmartSolTech - Innovative web development, mobile app development, UI/UX design services", + "keywords": "web development, mobile apps, UI/UX design, Korea", + "title": "SmartSolTech" + }, + "nav": { + "home": "Home", + "about": "About", + "services": "Services", + "portfolio": "Portfolio", + "calculator": "Calculator" + }, + "admin": { + "login": "Admin Panel Login", + "dashboard": "Dashboard", + "title": "SmartSolTech Admin" + }, + "company": { + "name": "SmartSolTech", + "description": "Digital solution specialist leading innovation", + "email": "info@smartsoltech.kr", + "phone": "+82-10-1234-5678" + }, + "errors": { + "page_not_found": "Page not found", + "error_occurred": "Error occurred", + "title": "Error - SmartSolTech", + "default_title": "An Error Occurred", + "default_message": "A problem occurred while processing the request.", + "back_home": "Back to Home", + "go_back": "Go Back", + "need_help": "Need Help?", + "help_message": "If the problem persists, please contact us anytime.", + "contact_support": "Contact Support" + }, + "pages": { + "home": "Home page", + "about": "About us", + "services": "Services", + "portfolio": "Portfolio", + "contact": "Contact", + "calculator": "Calculator" + }, + "portfolio_page": { + "title": "우리의 포트폴리오", + "subtitle": "혁신적인 프로젝트와 창의적인 솔루션들을 만나보세요", + "categories": { + "all": "전체", + "web-development": "웹 개발", + "mobile-app": "모바일 앱", + "ui-ux-design": "UI/UX 디자인", + "branding": "브랜딩", + "marketing": "디지털 마케팅" + }, + "buttons": { + "details": "자세히 보기", + "projectDetails": "프로젝트 상세보기", + "loadMore": "더 많은 프로젝트 보기", + "contact": "프로젝트 문의하기", + "calculate": "비용 계산하기" + }, + "empty": { + "title": "아직 포트폴리오가 없습니다", + "subtitle": "곧 멋진 프로젝트들을 공개할 예정입니다!" + }, + "cta": { + "title": "다음 프로젝트의 주인공이 되어보세요", + "subtitle": "우리와 함께 혁신적인 디지털 솔루션을 만들어보세요" + }, + "labels": { + "featured": "추천", + "views": "조회수", + "likes": "좋아요" + } + } } \ No newline at end of file diff --git a/locales/ru.json b/locales/ru.json index 0cdb277..538c0a2 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -1,318 +1,331 @@ { - "navigation": { - "home": "Главная", - "about": "О нас", - "services": "Услуги", - "portfolio": "Портфолио", - "contact": "Контакты", - "calculator": "Калькулятор", - "admin": "Админ", - "home - SmartSolTech": "navigation.home - SmartSolTech" - }, - "hero": { - "title": { - "smart": "hero.title.smart", - "solutions": "hero.title.solutions" - }, - "subtitle": "Решения", - "description": "Инновационная веб-разработка, мобильные приложения, UI/UX дизайн для цифровой трансформации вашего бизнеса", - "cta_primary": "Начать проект", - "cta_secondary": "Посмотреть портфолио", - "cta": { - "start": "hero.cta.start", - "portfolio": "hero.cta.portfolio" - } - }, - "services": { - "title": { - "our": "services.title.our", - "services": "services.title.services" - }, - "title_highlight": "Услуги", - "description": "Цифровые решения с использованием передовых технологий и творческих идей", - "web_development": { - "title": "Веб-разработка", - "description": "Современные и адаптивные веб-сайты и веб-приложения", - "price": "$5,000~" - }, - "mobile_app": { - "title": "Мобильные приложения", - "description": "Нативные и кроссплатформенные приложения для iOS и Android", - "price": "$8,000~" - }, - "ui_ux_design": { - "title": "UI/UX дизайн", - "description": "Ориентированный на пользователя интуитивный и красивый дизайн интерфейса", - "price": "$3,000~" - }, - "digital_marketing": { - "title": "Цифровой маркетинг", - "description": "Цифровой маркетинг через SEO, социальные сети, онлайн-рекламу", - "price": "$2,000~" - }, - "view_all": "Посмотреть все услуги", - "subtitle": "services.subtitle", - "web": { - "title": "services.web.title", - "description": "services.web.description", - "price": "services.web.price" - }, - "mobile": { - "title": "services.mobile.title", - "description": "services.mobile.description", - "price": "services.mobile.price" - }, - "design": { - "title": "services.design.title", - "description": "services.design.description", - "price": "services.design.price" - }, - "marketing": { - "title": "services.marketing.title", - "description": "services.marketing.description", - "price": "services.marketing.price" - } - }, - "portfolio": { - "title": { - "recent": "portfolio.title.recent", - "projects": "portfolio.title.projects" - }, - "title_highlight": "Проекты", - "description": "Ознакомьтесь с проектами, выполненными для успеха клиентов", - "view_details": "Подробнее", - "view_all": "Посмотреть все портфолио", - "subtitle": "portfolio.subtitle" - }, - "calculator": { - "title": "Калькулятор Стоимости Проекта", - "subtitle": "Выберите нужные услуги и требования для получения точной оценки стоимости в режиме реального времени", - "meta": { - "title": "Калькулятор стоимости проекта", - "description": "Рассчитайте стоимость вашего проекта веб-разработки, мобильного приложения или дизайна с помощью нашего интерактивного калькулятора" - }, - "cta": { - "title": "Узнайте стоимость вашего проекта", - "subtitle": "Выберите необходимые услуги и требования, и мы рассчитаем стоимость в режиме реального времени", - "button": "Использовать калькулятор стоимости" - }, - "step1": { - "title": "Шаг 1: Выбор услуг", - "subtitle": "Выберите необходимые услуги (можно выбрать несколько)" - }, - "step2": { - "title": "Шаг 2: Детали проекта", - "subtitle": "Выберите сложность проекта и сроки" - }, - "complexity": { - "title": "Сложность проекта", - "simple": "Простой", - "simple_desc": "Базовый функционал, стандартный дизайн", - "medium": "Средний", - "medium_desc": "Дополнительные функции, кастомный дизайн", - "complex": "Сложный", - "complex_desc": "Расширенный функционал, интеграции" - }, - "timeline": { - "title": "Временные рамки", - "standard": "Стандартные", - "standard_desc": "Обычные сроки разработки", - "rush": "Срочно", - "rush_desc": "Ускоренная разработка (+50%)", - "extended": "Расширенные", - "extended_desc": "Длительная разработка (-20%)" - }, - "result": { - "title": "Результат расчета", - "subtitle": "Вот ваша предварительная оценка стоимости проекта", - "estimated_price": "Предварительная стоимость", - "price_note": "* Окончательная стоимость может варьироваться в зависимости от деталей проекта", - "summary": "Сводка проекта", - "selected_services": "Выбранные услуги", - "complexity": "Сложность", - "timeline": "Временные рамки", - "get_quote": "Получить точное предложение", - "recalculate": "Пересчитать", - "contact_note": "Свяжитесь с нами для получения точного предложения и обсуждения деталей проекта" - }, - "next_step": "Следующий шаг", - "prev_step": "Назад", - "calculate": "Рассчитать" - }, - "contact": { - "ready_title": "Готовы начать свой проект?", - "ready_description": "Превратите свои идеи в реальность. Эксперты предоставят лучшие решения.", - "phone_consultation": "Телефонная консультация", - "email_inquiry": "Запрос по электронной почте", - "telegram_chat": "Чат в Telegram", - "instant_response": "Мгновенный ответ доступен", - "free_consultation": "Заявка на бесплатную консультацию", - "form": { - "name": "Имя", - "email": "Электронная почта", - "phone": "Телефон", - "service_interest": "Интересующая услуга", - "service_options": { - "select": "Выберите интересующую услугу", - "web_development": "Веб-разработка", - "mobile_app": "Мобильное приложение", - "ui_ux_design": "UI/UX дизайн", - "branding": "Брендинг", - "consulting": "Консалтинг", - "other": "Другое" - }, - "message": "Кратко опишите ваш проект", - "submit": "Подать заявку на консультацию", - "title": "contact.form.title", - "service": { - "select": "contact.form.service.select", - "web": "contact.form.service.web", - "mobile": "contact.form.service.mobile", - "design": "contact.form.service.design", - "branding": "contact.form.service.branding", - "consulting": "contact.form.service.consulting", - "other": "contact.form.service.other" - }, - "success": "contact.form.success", - "error": "contact.form.error" - }, - "cta": { - "ready": "contact.cta.ready", - "start": "contact.cta.start", - "question": "contact.cta.question", - "subtitle": "contact.cta.subtitle" - }, - "phone": { - "title": "contact.phone.title", - "number": "contact.phone.number" - }, - "email": { - "title": "contact.email.title", - "address": "contact.email.address" - }, - "telegram": { - "title": "contact.telegram.title", - "subtitle": "contact.telegram.subtitle" - } - }, - "about": { - "hero_title": "О", - "hero_highlight": "SmartSolTech", - "hero_description": "Специалист по цифровым решениям, ведущий к успеху клиентов с помощью инновационных технологий", - "overview": { - "title": "Создавая будущее с инновациями и креативностью", - "description_1": "SmartSolTech - это специалист по цифровым решениям, основанный в 2020 году, поддерживающий успех клиентского бизнеса с помощью инновационных технологий и творческих идей в области веб-разработки, мобильных приложений и UI/UX дизайна.", - "description_2": "Мы не просто предоставляем технологии, но понимаем цели клиентов и предлагаем оптимальные решения, чтобы стать партнерами, растущими вместе.", - "stats": { - "projects": "100+", - "projects_label": "Завершенные проекты", - "clients": "50+", - "clients_label": "Довольные клиенты", - "experience": "4 года", - "experience_label": "Опыт в отрасли" - }, - "mission": "Наша миссия", - "mission_text": "Помощь всем предприятиям в достижении успеха в цифровую эпоху с помощью технологий", - "vision": "Наше видение", - "vision_text": "Рост как глобальной компании цифровых решений, представляющей Корею, для ведения цифровых инноваций для клиентов по всему миру" - }, - "values": { - "title": "Основные", - "title_highlight": "Ценности", - "description": "Основные ценности, которых придерживается SmartSolTech", - "innovation": { - "title": "Инновации", - "description": "Мы предоставляем инновационные решения через непрерывные исследования и внедрение передовых технологий." - }, - "collaboration": { - "title": "Сотрудничество", - "description": "Мы создаем лучшие результаты через тесное общение и сотрудничество с клиентами." - }, - "quality": { - "title": "Качество", - "description": "Мы поддерживаем высокие стандарты качества и предоставляем высококачественные продукты, которыми клиенты могут быть довольны." - }, - "growth": { - "title": "Рост", - "description": "Мы растем вместе с клиентами и стремимся к непрерывному обучению и развитию." - } - }, - "team": { - "title": "Наша", - "title_highlight": "Команда", - "description": "Представляем команду SmartSolTech с экспертизой и страстью" - }, - "tech_stack": { - "title": "Технологический", - "title_highlight": "Стек", - "description": "Мы предоставляем лучшие решения с передовыми технологиями и проверенными инструментами", - "frontend": "Frontend", - "backend": "Backend", - "mobile": "Мобильные" - }, - "cta": { - "title": "Станьте партнером для совместного успеха", - "description": "Выведите свой бизнес на следующий уровень с SmartSolTech", - "partnership": "Запрос о партнерстве", - "portfolio": "Посмотреть портфолио" - } - }, - "footer": { - "company": { - "description": "footer.company.description" - }, - "description": "Специалист по цифровым решениям, ведущий инновации", - "quick_links": "Быстрые ссылки", - "services": "Услуги", - "contact_info": "Контактная информация", - "follow_us": "Подписывайтесь", - "rights": "Все права защищены.", - "links": { - "title": "footer.links.title" - }, - "contact": { - "title": "footer.contact.title", - "email": "footer.contact.email", - "phone": "footer.contact.phone", - "address": "footer.contact.address" - }, - "copyright": "footer.copyright", - "privacy": "footer.privacy", - "terms": "footer.terms" - }, - "theme": { - "light": "Светлая тема", - "dark": "Темная тема", - "toggle": "Переключить тему" - }, - "language": { - "english": "English", - "korean": "한국어", - "russian": "Русский", - "kazakh": "Қазақша", - "ko": "language.ko" - }, - "common": { - "loading": "Загрузка...", - "error": "Произошла ошибка", - "success": "Успешно", - "view_more": "Посмотреть еще", - "back": "Назад", - "next": "Далее", - "previous": "Предыдущий", - "view_details": "common.view_details" - }, - "undefined - SmartSolTech": "undefined - SmartSolTech", - "meta": { - "description": "meta.description", - "keywords": "meta.keywords", - "title": "meta.title" - }, - "nav": { - "home": "nav.home", - "about": "nav.about", - "services": "nav.services", - "portfolio": "nav.portfolio", - "calculator": "nav.calculator" - } + "navigation": { + "home": "Home", + "about": "About", + "services": "Services", + "portfolio": "Portfolio", + "contact": "Contact", + "calculator": "Calculator", + "admin": "Admin" + }, + "hero": { + "title": { + "smart": "Smart", + "solutions": "Solutions" + }, + "subtitle": "Grow your business with innovative technology", + "description": "Innovative web development, mobile apps, UI/UX design leading your business digital transformation", + "cta": { + "start": "Get Started", + "portfolio": "View Portfolio" + } + }, + "services": { + "title": { + "our": "Our", + "services": "Services" + }, + "subtitle": "Professional development services to turn your ideas into reality", + "description": "Digital solutions completed with cutting-edge technology and creative ideas", + "view_all": "View All Services", + "web": { + "title": "Web Development", + "description": "Responsive websites and web application development", + "price": "From $500" + }, + "mobile": { + "title": "Mobile Apps", + "description": "iOS and Android native app development", + "price": "From $1,000" + }, + "design": { + "title": "UI/UX Design", + "description": "User-centered interface and experience design", + "price": "From $300" + }, + "marketing": { + "title": "Digital Marketing", + "description": "SEO, social media marketing, advertising management", + "price": "From $200" + }, + "meta": { + "title": "Services", + "description": "Check out SmartSolTech's professional services. Web development, mobile apps, UI/UX design, digital marketing and other technology solutions.", + "keywords": "web development, mobile apps, UI/UX design, digital marketing, technology solutions, SmartSolTech" + }, + "hero": { + "title": "Our", + "title_highlight": "Services", + "subtitle": "Support business growth with innovative technology" + }, + "cards": { + "starting_price": "Starting Price", + "consultation": "consultation", + "contact": "Contact", + "calculate_cost": "Calculate Cost", + "popular": "Popular", + "coming_soon": "Services Coming Soon", + "coming_soon_desc": "We'll soon offer various services!" + }, + "process": { + "title": "Project Implementation Process", + "subtitle": "We conduct projects with systematic and professional processes", + "consultation": { + "title": "Consultation and Planning", + "description": "Accurately understand customer requirements" + } + } + }, + "portfolio": { + "title": { + "recent": "Recent", + "projects": "Projects" + }, + "subtitle": "Check out successfully completed projects", + "description": "Check out the projects completed for customer success", + "view_details": "View Details", + "view_all": "View All Portfolio", + "categories": { + "all": "All", + "web": "Web Development", + "mobile": "Mobile Apps", + "uiux": "UI/UX Design" + }, + "project_details": "Project Details", + "default": { + "ecommerce": "E-commerce", + "title": "E-commerce Platform", + "description": "Modern online commerce solution with intuitive interface" + }, + "meta": { + "title": "Portfolio", + "description": "Check out SmartSolTech's diverse projects and success stories. Web development, mobile apps, UI/UX design portfolio.", + "keywords": "portfolio, web development, mobile apps, UI/UX design, projects, SmartSolTech" + } + }, + "calculator": { + "title": "Project Cost Calculator", + "subtitle": "Select your desired services and requirements to get accurate cost estimates in real time", + "meta": { + "title": "Project Cost Calculator", + "description": "Calculate the cost of your web development, mobile app, or design project with our interactive calculator" + }, + "cta": { + "title": "Check Your Project Estimate", + "subtitle": "Select your desired services and requirements to calculate costs in real time", + "button": "Use Cost Calculator" + } + }, + "contact": { + "hero": { + "title": "Contact Us", + "subtitle": "We're here to help bring your ideas to life" + }, + "ready_title": "Ready to Start Your Project?", + "ready_description": "Turn your ideas into reality. Experts provide the best solutions.", + "form": { + "title": "Project Inquiry", + "name": "Name", + "email": "Email", + "phone": "Phone", + "message": "Message", + "submit": "Send Inquiry", + "success": "Inquiry sent successfully", + "error": "Error occurred while sending inquiry", + "service": { + "title": "Service Interest", + "select": "Select service of interest", + "web": "Web Development", + "mobile": "Mobile App", + "design": "UI/UX Design", + "branding": "Branding", + "consulting": "Consulting", + "other": "Other" + } + }, + "info": { + "title": "Contact Information" + }, + "phone": { + "title": "Phone Inquiry", + "number": "+82-2-1234-5678", + "hours": "Mon-Fri 9:00-18:00" + }, + "email": { + "title": "Email Inquiry", + "address": "info@smartsoltech.co.kr", + "response": "Response within 24 hours" + }, + "telegram": { + "title": "Telegram", + "subtitle": "For quick response" + }, + "address": { + "title": "Office Address", + "line1": "123 Teheran-ro, Gangnam-gu", + "line2": "Seoul, South Korea" + }, + "cta": { + "ready": "Ready?", + "start": "Get Started", + "question": "Have questions?", + "subtitle": "We provide consultation on projects" + }, + "meta": { + "title": "Contact", + "description": "Contact us anytime for project inquiries or consultation" + } + }, + "about": { + "hero": { + "title": "About SmartSolTech", + "subtitle": "Creating the future with innovation and technology" + }, + "company": { + "title": "Company Information", + "description1": "SmartSolTech is a technology company established in 2020, recognized for expertise in web development, mobile app development, and UI/UX design.", + "description2": "We accurately understand customer needs and provide innovative solutions using the latest technology." + }, + "stats": { + "projects": "Completed Projects", + "experience": "Years Experience", + "clients": "Satisfied Customers" + }, + "mission": { + "title": "Our Mission", + "description": "Our mission is to support customer business growth through technology and lead digital innovation." + }, + "values": { + "innovation": { + "title": "Innovation", + "description": "We provide innovative solutions through continuous R&D and adoption of cutting-edge technology." + }, + "quality": { + "title": "Quality", + "description": "We maintain high quality standards and provide high-quality products that customers can be satisfied with." + }, + "partnership": { + "title": "Partnership", + "description": "We create the best results through close communication and collaboration with customers." + } + }, + "cta": { + "title": "We'll Grow Together", + "subtitle": "Turn your ideas into reality", + "button": "Contact Us" + }, + "meta": { + "title": "About Us", + "description": "SmartSolTech is a professional development company that supports customer business growth with innovative technology" + } + }, + "footer": { + "description": "Digital solution specialist leading innovation", + "links": { + "title": "Quick Links" + }, + "contact": { + "title": "Contact", + "email": "info@smartsoltech.co.kr", + "phone": "+82-2-1234-5678", + "address": "123 Teheran-ro, Gangnam-gu, Seoul" + }, + "copyright": "© 2024 SmartSolTech. All rights reserved." + }, + "theme": { + "light": "Light Theme", + "dark": "Dark Theme", + "toggle": "Toggle Theme" + }, + "language": { + "english": "English", + "korean": "한국어", + "russian": "Русский", + "kazakh": "Қазақша" + }, + "common": { + "loading": "Loading...", + "error": "Error occurred", + "success": "Success", + "view_more": "View More", + "back": "Back", + "next": "Next", + "previous": "Previous", + "view_details": "View Details" + }, + "meta": { + "description": "SmartSolTech - Innovative web development, mobile app development, UI/UX design services", + "keywords": "web development, mobile apps, UI/UX design, Korea", + "title": "SmartSolTech" + }, + "nav": { + "home": "Home", + "about": "About", + "services": "Services", + "portfolio": "Portfolio", + "calculator": "Calculator" + }, + "admin": { + "login": "Admin Panel Login", + "dashboard": "Dashboard", + "title": "SmartSolTech Admin" + }, + "company": { + "name": "SmartSolTech", + "description": "Digital solution specialist leading innovation", + "email": "info@smartsoltech.kr", + "phone": "+82-10-1234-5678" + }, + "errors": { + "page_not_found": "Page not found", + "error_occurred": "Error occurred", + "title": "Error - SmartSolTech", + "default_title": "An Error Occurred", + "default_message": "A problem occurred while processing the request.", + "back_home": "Back to Home", + "go_back": "Go Back", + "need_help": "Need Help?", + "help_message": "If the problem persists, please contact us anytime.", + "contact_support": "Contact Support" + }, + "pages": { + "home": "Home page", + "about": "About us", + "services": "Services", + "portfolio": "Portfolio", + "contact": "Contact", + "calculator": "Calculator" + }, + "portfolio_page": { + "title": "Our Portfolio", + "subtitle": "Discover innovative projects and creative solutions", + "categories": { + "all": "All", + "web-development": "Web Development", + "mobile-app": "Mobile App", + "ui-ux-design": "UI/UX Design", + "branding": "Branding", + "marketing": "Digital Marketing" + }, + "buttons": { + "details": "View Details", + "projectDetails": "Project Details", + "loadMore": "Load More Projects", + "contact": "Request Project", + "calculate": "Calculate Cost" + }, + "empty": { + "title": "No portfolio yet", + "subtitle": "We'll be showcasing amazing projects soon!" + }, + "cta": { + "title": "Be the star of the next project", + "subtitle": "Create innovative digital solutions with us" + }, + "labels": { + "featured": "FEATURED", + "views": "views", + "likes": "likes" + } + } } \ No newline at end of file diff --git a/middleware/auth.js b/middleware/auth.js index e1aa95c..cee7633 100644 --- a/middleware/auth.js +++ b/middleware/auth.js @@ -1,5 +1,5 @@ const jwt = require('jsonwebtoken'); -const User = require('../models/User'); +const { User } = require('../models'); /** * Authentication middleware @@ -18,7 +18,9 @@ const authenticateToken = async (req, res, next) => { } const decoded = jwt.verify(token, process.env.JWT_SECRET); - const user = await User.findById(decoded.userId).select('-password'); + const user = await User.findByPk(decoded.userId, { + attributes: { exclude: ['password'] } + }); if (!user || !user.isActive) { return res.status(401).json({ @@ -49,7 +51,9 @@ const authenticateSession = async (req, res, next) => { return res.redirect('/auth/login'); } - const user = await User.findById(req.session.userId).select('-password'); + const user = await User.findByPk(req.session.userId, { + attributes: { exclude: ['password'] } + }); if (!user || !user.isActive) { req.session.destroy(); @@ -115,7 +119,9 @@ const optionalAuth = async (req, res, next) => { try { // Check session first if (req.session.userId) { - const user = await User.findById(req.session.userId).select('-password'); + const user = await User.findByPk(req.session.userId, { + attributes: { exclude: ['password'] } + }); if (user && user.isActive) { req.user = user; res.locals.user = user; @@ -129,7 +135,9 @@ const optionalAuth = async (req, res, next) => { if (token) { const decoded = jwt.verify(token, process.env.JWT_SECRET); - const user = await User.findById(decoded.userId).select('-password'); + const user = await User.findByPk(decoded.userId, { + attributes: { exclude: ['password'] } + }); if (user && user.isActive) { req.user = user; diff --git a/models/Banner.js b/models/Banner.js new file mode 100644 index 0000000..ab3dd28 --- /dev/null +++ b/models/Banner.js @@ -0,0 +1,195 @@ +const { DataTypes } = require('sequelize'); +const { sequelize } = require('../config/database'); + +const Banner = sequelize.define('Banner', { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true + }, + title: { + type: DataTypes.STRING, + allowNull: false, + validate: { + notEmpty: true, + len: [1, 200] + } + }, + subtitle: { + type: DataTypes.STRING, + allowNull: true, + validate: { + len: [0, 300] + } + }, + description: { + type: DataTypes.TEXT, + allowNull: true + }, + buttonText: { + type: DataTypes.STRING, + allowNull: true, + validate: { + len: [0, 50] + } + }, + buttonUrl: { + type: DataTypes.STRING, + allowNull: true, + validate: { + isUrl: true + } + }, + image: { + type: DataTypes.STRING, + allowNull: true, + comment: 'Banner background image URL' + }, + mobileImage: { + type: DataTypes.STRING, + allowNull: true, + comment: 'Mobile-optimized banner image URL' + }, + position: { + type: DataTypes.ENUM('hero', 'secondary', 'footer'), + defaultValue: 'hero', + allowNull: false + }, + order: { + type: DataTypes.INTEGER, + defaultValue: 0, + allowNull: false, + comment: 'Display order (lower numbers appear first)' + }, + isActive: { + type: DataTypes.BOOLEAN, + defaultValue: true, + allowNull: false + }, + startDate: { + type: DataTypes.DATE, + allowNull: true, + comment: 'Banner start display date' + }, + endDate: { + type: DataTypes.DATE, + allowNull: true, + comment: 'Banner end display date' + }, + clickCount: { + type: DataTypes.INTEGER, + defaultValue: 0, + allowNull: false + }, + impressions: { + type: DataTypes.INTEGER, + defaultValue: 0, + allowNull: false + }, + targetAudience: { + type: DataTypes.ENUM('all', 'mobile', 'desktop'), + defaultValue: 'all', + allowNull: false + }, + backgroundColor: { + type: DataTypes.STRING, + allowNull: true, + validate: { + is: /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/ + }, + comment: 'Hex color code for banner background' + }, + textColor: { + type: DataTypes.STRING, + allowNull: true, + validate: { + is: /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/ + }, + comment: 'Hex color code for banner text' + }, + animation: { + type: DataTypes.ENUM('none', 'fade', 'slide', 'zoom'), + defaultValue: 'none', + allowNull: false + }, + metadata: { + type: DataTypes.JSONB, + allowNull: true, + defaultValue: {}, + comment: 'Additional banner metadata and settings' + } +}, { + tableName: 'banners', + timestamps: true, + paranoid: true, + indexes: [ + { + fields: ['isActive'] + }, + { + fields: ['position'] + }, + { + fields: ['order'] + }, + { + fields: ['startDate', 'endDate'] + } + ] +}); + +// Virtual field for checking if banner is currently active +Banner.prototype.isCurrentlyActive = function() { + if (!this.isActive) return false; + + const now = new Date(); + + if (this.startDate && now < this.startDate) return false; + if (this.endDate && now > this.endDate) return false; + + return true; +}; + +// Method to increment click count +Banner.prototype.recordClick = async function() { + this.clickCount += 1; + await this.save(); + return this; +}; + +// Method to increment impressions +Banner.prototype.recordImpression = async function() { + this.impressions += 1; + await this.save(); + return this; +}; + +// Static method to get active banners +Banner.getActiveBanners = async function(position = null) { + const whereClause = { + isActive: true + }; + + if (position) { + whereClause.position = position; + } + + const now = new Date(); + + return await this.findAll({ + where: { + ...whereClause, + [require('sequelize').Op.or]: [ + { startDate: null }, + { startDate: { [require('sequelize').Op.lte]: now } } + ], + [require('sequelize').Op.or]: [ + { endDate: null }, + { endDate: { [require('sequelize').Op.gte]: now } } + ] + }, + order: [['order', 'ASC'], ['createdAt', 'DESC']] + }); +}; + +module.exports = Banner; \ No newline at end of file diff --git a/models/Contact.js b/models/Contact.js index c7a79ff..be16e21 100644 --- a/models/Contact.js +++ b/models/Contact.js @@ -1,80 +1,108 @@ -const mongoose = require('mongoose'); +const { DataTypes } = require('sequelize'); +const { sequelize } = require('../config/database'); -const contactSchema = new mongoose.Schema({ +const Contact = sequelize.define('Contact', { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true + }, name: { - type: String, - required: true, - trim: true + type: DataTypes.STRING, + allowNull: false, + set(value) { + this.setDataValue('name', value.trim()); + } }, email: { - type: String, - required: true, - lowercase: true, - trim: true + type: DataTypes.STRING, + allowNull: false, + validate: { + isEmail: true + }, + set(value) { + this.setDataValue('email', value.toLowerCase().trim()); + } }, phone: { - type: String, - trim: true + type: DataTypes.STRING, + allowNull: true, + set(value) { + this.setDataValue('phone', value ? value.trim() : null); + } }, company: { - type: String, - trim: true + type: DataTypes.STRING, + allowNull: true, + set(value) { + this.setDataValue('company', value ? value.trim() : null); + } }, subject: { - type: String, - required: true, - trim: true + type: DataTypes.STRING, + allowNull: false, + set(value) { + this.setDataValue('subject', value.trim()); + } }, message: { - type: String, - required: true + type: DataTypes.TEXT, + allowNull: false }, serviceInterest: { - type: String, - enum: ['web-development', 'mobile-app', 'ui-ux-design', 'branding', 'consulting', 'other'] + type: DataTypes.ENUM('web-development', 'mobile-app', 'ui-ux-design', 'branding', 'consulting', 'other'), + allowNull: true }, budget: { - type: String, - enum: ['under-1m', '1m-5m', '5m-10m', '10m-20m', '20m-50m', 'over-50m'] + type: DataTypes.ENUM('under-1m', '1m-5m', '5m-10m', '10m-20m', '20m-50m', 'over-50m'), + allowNull: true }, timeline: { - type: String, - enum: ['asap', '1-month', '1-3-months', '3-6-months', 'flexible'] + type: DataTypes.ENUM('asap', '1-month', '1-3-months', '3-6-months', 'flexible'), + allowNull: true }, status: { - type: String, - enum: ['new', 'in-progress', 'replied', 'closed'], - default: 'new' + type: DataTypes.ENUM('new', 'in-progress', 'replied', 'closed'), + defaultValue: 'new' }, priority: { - type: String, - enum: ['low', 'medium', 'high', 'urgent'], - default: 'medium' + type: DataTypes.ENUM('low', 'medium', 'high', 'urgent'), + defaultValue: 'medium' }, source: { - type: String, - enum: ['website', 'telegram', 'email', 'phone', 'referral'], - default: 'website' + type: DataTypes.ENUM('website', 'telegram', 'email', 'phone', 'referral'), + defaultValue: 'website' }, isRead: { - type: Boolean, - default: false + type: DataTypes.BOOLEAN, + defaultValue: false }, adminNotes: { - type: String + type: DataTypes.TEXT, + allowNull: true }, ipAddress: { - type: String + type: DataTypes.STRING, + allowNull: true }, userAgent: { - type: String + type: DataTypes.TEXT, + allowNull: true } }, { - timestamps: true + tableName: 'contacts', + timestamps: true, + indexes: [ + { + fields: ['status', 'createdAt'] + }, + { + fields: ['isRead', 'createdAt'] + }, + { + fields: ['email'] + } + ] }); -contactSchema.index({ status: 1, createdAt: -1 }); -contactSchema.index({ isRead: 1, createdAt: -1 }); -contactSchema.index({ email: 1 }); - -module.exports = mongoose.model('Contact', contactSchema); \ No newline at end of file +module.exports = Contact; \ No newline at end of file diff --git a/models/Portfolio.js b/models/Portfolio.js index d86985a..089f685 100644 --- a/models/Portfolio.js +++ b/models/Portfolio.js @@ -1,107 +1,121 @@ -const mongoose = require('mongoose'); +const { DataTypes } = require('sequelize'); +const { sequelize } = require('../config/database'); -const portfolioSchema = new mongoose.Schema({ +const Portfolio = sequelize.define('Portfolio', { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true + }, title: { - type: String, - required: true, - trim: true + type: DataTypes.STRING, + allowNull: false, + validate: { + len: [1, 255] + }, + set(value) { + this.setDataValue('title', value.trim()); + } }, description: { - type: String, - required: true + type: DataTypes.TEXT, + allowNull: false }, shortDescription: { - type: String, - required: true, - maxlength: 200 + type: DataTypes.STRING(200), + allowNull: false }, category: { - type: String, - required: true, - enum: ['web-development', 'mobile-app', 'ui-ux-design', 'branding', 'e-commerce', 'other'] + type: DataTypes.ENUM('web-development', 'mobile-app', 'ui-ux-design', 'branding', 'e-commerce', 'other'), + allowNull: false + }, + technologies: { + type: DataTypes.ARRAY(DataTypes.STRING), + defaultValue: [] + }, + images: { + type: DataTypes.JSONB, + defaultValue: [] }, - technologies: [{ - type: String, - trim: true - }], - images: [{ - url: { - type: String, - required: true - }, - alt: { - type: String, - default: '' - }, - isPrimary: { - type: Boolean, - default: false - } - }], clientName: { - type: String, - trim: true + type: DataTypes.STRING, + allowNull: true, + set(value) { + this.setDataValue('clientName', value ? value.trim() : null); + } }, projectUrl: { - type: String, - trim: true + type: DataTypes.STRING, + allowNull: true, + validate: { + isUrl: true + } }, githubUrl: { - type: String, - trim: true + type: DataTypes.STRING, + allowNull: true, + validate: { + isUrl: true + } }, status: { - type: String, - enum: ['completed', 'in-progress', 'planning'], - default: 'completed' + type: DataTypes.ENUM('completed', 'in-progress', 'planning'), + defaultValue: 'completed' }, featured: { - type: Boolean, - default: false + type: DataTypes.BOOLEAN, + defaultValue: false }, publishedAt: { - type: Date, - default: Date.now + type: DataTypes.DATE, + defaultValue: DataTypes.NOW }, completedAt: { - type: Date + type: DataTypes.DATE, + allowNull: true }, isPublished: { - type: Boolean, - default: true + type: DataTypes.BOOLEAN, + defaultValue: true }, viewCount: { - type: Number, - default: 0 + type: DataTypes.INTEGER, + defaultValue: 0 }, likes: { - type: Number, - default: 0 + type: DataTypes.INTEGER, + defaultValue: 0 }, order: { - type: Number, - default: 0 + type: DataTypes.INTEGER, + defaultValue: 0 }, seo: { - metaTitle: String, - metaDescription: String, - keywords: [String] + type: DataTypes.JSONB, + defaultValue: {} } }, { - timestamps: true + tableName: 'portfolios', + timestamps: true, + indexes: [ + { + fields: ['category', 'publishedAt'] + }, + { + fields: ['featured', 'publishedAt'] + }, + { + type: 'gin', + fields: ['technologies'] + } + ] }); -// Index for search and sorting -portfolioSchema.index({ title: 'text', description: 'text', technologies: 'text' }); -portfolioSchema.index({ category: 1, publishedAt: -1 }); -portfolioSchema.index({ featured: -1, publishedAt: -1 }); - // Virtual for primary image -portfolioSchema.virtual('primaryImage').get(function() { +Portfolio.prototype.getPrimaryImage = function() { + if (!this.images || this.images.length === 0) return null; const primary = this.images.find(img => img.isPrimary); - return primary || (this.images.length > 0 ? this.images[0] : null); -}); + return primary || this.images[0]; +}; -portfolioSchema.set('toJSON', { virtuals: true }); - -module.exports = mongoose.model('Portfolio', portfolioSchema); \ No newline at end of file +module.exports = Portfolio; \ No newline at end of file diff --git a/models/Service.js b/models/Service.js index d8f37e6..61a9098 100644 --- a/models/Service.js +++ b/models/Service.js @@ -1,102 +1,96 @@ -const mongoose = require('mongoose'); +const { DataTypes } = require('sequelize'); +const { sequelize } = require('../config/database'); -const serviceSchema = new mongoose.Schema({ +const Service = sequelize.define('Service', { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true + }, name: { - type: String, - required: true, - trim: true + type: DataTypes.STRING, + allowNull: false, + validate: { + len: [1, 255] + }, + set(value) { + this.setDataValue('name', value.trim()); + } }, description: { - type: String, - required: true + type: DataTypes.TEXT, + allowNull: false }, shortDescription: { - type: String, - required: true, - maxlength: 150 + type: DataTypes.STRING(150), + allowNull: false }, icon: { - type: String, - required: true + type: DataTypes.STRING, + allowNull: false }, category: { - type: String, - required: true, - enum: ['development', 'design', 'consulting', 'marketing', 'maintenance'] + type: DataTypes.ENUM('development', 'design', 'consulting', 'marketing', 'maintenance'), + allowNull: false + }, + features: { + type: DataTypes.JSONB, + defaultValue: [] }, - features: [{ - name: String, - description: String, - included: { - type: Boolean, - default: true - } - }], pricing: { - basePrice: { - type: Number, - required: true, - min: 0 - }, - currency: { - type: String, - default: 'KRW' - }, - priceType: { - type: String, - enum: ['fixed', 'hourly', 'project'], - default: 'project' - }, - priceRange: { - min: Number, - max: Number + type: DataTypes.JSONB, + allowNull: false, + validate: { + isValidPricing(value) { + if (!value.basePrice || value.basePrice < 0) { + throw new Error('Base price must be a positive number'); + } + } } }, estimatedTime: { - min: { - type: Number, - required: true - }, - max: { - type: Number, - required: true - }, - unit: { - type: String, - enum: ['hours', 'days', 'weeks', 'months'], - default: 'days' + type: DataTypes.JSONB, + allowNull: false, + validate: { + isValidTime(value) { + if (!value.min || !value.max || value.min > value.max) { + throw new Error('Invalid estimated time range'); + } + } } }, isActive: { - type: Boolean, - default: true + type: DataTypes.BOOLEAN, + defaultValue: true }, featured: { - type: Boolean, - default: false + type: DataTypes.BOOLEAN, + defaultValue: false }, order: { - type: Number, - default: 0 + type: DataTypes.INTEGER, + defaultValue: 0 + }, + tags: { + type: DataTypes.ARRAY(DataTypes.STRING), + defaultValue: [] }, - portfolio: [{ - type: mongoose.Schema.Types.ObjectId, - ref: 'Portfolio' - }], - tags: [{ - type: String, - trim: true - }], seo: { - metaTitle: String, - metaDescription: String, - keywords: [String] + type: DataTypes.JSONB, + defaultValue: {} } }, { - timestamps: true + tableName: 'services', + timestamps: true, + indexes: [ + { + fields: ['category', 'featured', 'order'] + }, + { + type: 'gin', + fields: ['tags'] + } + ] }); -serviceSchema.index({ name: 'text', description: 'text', tags: 'text' }); -serviceSchema.index({ category: 1, featured: -1, order: 1 }); - -module.exports = mongoose.model('Service', serviceSchema); \ No newline at end of file +module.exports = Service; \ No newline at end of file diff --git a/models/SiteSettings.js b/models/SiteSettings.js index 0156ce7..a8574c2 100644 --- a/models/SiteSettings.js +++ b/models/SiteSettings.js @@ -1,116 +1,82 @@ -const mongoose = require('mongoose'); +const { DataTypes } = require('sequelize'); +const { sequelize } = require('../config/database'); -const siteSettingsSchema = new mongoose.Schema({ +const SiteSettings = sequelize.define('SiteSettings', { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true + }, siteName: { - type: String, - default: 'SmartSolTech' + type: DataTypes.STRING, + defaultValue: 'SmartSolTech' }, siteDescription: { - type: String, - default: 'Innovative technology solutions for modern businesses' + type: DataTypes.TEXT, + defaultValue: 'Innovative technology solutions for modern businesses' }, logo: { - type: String, - default: '/images/logo.png' + type: DataTypes.STRING, + defaultValue: '/images/logo.png' }, favicon: { - type: String, - default: '/images/favicon.ico' + type: DataTypes.STRING, + defaultValue: '/images/favicon.ico' }, contact: { - email: { - type: String, - default: 'info@smartsoltech.kr' - }, - phone: { - type: String, - default: '+82-10-0000-0000' - }, - address: { - type: String, - default: 'Seoul, South Korea' + type: DataTypes.JSONB, + defaultValue: { + email: 'info@smartsoltech.kr', + phone: '+82-10-0000-0000', + address: 'Seoul, South Korea' } }, social: { - facebook: String, - twitter: String, - linkedin: String, - instagram: String, - github: String, - telegram: String + type: DataTypes.JSONB, + defaultValue: {} }, telegram: { - botToken: String, - chatId: String, - isEnabled: { - type: Boolean, - default: false + type: DataTypes.JSONB, + defaultValue: { + isEnabled: false } }, seo: { - metaTitle: { - type: String, - default: 'SmartSolTech - Technology Solutions' - }, - metaDescription: { - type: String, - default: 'Professional web development, mobile apps, and digital solutions in Korea' - }, - keywords: { - type: String, - default: 'web development, mobile apps, UI/UX design, Korea, technology' - }, - googleAnalytics: String, - googleTagManager: String + type: DataTypes.JSONB, + defaultValue: { + metaTitle: 'SmartSolTech - Technology Solutions', + metaDescription: 'Professional web development, mobile apps, and digital solutions in Korea', + keywords: 'web development, mobile apps, UI/UX design, Korea, technology' + } }, hero: { - title: { - type: String, - default: 'Smart Technology Solutions' - }, - subtitle: { - type: String, - default: 'We create innovative digital experiences that drive business growth' - }, - backgroundImage: { - type: String, - default: '/images/hero-bg.jpg' - }, - ctaText: { - type: String, - default: 'Get Started' - }, - ctaLink: { - type: String, - default: '#contact' + type: DataTypes.JSONB, + defaultValue: { + title: 'Smart Technology Solutions', + subtitle: 'We create innovative digital experiences that drive business growth', + backgroundImage: '/images/hero-bg.jpg', + ctaText: 'Get Started', + ctaLink: '#contact' } }, about: { - title: { - type: String, - default: 'About SmartSolTech' - }, - description: { - type: String, - default: 'We are a team of passionate developers and designers creating cutting-edge technology solutions.' - }, - image: { - type: String, - default: '/images/about.jpg' + type: DataTypes.JSONB, + defaultValue: { + title: 'About SmartSolTech', + description: 'We are a team of passionate developers and designers creating cutting-edge technology solutions.', + image: '/images/about.jpg' } }, maintenance: { - isEnabled: { - type: Boolean, - default: false - }, - message: { - type: String, - default: 'We are currently performing maintenance. Please check back soon.' + type: DataTypes.JSONB, + defaultValue: { + isEnabled: false, + message: 'We are currently performing maintenance. Please check back soon.' } } }, { + tableName: 'site_settings', timestamps: true }); -module.exports = mongoose.model('SiteSettings', siteSettingsSchema); \ No newline at end of file +module.exports = SiteSettings; \ No newline at end of file diff --git a/models/User.js b/models/User.js index 27dc646..8643388 100644 --- a/models/User.js +++ b/models/User.js @@ -1,75 +1,78 @@ -const mongoose = require('mongoose'); +const { DataTypes } = require('sequelize'); const bcrypt = require('bcryptjs'); +const { sequelize } = require('../config/database'); -const userSchema = new mongoose.Schema({ +const User = sequelize.define('User', { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true + }, email: { - type: String, - required: true, + type: DataTypes.STRING, + allowNull: false, unique: true, - lowercase: true, - trim: true + validate: { + isEmail: true + }, + set(value) { + this.setDataValue('email', value.toLowerCase().trim()); + } }, password: { - type: String, - required: true, - minlength: 6 + type: DataTypes.STRING, + allowNull: false, + validate: { + len: [6, 255] + } }, name: { - type: String, - required: true, - trim: true + type: DataTypes.STRING, + allowNull: false, + validate: { + len: [1, 100] + }, + set(value) { + this.setDataValue('name', value.trim()); + } }, role: { - type: String, - enum: ['admin', 'moderator'], - default: 'admin' + type: DataTypes.ENUM('admin', 'moderator'), + defaultValue: 'admin' }, avatar: { - type: String, - default: null + type: DataTypes.STRING, + allowNull: true }, isActive: { - type: Boolean, - default: true + type: DataTypes.BOOLEAN, + defaultValue: true }, lastLogin: { - type: Date, - default: null - }, - createdAt: { - type: Date, - default: Date.now - }, - updatedAt: { - type: Date, - default: Date.now + type: DataTypes.DATE, + allowNull: true } }, { - timestamps: true -}); - -// Hash password before saving -userSchema.pre('save', async function(next) { - if (!this.isModified('password')) return next(); - - try { - const salt = await bcrypt.genSalt(12); - this.password = await bcrypt.hash(this.password, salt); - next(); - } catch (error) { - next(error); + tableName: 'users', + timestamps: true, + hooks: { + beforeSave: async (user) => { + if (user.changed('password')) { + const salt = await bcrypt.genSalt(12); + user.password = await bcrypt.hash(user.password, salt); + } + } } }); -// Compare password method -userSchema.methods.comparePassword = async function(candidatePassword) { +// Instance methods +User.prototype.comparePassword = async function(candidatePassword) { return bcrypt.compare(candidatePassword, this.password); }; -// Update last login -userSchema.methods.updateLastLogin = function() { +User.prototype.updateLastLogin = function() { this.lastLogin = new Date(); return this.save(); }; -module.exports = mongoose.model('User', userSchema); \ No newline at end of file +module.exports = User; \ No newline at end of file diff --git a/models/index.js b/models/index.js new file mode 100644 index 0000000..2a040ac --- /dev/null +++ b/models/index.js @@ -0,0 +1,25 @@ +const { sequelize } = require('../config/database'); + +// Import models +const User = require('./User'); +const Portfolio = require('./Portfolio'); +const Service = require('./Service'); +const Contact = require('./Contact'); +const SiteSettings = require('./SiteSettings'); +const Banner = require('./Banner'); + +// Define associations here if needed +// For example: +// Service.belongsToMany(Portfolio, { through: 'ServicePortfolio' }); +// Portfolio.belongsToMany(Service, { through: 'ServicePortfolio' }); + +// Export models and sequelize instance +module.exports = { + sequelize, + User, + Portfolio, + Service, + Contact, + SiteSettings, + Banner +}; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 5b04506..76ee468 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,26 +9,30 @@ "version": "1.0.0", "license": "MIT", "dependencies": { + "axios": "^1.12.2", "bcryptjs": "^2.4.3", "compression": "^1.7.4", "connect-flash": "^0.1.1", - "connect-mongo": "^5.1.0", + "connect-session-sequelize": "^8.0.2", "cookie-parser": "^1.4.7", "cors": "^2.8.5", "dotenv": "^16.3.1", "express": "^4.18.2", + "express-ejs-layouts": "^2.5.1", "express-rate-limit": "^7.1.5", "express-session": "^1.17.3", "express-validator": "^7.0.1", "helmet": "^7.1.0", "i18n": "^0.15.2", "jsonwebtoken": "^9.0.2", - "mongoose": "^8.0.3", "morgan": "^1.10.0", "multer": "^1.4.5-lts.1", "node-telegram-bot-api": "^0.64.0", "nodemailer": "^6.9.7", - "sharp": "^0.33.0", + "pg": "^8.16.3", + "pg-hstore": "^2.3.4", + "sequelize": "^6.37.7", + "sharp": "^0.33.5", "socket.io": "^4.7.4" }, "devDependencies": { @@ -2127,14 +2131,6 @@ "make-plural": "^7.0.0" } }, - "node_modules/@mongodb-js/saslprep": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.3.2.tgz", - "integrity": "sha512-QgA5AySqB27cGTXBFmnpifAi7HxoGUeezwo6p9dI03MuDB6Pp33zgclqVb6oVK3j6I9Vesg0+oojW2XxB59SGg==", - "dependencies": { - "sparse-bitfield": "^3.0.3" - } - }, "node_modules/@rollup/plugin-babel": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz", @@ -2305,6 +2301,14 @@ "@types/node": "*" } }, + "node_modules/@types/debug": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", + "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "dependencies": { + "@types/ms": "*" + } + }, "node_modules/@types/eslint": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", @@ -2343,6 +2347,11 @@ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==" + }, "node_modules/@types/node": { "version": "24.8.1", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.8.1.tgz", @@ -2363,18 +2372,10 @@ "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", "dev": true }, - "node_modules/@types/webidl-conversions": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", - "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==" - }, - "node_modules/@types/whatwg-url": { - "version": "11.0.5", - "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.5.tgz", - "integrity": "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==", - "dependencies": { - "@types/webidl-conversions": "*" - } + "node_modules/@types/validator": { + "version": "13.15.3", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.3.tgz", + "integrity": "sha512-7bcUmDyS6PN3EuD9SlGGOxM77F8WLVsrwkxyWxKnxzmXoequ6c7741QBrANq6htVRGOITJ7z72mTP6Z4XyuG+Q==" }, "node_modules/@webassemblyjs/ast": { "version": "1.14.1", @@ -2776,17 +2777,6 @@ "safer-buffer": "~2.1.0" } }, - "node_modules/asn1.js": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz", - "integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==", - "dependencies": { - "bn.js": "^4.0.0", - "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0", - "safer-buffer": "^2.1.0" - } - }, "node_modules/assert-plus": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", @@ -2850,6 +2840,16 @@ "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.13.2.tgz", "integrity": "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==" }, + "node_modules/axios": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz", + "integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/babel-plugin-polyfill-corejs2": { "version": "0.4.14", "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.14.tgz", @@ -2985,11 +2985,6 @@ "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==" }, - "node_modules/bn.js": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz", - "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==" - }, "node_modules/body-parser": { "version": "1.20.3", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", @@ -3074,14 +3069,6 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, - "node_modules/bson": { - "version": "6.10.4", - "resolved": "https://registry.npmjs.org/bson/-/bson-6.10.4.tgz", - "integrity": "sha512-WIsKqkSC0ABoBJuT1LEX+2HEvNmNKKgnTAyd0fL8qzK4SH2i9NXg+t08YtdZp/V9IZ33cxe3iV4yM0qg8lMQng==", - "engines": { - "node": ">=16.20.1" - } - }, "node_modules/buffer-equal-constant-time": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", @@ -3377,23 +3364,21 @@ "node": ">= 0.4.0" } }, - "node_modules/connect-mongo": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/connect-mongo/-/connect-mongo-5.1.0.tgz", - "integrity": "sha512-xT0vxQLqyqoUTxPLzlP9a/u+vir0zNkhiy9uAdHjSCcUUf7TS5b55Icw8lVyYFxfemP3Mf9gdwUOgeF3cxCAhw==", + "node_modules/connect-session-sequelize": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/connect-session-sequelize/-/connect-session-sequelize-8.0.2.tgz", + "integrity": "sha512-qelSESMV/GWO+w5OIPpDs/a53x/e9BeYWVLqCr5Kvx6tNwNUypR5m3+408oI3pSCCmv7G/iderhvLvcqZgdVmA==", "dependencies": { - "debug": "^4.3.1", - "kruptein": "^3.0.0" + "debug": "^4.4.1" }, "engines": { - "node": ">=12.9.0" + "node": ">= 22" }, "peerDependencies": { - "express-session": "^1.17.1", - "mongodb": ">= 5.1.0 < 7" + "sequelize": ">= 6.37.7" } }, - "node_modules/connect-mongo/node_modules/debug": { + "node_modules/connect-session-sequelize/node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", @@ -3409,7 +3394,7 @@ } } }, - "node_modules/connect-mongo/node_modules/ms": { + "node_modules/connect-session-sequelize/node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" @@ -3440,9 +3425,9 @@ "dev": true }, "node_modules/cookie": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", - "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", "engines": { "node": ">= 0.6" } @@ -3459,14 +3444,6 @@ "node": ">= 0.8.0" } }, - "node_modules/cookie-parser/node_modules/cookie": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/cookie-signature": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", @@ -3826,6 +3803,11 @@ "url": "https://dotenvx.com" } }, + "node_modules/dottie": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/dottie/-/dottie-2.0.6.tgz", + "integrity": "sha512-iGCHkfUc5kFekGiqhe8B/mdaurD+lakO9txNnTvKtA6PISrw86LgqHvRzWYPyoE2Ph5aMIrCw9/uko6XHTKCwA==" + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -3934,14 +3916,6 @@ "node": ">=10.0.0" } }, - "node_modules/engine.io/node_modules/cookie": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/engine.io/node_modules/debug": { "version": "4.3.7", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", @@ -4277,6 +4251,11 @@ "url": "https://opencollective.com/express" } }, + "node_modules/express-ejs-layouts": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/express-ejs-layouts/-/express-ejs-layouts-2.5.1.tgz", + "integrity": "sha512-IXROv9n3xKga7FowT06n1Qn927JR8ZWDn5Dc9CJQoiiaaDqbhW5PDmWShzbpAa2wjWT1vJqaIM1S6vJwwX11gA==" + }, "node_modules/express-rate-limit": { "version": "7.5.1", "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.1.tgz", @@ -4309,14 +4288,6 @@ "node": ">= 0.8.0" } }, - "node_modules/express-session/node_modules/cookie": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/express-session/node_modules/cookie-signature": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", @@ -4334,6 +4305,14 @@ "node": ">= 8.0.0" } }, + "node_modules/express/node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -4499,6 +4478,25 @@ "flat": "cli.js" } }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/for-each": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", @@ -5090,6 +5088,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/inflection": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/inflection/-/inflection-1.13.4.tgz", + "integrity": "sha512-6I/HUDeYFfuNCVS3td055BaXBwKYuzw7K3ExVMStBowKo9oOAMJIXIHvdyR3iboTCp1b+1i5DSkIZTcwIktuDw==", + "engines": [ + "node >= 0.4.0" + ] + }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -5768,14 +5774,6 @@ "safe-buffer": "^5.0.1" } }, - "node_modules/kareem": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.6.3.tgz", - "integrity": "sha512-C3iHfuGUXK2u8/ipq9LfjFfXFxAZMQJJq7vLS45r3D9Y2xQ/m4S8zaR4zMLFWh9AsNPXmcFfUDhTEO8UIC/V6Q==", - "engines": { - "node": ">=12.0.0" - } - }, "node_modules/kind-of": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", @@ -5785,17 +5783,6 @@ "node": ">=0.10.0" } }, - "node_modules/kruptein": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/kruptein/-/kruptein-3.0.8.tgz", - "integrity": "sha512-0CyalFA0Cjp3jnziMp0u1uLZW2/ouhQ0mEMfYlroBXNe86na1RwAuwBcdRAegeWZNMfQy/G5fN47g/Axjtqrfw==", - "dependencies": { - "asn1.js": "^5.4.1" - }, - "engines": { - "node": ">8" - } - }, "node_modules/leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", @@ -5952,11 +5939,6 @@ "node": ">= 0.6" } }, - "node_modules/memory-pager": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", - "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==" - }, "node_modules/merge-descriptors": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", @@ -6090,11 +6072,6 @@ "url": "https://opencollective.com/webpack" } }, - "node_modules/minimalistic-assert": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", - "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" - }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -6126,86 +6103,25 @@ "mkdirp": "bin/cmd.js" } }, - "node_modules/mongodb": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.20.0.tgz", - "integrity": "sha512-Tl6MEIU3K4Rq3TSHd+sZQqRBoGlFsOgNrH5ltAcFBV62Re3Fd+FcaVf8uSEQFOJ51SDowDVttBTONMfoYWrWlQ==", + "node_modules/moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "engines": { + "node": "*" + } + }, + "node_modules/moment-timezone": { + "version": "0.5.48", + "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.48.tgz", + "integrity": "sha512-f22b8LV1gbTO2ms2j2z13MuPogNoh5UzxL3nzNAYKGraILnbGc9NEE6dyiiiLv46DGRb8A4kg8UKWLjPthxBHw==", "dependencies": { - "@mongodb-js/saslprep": "^1.3.0", - "bson": "^6.10.4", - "mongodb-connection-string-url": "^3.0.2" + "moment": "^2.29.4" }, "engines": { - "node": ">=16.20.1" - }, - "peerDependencies": { - "@aws-sdk/credential-providers": "^3.188.0", - "@mongodb-js/zstd": "^1.1.0 || ^2.0.0", - "gcp-metadata": "^5.2.0", - "kerberos": "^2.0.1", - "mongodb-client-encryption": ">=6.0.0 <7", - "snappy": "^7.3.2", - "socks": "^2.7.1" - }, - "peerDependenciesMeta": { - "@aws-sdk/credential-providers": { - "optional": true - }, - "@mongodb-js/zstd": { - "optional": true - }, - "gcp-metadata": { - "optional": true - }, - "kerberos": { - "optional": true - }, - "mongodb-client-encryption": { - "optional": true - }, - "snappy": { - "optional": true - }, - "socks": { - "optional": true - } + "node": "*" } }, - "node_modules/mongodb-connection-string-url": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.2.tgz", - "integrity": "sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==", - "dependencies": { - "@types/whatwg-url": "^11.0.2", - "whatwg-url": "^14.1.0 || ^13.0.0" - } - }, - "node_modules/mongoose": { - "version": "8.19.1", - "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.19.1.tgz", - "integrity": "sha512-oB7hGQJn4f8aebqE7mhE54EReb5cxVgpCxQCQj0K/cK3q4J3Tg08nFP6sM52nJ4Hlm8jsDnhVYpqIITZUAhckQ==", - "dependencies": { - "bson": "^6.10.4", - "kareem": "2.6.3", - "mongodb": "~6.20.0", - "mpath": "0.9.0", - "mquery": "5.0.0", - "ms": "2.1.3", - "sift": "17.1.3" - }, - "engines": { - "node": ">=16.20.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mongoose" - } - }, - "node_modules/mongoose/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - }, "node_modules/moo": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/moo/-/moo-0.5.2.tgz", @@ -6237,46 +6153,6 @@ "node": ">= 0.8" } }, - "node_modules/mpath": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.9.0.tgz", - "integrity": "sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==", - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/mquery": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/mquery/-/mquery-5.0.0.tgz", - "integrity": "sha512-iQMncpmEK8R8ncT8HJGsGc9Dsp8xcgYMVSbs5jgnm1lFHTZqMJTUWTDx1LBO8+mK3tPNZWFLBghQEIOULSTHZg==", - "dependencies": { - "debug": "4.x" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/mquery/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/mquery/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - }, "node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", @@ -6673,6 +6549,98 @@ "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==" }, + "node_modules/pg": { + "version": "8.16.3", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz", + "integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==", + "dependencies": { + "pg-connection-string": "^2.9.1", + "pg-pool": "^3.10.1", + "pg-protocol": "^1.10.3", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.2.7" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.2.7.tgz", + "integrity": "sha512-YgCtzMH0ptvZJslLM1ffsY4EuGaU0cx4XSdXLRFae8bPP4dS5xL1tNB3k2o/N64cHJpwU7dxKli/nZ2lUa5fLg==", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.9.1.tgz", + "integrity": "sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w==" + }, + "node_modules/pg-hstore": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/pg-hstore/-/pg-hstore-2.3.4.tgz", + "integrity": "sha512-N3SGs/Rf+xA1M2/n0JBiXFDVMzdekwLZLAO0g7mpDY9ouX+fDI7jS6kTq3JujmYbtNSJ53TJ0q4G98KVZSM4EA==", + "dependencies": { + "underscore": "^1.13.1" + }, + "engines": { + "node": ">= 0.8.x" + } + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.10.1.tgz", + "integrity": "sha512-Tu8jMlcX+9d8+QVzKIvM/uJtp07PKr82IUOYEphaWcoBhIYkoHpLXN3qO59nAI11ripznDsEzEv8nUxBVWajGg==", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.10.3.tgz", + "integrity": "sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ==" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "dependencies": { + "split2": "^4.1.0" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -6817,6 +6785,41 @@ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", "dev": true }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", + "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/pretty-bytes": { "version": "5.6.0", "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", @@ -6856,6 +6859,11 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "node_modules/psl": { "version": "1.15.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", @@ -7283,6 +7291,11 @@ "node": ">=8" } }, + "node_modules/retry-as-promised": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/retry-as-promised/-/retry-as-promised-7.1.1.tgz", + "integrity": "sha512-hMD7odLOt3LkTjcif8aRZqi/hybjpLNgSk5oF5FCowfCjok6LukpN2bDX7R5wDmbgBQFn7YoBxSagmtXHaJYJw==" + }, "node_modules/rollup": { "version": "2.79.2", "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.2.tgz", @@ -7451,6 +7464,96 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, + "node_modules/sequelize": { + "version": "6.37.7", + "resolved": "https://registry.npmjs.org/sequelize/-/sequelize-6.37.7.tgz", + "integrity": "sha512-mCnh83zuz7kQxxJirtFD7q6Huy6liPanI67BSlbzSYgVNl5eXVdE2CN1FuAeZwG1SNpGsNRCV+bJAVVnykZAFA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/sequelize" + } + ], + "dependencies": { + "@types/debug": "^4.1.8", + "@types/validator": "^13.7.17", + "debug": "^4.3.4", + "dottie": "^2.0.6", + "inflection": "^1.13.4", + "lodash": "^4.17.21", + "moment": "^2.29.4", + "moment-timezone": "^0.5.43", + "pg-connection-string": "^2.6.1", + "retry-as-promised": "^7.0.4", + "semver": "^7.5.4", + "sequelize-pool": "^7.1.0", + "toposort-class": "^1.0.1", + "uuid": "^8.3.2", + "validator": "^13.9.0", + "wkx": "^0.5.0" + }, + "engines": { + "node": ">=10.0.0" + }, + "peerDependenciesMeta": { + "ibm_db": { + "optional": true + }, + "mariadb": { + "optional": true + }, + "mysql2": { + "optional": true + }, + "oracledb": { + "optional": true + }, + "pg": { + "optional": true + }, + "pg-hstore": { + "optional": true + }, + "snowflake-sdk": { + "optional": true + }, + "sqlite3": { + "optional": true + }, + "tedious": { + "optional": true + } + } + }, + "node_modules/sequelize-pool": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/sequelize-pool/-/sequelize-pool-7.1.0.tgz", + "integrity": "sha512-G9c0qlIWQSK29pR/5U2JF5dDQeqqHRragoyahj/Nx4KOOQ3CPPfzxnfqFPCSB7x5UgjOgnZ61nSxz+fjDpRlJg==", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/sequelize/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/sequelize/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, "node_modules/serialize-javascript": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", @@ -7661,11 +7764,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/sift": { - "version": "17.1.3", - "resolved": "https://registry.npmjs.org/sift/-/sift-17.1.3.tgz", - "integrity": "sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ==" - }, "node_modules/simple-swizzle": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz", @@ -7834,12 +7932,12 @@ "deprecated": "Please use @jridgewell/sourcemap-codec instead", "dev": true }, - "node_modules/sparse-bitfield": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", - "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", - "dependencies": { - "memory-pager": "^1.0.2" + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "engines": { + "node": ">= 10.x" } }, "node_modules/sshpk": { @@ -8257,6 +8355,11 @@ "node": ">=0.6" } }, + "node_modules/toposort-class": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toposort-class/-/toposort-class-1.0.1.tgz", + "integrity": "sha512-OsLcGGbYF3rMjPUf8oKktyvCiUxSbqMMS39m33MAjLTC1DVIH6x3WSt63/M77ihI09+Sdfk1AXvfhCEeUmC7mg==" + }, "node_modules/touch": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", @@ -8278,14 +8381,12 @@ } }, "node_modules/tr46": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", - "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", + "integrity": "sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==", + "dev": true, "dependencies": { - "punycode": "^2.3.1" - }, - "engines": { - "node": ">=18" + "punycode": "^2.1.0" } }, "node_modules/tslib": { @@ -8443,6 +8544,11 @@ "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", "dev": true }, + "node_modules/underscore": { + "version": "1.13.7", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.7.tgz", + "integrity": "sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g==" + }, "node_modules/undici-types": { "version": "7.14.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.14.0.tgz", @@ -8649,12 +8755,10 @@ } }, "node_modules/webidl-conversions": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", - "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", - "engines": { - "node": ">=12" - } + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", + "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", + "dev": true }, "node_modules/webpack": { "version": "5.102.1", @@ -8835,15 +8939,14 @@ } }, "node_modules/whatwg-url": { - "version": "14.2.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", - "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz", + "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==", + "dev": true, "dependencies": { - "tr46": "^5.1.0", - "webidl-conversions": "^7.0.0" - }, - "engines": { - "node": ">=18" + "lodash.sortby": "^4.7.0", + "tr46": "^1.0.1", + "webidl-conversions": "^4.0.2" } }, "node_modules/which": { @@ -8953,6 +9056,14 @@ "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", "dev": true }, + "node_modules/wkx": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/wkx/-/wkx-0.5.0.tgz", + "integrity": "sha512-Xng/d4Ichh8uN4l0FToV/258EjMGU9MGcA0HV2d9B/ZpZB3lqQm7nkOdZdm5GhKtLLhAE7PiVQwN4eN+2YJJUg==", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/workbox-background-sync": { "version": "7.3.0", "resolved": "https://registry.npmjs.org/workbox-background-sync/-/workbox-background-sync-7.3.0.tgz", @@ -9072,32 +9183,6 @@ "node": ">= 8" } }, - "node_modules/workbox-build/node_modules/tr46": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", - "integrity": "sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==", - "dev": true, - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/workbox-build/node_modules/webidl-conversions": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", - "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", - "dev": true - }, - "node_modules/workbox-build/node_modules/whatwg-url": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz", - "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==", - "dev": true, - "dependencies": { - "lodash.sortby": "^4.7.0", - "tr46": "^1.0.1", - "webidl-conversions": "^4.0.2" - } - }, "node_modules/workbox-cacheable-response": { "version": "7.3.0", "resolved": "https://registry.npmjs.org/workbox-cacheable-response/-/workbox-cacheable-response-7.3.0.tgz", diff --git a/package.json b/package.json index de4abf4..f8ced90 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,8 @@ "dev": "node scripts/dev.js", "build": "node scripts/build.js", "init-db": "node scripts/init-db.js", - "test": "echo \"Error: no test specified\" && exit 1" + "test": "echo \"Error: no test specified\" && exit 1", + "sync-locales": "node scripts/sync-locales.js" }, "keywords": [ "pwa", @@ -22,26 +23,30 @@ "author": "SmartSolTech", "license": "MIT", "dependencies": { + "axios": "^1.12.2", "bcryptjs": "^2.4.3", "compression": "^1.7.4", "connect-flash": "^0.1.1", - "connect-mongo": "^5.1.0", + "connect-session-sequelize": "^8.0.2", "cookie-parser": "^1.4.7", "cors": "^2.8.5", "dotenv": "^16.3.1", "express": "^4.18.2", + "express-ejs-layouts": "^2.5.1", "express-rate-limit": "^7.1.5", "express-session": "^1.17.3", "express-validator": "^7.0.1", "helmet": "^7.1.0", "i18n": "^0.15.2", "jsonwebtoken": "^9.0.2", - "mongoose": "^8.0.3", "morgan": "^1.10.0", "multer": "^1.4.5-lts.1", "node-telegram-bot-api": "^0.64.0", "nodemailer": "^6.9.7", - "sharp": "^0.33.0", + "pg": "^8.16.3", + "pg-hstore": "^2.3.4", + "sequelize": "^6.37.7", + "sharp": "^0.33.5", "socket.io": "^4.7.4" }, "devDependencies": { diff --git a/public/css/base.css b/public/css/base.css new file mode 100644 index 0000000..0df34a4 --- /dev/null +++ b/public/css/base.css @@ -0,0 +1,686 @@ +/* SmartSolTech - Base Styles for Elements */ + +/* Ensure proper styling without Tailwind conflicts */ +.navbar { + position: fixed !important; + top: 0 !important; + left: 0 !important; + right: 0 !important; + z-index: 1000 !important; + background: rgba(255, 255, 255, 0.95) !important; + backdrop-filter: blur(10px) !important; + border-bottom: 1px solid #e5e7eb !important; + padding: 1rem 0 !important; +} + +.navbar-brand { + font-size: 1.5rem !important; + font-weight: 700 !important; + color: #3B82F6 !important; + text-decoration: none !important; +} + +.navbar-nav { + display: flex !important; + align-items: center !important; + gap: 2rem !important; + list-style: none !important; + margin: 0 !important; + padding: 0 !important; +} + +.nav-link { + color: #6b7280 !important; + text-decoration: none !important; + font-weight: 500 !important; + padding: 0.5rem 1rem !important; + border-radius: 0.5rem !important; + transition: all 0.3s ease !important; +} + +.nav-link:hover, +.nav-link.active { + color: #3B82F6 !important; + background-color: #eff6ff !important; +} + +/* Base CSS - Исправленные стили для главной страницы */ + +/* Reset и базовые стили */ +* { + box-sizing: border-box !important; + margin: 0 !important; + padding: 0 !important; +} + +html { + font-size: 16px !important; + scroll-behavior: smooth !important; +} + +body { + font-family: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif !important; + line-height: 1.6 !important; + color: #1f2937 !important; + background-color: #ffffff !important; + margin: 0 !important; + padding: 0 !important; + padding-top: 80px !important; /* Отступ для навигации */ +} + +/* Навигация - исправленная */ +nav, .navigation { + position: fixed !important; + top: 0 !important; + left: 0 !important; + right: 0 !important; + z-index: 1000 !important; + background: rgba(255, 255, 255, 0.95) !important; + -webkit-backdrop-filter: blur(20px) !important; + backdrop-filter: blur(20px) !important; + border-bottom: 1px solid rgba(0, 0, 0, 0.1) !important; + height: 80px !important; + box-shadow: 0 2px 20px rgba(0, 0, 0, 0.1) !important; +} + +.navbar, .nav-container { + display: flex !important; + justify-content: space-between !important; + align-items: center !important; + padding: 0 2rem !important; + max-width: 1200px !important; + margin: 0 auto !important; + height: 100% !important; +} + +.logo, .brand { + font-size: 1.75rem !important; + font-weight: 800 !important; + color: #1d4ed8 !important; + text-decoration: none !important; + letter-spacing: -0.025em !important; +} + +.nav-links, .menu { + display: flex !important; + list-style: none !important; + margin: 0 !important; + padding: 0 !important; + gap: 2.5rem !important; +} + +.nav-links li, .menu li { + margin: 0 !important; + padding: 0 !important; +} + +.nav-links a, .menu a { + color: #374151 !important; + text-decoration: none !important; + font-weight: 500 !important; + font-size: 0.95rem !important; + transition: all 0.3s ease !important; + padding: 0.75rem 0 !important; + position: relative !important; +} + +.nav-links a:hover, .menu a:hover { + color: #1d4ed8 !important; + transform: translateY(-1px) !important; +} + +.nav-links a::after, .menu a::after { + content: '' !important; + position: absolute !important; + bottom: 0.5rem !important; + left: 0 !important; + width: 0 !important; + height: 2px !important; + background: linear-gradient(45deg, #1d4ed8, #8b5cf6) !important; + transition: width 0.3s ease !important; +} + +.nav-links a:hover::after, .menu a:hover::after { + width: 100% !important; +} + +/* Hero секция - исправленная */ +.hero-section { + min-height: 100vh !important; + background: linear-gradient(135deg, #0f172a 0%, #1e1b4b 25%, #312e81 50%, #7c3aed 75%, #3730a3 100%) !important; + display: flex !important; + align-items: center !important; + justify-content: center !important; + text-align: center !important; + color: white !important; + position: relative !important; + overflow: hidden !important; + margin-top: -80px !important; + padding-top: 80px !important; +} + +.hero-section::before { + content: '' !important; + position: absolute !important; + top: 0 !important; + left: 0 !important; + right: 0 !important; + bottom: 0 !important; + background: radial-gradient(circle at 30% 40%, rgba(59, 130, 246, 0.3) 0%, transparent 50%), + radial-gradient(circle at 80% 20%, rgba(139, 92, 246, 0.3) 0%, transparent 50%), + radial-gradient(circle at 40% 80%, rgba(16, 185, 129, 0.2) 0%, transparent 50%) !important; + z-index: 1 !important; +} + +.hero-section > div { + position: relative !important; + z-index: 2 !important; +} + +.hero-section h1 { + font-size: 4rem !important; + font-weight: 800 !important; + margin-bottom: 1.5rem !important; + line-height: 1.1 !important; + letter-spacing: -0.025em !important; +} + +.hero-section p { + font-size: 1.35rem !important; + margin-bottom: 2.5rem !important; + opacity: 0.9 !important; + max-width: 600px !important; + margin-left: auto !important; + margin-right: auto !important; +} + +/* Кнопки - улучшенные */ +.btn-primary, .btn { + display: inline-flex !important; + align-items: center !important; + justify-content: center !important; + padding: 1.25rem 2.5rem !important; + background: linear-gradient(135deg, #3b82f6 0%, #8b5cf6 100%) !important; + color: white !important; + text-decoration: none !important; + border-radius: 50px !important; + font-weight: 700 !important; + font-size: 1.1rem !important; + transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1) !important; + border: none !important; + cursor: pointer !important; + box-shadow: 0 8px 25px rgba(59, 130, 246, 0.3) !important; + position: relative !important; + overflow: hidden !important; +} + +.btn-primary:hover, .btn:hover { + transform: translateY(-3px) scale(1.05) !important; + box-shadow: 0 15px 35px rgba(59, 130, 246, 0.4) !important; + background: linear-gradient(135deg, #2563eb 0%, #7c3aed 100%) !important; +} + +.btn-primary::before, .btn::before { + content: '' !important; + position: absolute !important; + top: 0 !important; + left: -100% !important; + width: 100% !important; + height: 100% !important; + background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent) !important; + transition: left 0.6s !important; +} + +.btn-primary:hover::before, .btn:hover::before { + left: 100% !important; +} + +/* Карточки - улучшенные */ +.card-hover, .card { + transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1) !important; + border-radius: 1.5rem !important; + overflow: hidden !important; + background: white !important; + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.05) !important; + border: 1px solid rgba(0, 0, 0, 0.05) !important; +} + +.card-hover:hover, .card:hover { + transform: translateY(-12px) scale(1.02) !important; + box-shadow: 0 25px 50px rgba(0, 0, 0, 0.15) !important; + border-color: rgba(59, 130, 246, 0.2) !important; +} + +/* Секции */ +section { + padding: 6rem 1rem !important; + position: relative !important; +} + +.container, .max-w-7xl { + max-width: 1200px !important; + margin: 0 auto !important; + padding-left: 1rem !important; + padding-right: 1rem !important; +} + +/* Заголовки */ +h1, h2, h3, h4, h5, h6 { + color: #1f2937 !important; + font-weight: 700 !important; + line-height: 1.2 !important; +} + +h2 { + font-size: 2.75rem !important; + margin-bottom: 1rem !important; +} + +h3 { + font-size: 1.5rem !important; + margin-bottom: 0.75rem !important; +} + +/* Параграфы и текст */ +p { + color: #6b7280 !important; + line-height: 1.7 !important; + margin-bottom: 1rem !important; +} + +/* Утилитарные классы Tailwind - принудительные */ +.text-center { + text-align: center !important; +} + +.text-white { + color: white !important; +} + +.bg-white { + background-color: white !important; +} + +.bg-gray-50 { + background-color: #f9fafb !important; +} + +.rounded-lg { + border-radius: 0.5rem !important; +} + +.rounded-2xl { + border-radius: 1rem !important; +} + +.shadow-lg { + box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1) !important; +} + +.mb-4 { + margin-bottom: 1rem !important; +} + +.mb-8 { + margin-bottom: 2rem !important; +} + +.p-8 { + padding: 2rem !important; +} + +.flex { + display: flex !important; +} + +.grid { + display: grid !important; +} + +.items-center { + align-items: center !important; +} + +.justify-center { + justify-content: center !important; +} + +/* Грид системы */ +.grid-cols-1 { + grid-template-columns: repeat(1, minmax(0, 1fr)) !important; +} + +.grid-cols-2 { + grid-template-columns: repeat(2, minmax(0, 1fr)) !important; +} + +.grid-cols-3 { + grid-template-columns: repeat(3, minmax(0, 1fr)) !important; +} + +.grid-cols-4 { + grid-template-columns: repeat(4, minmax(0, 1fr)) !important; +} + +.gap-8 { + gap: 2rem !important; +} + +/* Отзывчивость - улучшенная */ +@media (max-width: 1024px) { + .grid-cols-4 { + grid-template-columns: repeat(2, minmax(0, 1fr)) !important; + } + + .hero-section h1 { + font-size: 3.5rem !important; + } +} + +@media (max-width: 768px) { + .nav-links, .menu { + display: none !important; + } + + body { + padding-top: 60px !important; + } + + nav, .navigation { + height: 60px !important; + } + + .hero-section { + margin-top: -60px !important; + padding-top: 60px !important; + } + + section { + padding: 4rem 1rem !important; + } + + .hero-section h1 { + font-size: 2.75rem !important; + } + + .hero-section p { + font-size: 1.1rem !important; + } + + .grid-cols-2, .grid-cols-3, .grid-cols-4 { + grid-template-columns: repeat(1, minmax(0, 1fr)) !important; + } + + .btn-primary, .btn { + padding: 1rem 2rem !important; + font-size: 1rem !important; + } +} + +.hero-title { + font-size: 3.5rem !important; + font-weight: 700 !important; + margin-bottom: 1rem !important; + line-height: 1.2 !important; +} + +.hero-subtitle { + font-size: 1.25rem !important; + margin-bottom: 2rem !important; + color: rgba(255, 255, 255, 0.9) !important; +} + +/* Buttons */ +.btn { + display: inline-block !important; + padding: 0.75rem 1.5rem !important; + border-radius: 0.5rem !important; + text-decoration: none !important; + font-weight: 600 !important; + text-align: center !important; + border: none !important; + cursor: pointer !important; + transition: all 0.3s ease !important; + font-size: 1rem !important; +} + +.btn-primary { + background: linear-gradient(135deg, #3B82F6, #1D4ED8) !important; + color: white !important; +} + +.btn-primary:hover { + transform: translateY(-2px) !important; + box-shadow: 0 10px 25px rgba(59, 130, 246, 0.3) !important; +} + +.btn-secondary { + background: transparent !important; + color: white !important; + border: 2px solid white !important; +} + +.btn-secondary:hover { + background: white !important; + color: #3B82F6 !important; +} + +/* Cards */ +.card { + background: white !important; + border-radius: 1rem !important; + padding: 2rem !important; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05) !important; + transition: all 0.3s ease !important; +} + +.card:hover { + transform: translateY(-4px) !important; + box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1) !important; +} + +/* Sections */ +.section { + padding: 4rem 2rem !important; +} + +.section-title { + font-size: 2.5rem !important; + font-weight: 700 !important; + text-align: center !important; + margin-bottom: 1rem !important; + color: #1f2937 !important; +} + +.section-description { + font-size: 1.125rem !important; + text-align: center !important; + color: #6b7280 !important; + max-width: 600px !important; + margin: 0 auto 3rem !important; +} + +/* Container */ +.container { + max-width: 1200px !important; + margin: 0 auto !important; + padding: 0 1rem !important; +} + +/* Grid Layouts */ +.grid-2 { + display: grid !important; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)) !important; + gap: 2rem !important; +} + +.grid-3 { + display: grid !important; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)) !important; + gap: 2rem !important; +} + +.grid-4 { + display: grid !important; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)) !important; + gap: 1.5rem !important; +} + +/* Language Selector */ +.language-selector { + display: flex !important; + align-items: center !important; + gap: 0.5rem !important; +} + +.language-selector a { + color: #6b7280 !important; + text-decoration: none !important; + padding: 0.25rem 0.5rem !important; + border-radius: 0.25rem !important; + font-size: 0.875rem !important; +} + +.language-selector a:hover { + color: #3B82F6 !important; + background-color: #f3f4f6 !important; +} + +/* Mobile Menu */ +.mobile-menu-button { + display: none !important; + background: none !important; + border: none !important; + font-size: 1.5rem !important; + color: #6b7280 !important; + cursor: pointer !important; +} + +/* Mobile Styles */ +@media (max-width: 768px) { + .hero-title { + font-size: 2.5rem !important; + } + + .mobile-menu-button { + display: block !important; + } + + .navbar-nav { + display: none !important; + position: absolute !important; + top: 100% !important; + left: 0 !important; + right: 0 !important; + background: white !important; + flex-direction: column !important; + padding: 1rem !important; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1) !important; + } + + .navbar-nav.show { + display: flex !important; + } + + .section { + padding: 2rem 1rem !important; + } + + .section-title { + font-size: 2rem !important; + } +} + +/* Footer */ +.footer { + background: #1f2937 !important; + color: white !important; + padding: 3rem 2rem 1rem !important; + text-align: center !important; +} + +.footer p { + color: #d1d5db !important; +} + +/* Service Icons */ +.service-icon { + width: 80px !important; + height: 80px !important; + background: linear-gradient(135deg, #3B82F6, #8B5CF6) !important; + border-radius: 50% !important; + display: flex !important; + align-items: center !important; + justify-content: center !important; + font-size: 2rem !important; + color: white !important; + margin: 0 auto 1rem !important; +} + +/* Portfolio Items */ +.portfolio-item { + background: white !important; + border-radius: 1rem !important; + overflow: hidden !important; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05) !important; + transition: all 0.3s ease !important; +} + +.portfolio-item:hover { + transform: translateY(-4px) !important; + box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1) !important; +} + +.portfolio-image { + width: 100% !important; + height: 200px !important; + object-fit: cover !important; + display: block !important; +} + +.portfolio-content { + padding: 1.5rem !important; +} + +.portfolio-title { + font-size: 1.25rem !important; + font-weight: 600 !important; + margin-bottom: 0.5rem !important; + color: #1f2937 !important; +} + +.portfolio-description { + color: #6b7280 !important; + margin-bottom: 1rem !important; +} + +/* Form Styles */ +.form-group { + margin-bottom: 1.5rem !important; +} + +.form-label { + display: block !important; + font-weight: 500 !important; + margin-bottom: 0.5rem !important; + color: #374151 !important; +} + +.form-input, +.form-textarea, +.form-select { + width: 100% !important; + padding: 0.75rem !important; + border: 2px solid #e5e7eb !important; + border-radius: 0.5rem !important; + font-size: 1rem !important; + transition: all 0.3s ease !important; +} + +.form-input:focus, +.form-textarea:focus, +.form-select:focus { + outline: none !important; + border-color: #3B82F6 !important; + box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1) !important; +} \ No newline at end of file diff --git a/public/css/dark-theme.css b/public/css/dark-theme.css index 2d0cca4..19d8f4f 100644 --- a/public/css/dark-theme.css +++ b/public/css/dark-theme.css @@ -297,8 +297,8 @@ html.dark { /* High contrast mode */ @media (prefers-contrast: high) { .dark { - --tw-bg-opacity: 1; - --tw-text-opacity: 1; + color: white !important; + background-color: black !important; } .dark .border { diff --git a/public/css/fixes.css b/public/css/fixes.css index 432d387..d161529 100644 --- a/public/css/fixes.css +++ b/public/css/fixes.css @@ -272,6 +272,35 @@ html { } } +/* iOS Style Theme Toggle */ +.theme-toggle-slider { + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); +} + +/* Theme toggle background states */ +input#theme-toggle:checked + label > div { + background-color: #4F46E5 !important; +} + +input#theme-toggle + label > div { + background-color: #D1D5DB; +} + +/* Dark mode adjustments for toggle */ +.dark input#theme-toggle + label > div { + background-color: #374151; +} + +.dark input#theme-toggle:checked + label > div { + background-color: #6366F1 !important; +} + +/* Smooth transitions for icons */ +.theme-sun-icon, +.theme-moon-icon { + transition: opacity 0.3s ease; +} + /* Dark mode support (if needed) */ @media (prefers-color-scheme: dark) { .auto-dark { diff --git a/public/css/main.css b/public/css/main.css index e13ca7f..8858763 100644 --- a/public/css/main.css +++ b/public/css/main.css @@ -1,5 +1,8 @@ /* SmartSolTech - Main Styles */ +/* Tailwind Base (if not loading properly) */ +@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap'); + /* CSS Reset and Base */ * { margin: 0; @@ -7,10 +10,26 @@ box-sizing: border-box; } +html { + scroll-behavior: smooth; +} + body { - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; + font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; line-height: 1.6; color: #1f2937; + background-color: #ffffff; +} + +/* Root Variables */ +:root { + --primary-color: #3B82F6; + --secondary-color: #8B5CF6; + --accent-color: #10B981; + --text-dark: #1f2937; + --text-light: #6b7280; + --bg-light: #f9fafb; + --border-color: #e5e7eb; } /* Utility Classes */ @@ -65,6 +84,8 @@ body { color: #3b82f6; background-color: #eff6ff; } + +.mobile-menu { overflow: hidden; } @@ -547,6 +568,39 @@ body { } #final-price { - font-size: 2.5rem; + font-size: 2rem; } +} + +/* Hero секции - компактные для внутренних страниц */ +.hero-section-compact { + min-height: 40vh !important; + max-height: 50vh !important; + padding: 4rem 0 !important; +} + +.hero-section-compact h1 { + font-size: 3rem !important; + margin-bottom: 1rem !important; +} + +.hero-section-compact p { + font-size: 1.125rem !important; + opacity: 0.9 !important; +} + +@media (max-width: 768px) { + .hero-section-compact { + min-height: 30vh !important; + padding: 3rem 0 !important; + } + + .hero-section-compact h1 { + font-size: 2.5rem !important; + } +} + +/* Полноэкранный Hero только для главной */ +.hero-section { + min-height: 100vh !important; } \ No newline at end of file diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000..94200ce Binary files /dev/null and b/public/favicon.ico differ diff --git a/public/images/icon-144x144.png b/public/images/icon-144x144.png new file mode 100644 index 0000000..c619d18 Binary files /dev/null and b/public/images/icon-144x144.png differ diff --git a/public/images/icon-192x192.png b/public/images/icon-192x192.png new file mode 100644 index 0000000..cd7e5f2 Binary files /dev/null and b/public/images/icon-192x192.png differ diff --git a/public/images/icons/icon-192x192.png b/public/images/icons/icon-192x192.png new file mode 100644 index 0000000..6d2ce2b --- /dev/null +++ b/public/images/icons/icon-192x192.png @@ -0,0 +1,4 @@ + + + ST + \ No newline at end of file diff --git a/public/images/logo.png b/public/images/logo.png new file mode 100644 index 0000000..4054628 --- /dev/null +++ b/public/images/logo.png @@ -0,0 +1,4 @@ + + + ST + \ No newline at end of file diff --git a/public/images/portfolio/corporate-1.jpg b/public/images/portfolio/corporate-1.jpg new file mode 100644 index 0000000..5649601 --- /dev/null +++ b/public/images/portfolio/corporate-1.jpg @@ -0,0 +1,4 @@ + + + Corporate Website + \ No newline at end of file diff --git a/public/images/portfolio/ecommerce-1.jpg b/public/images/portfolio/ecommerce-1.jpg new file mode 100644 index 0000000..559feb7 --- /dev/null +++ b/public/images/portfolio/ecommerce-1.jpg @@ -0,0 +1,4 @@ + + + E-commerce Project + \ No newline at end of file diff --git a/public/images/portfolio/fitness-1.jpg b/public/images/portfolio/fitness-1.jpg new file mode 100644 index 0000000..70126d4 --- /dev/null +++ b/public/images/portfolio/fitness-1.jpg @@ -0,0 +1,4 @@ + + + Fitness App Project + \ No newline at end of file diff --git a/public/sw.js b/public/sw.js index 4cc2eef..94421c1 100644 --- a/public/sw.js +++ b/public/sw.js @@ -1,21 +1,24 @@ // Service Worker for SmartSolTech PWA -const CACHE_NAME = 'smartsoltech-v1.0.0'; -const STATIC_CACHE_NAME = 'smartsoltech-static-v1.0.0'; -const DYNAMIC_CACHE_NAME = 'smartsoltech-dynamic-v1.0.0'; +const CACHE_NAME = 'smartsoltech-v1.0.1'; +const STATIC_CACHE_NAME = 'smartsoltech-static-v1.0.1'; +const DYNAMIC_CACHE_NAME = 'smartsoltech-dynamic-v1.0.1'; // Files to cache immediately const STATIC_FILES = [ '/', '/css/main.css', + '/css/fixes.css', + '/css/dark-theme.css', '/js/main.js', '/images/logo.png', '/images/icon-192x192.png', - '/images/icon-512x512.png', + '/images/icon-144x144.png', '/manifest.json', 'https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap', - 'https://cdnjs.cloudflare.com/ajax/libs/tailwindcss/2.2.19/tailwind.min.css', - 'https://cdnjs.cloudflare.com/ajax/libs/aos/2.3.4/aos.css', - 'https://cdnjs.cloudflare.com/ajax/libs/aos/2.3.4/aos.js' + 'https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css', + 'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css', + 'https://unpkg.com/aos@2.3.1/dist/aos.css', + 'https://unpkg.com/aos@2.3.1/dist/aos.js' ]; // Routes to cache dynamically @@ -155,17 +158,25 @@ async function networkFirst(request) { } async function staleWhileRevalidate(request) { - const cache = await caches.open(DYNAMIC_CACHE_NAME); - const cachedResponse = await cache.match(request); - - const fetchPromise = fetch(request).then(networkResponse => { - if (networkResponse.ok) { - cache.put(request, networkResponse.clone()); - } - return networkResponse; - }); - - return cachedResponse || fetchPromise; + try { + const cache = await caches.open(DYNAMIC_CACHE_NAME); + const cachedResponse = await cache.match(request); + + const fetchPromise = fetch(request).then(networkResponse => { + if (networkResponse && networkResponse.ok) { + cache.put(request, networkResponse.clone()); + } + return networkResponse; + }).catch(error => { + console.log('staleWhileRevalidate fetch failed:', error); + return null; + }); + + return cachedResponse || fetchPromise || new Response('Not available', { status: 503 }); + } catch (error) { + console.error('staleWhileRevalidate error:', error); + return new Response('Service unavailable', { status: 503 }); + } } // Helper functions diff --git a/routes/admin.js b/routes/admin.js index b4c8e82..52890fe 100644 --- a/routes/admin.js +++ b/routes/admin.js @@ -1,11 +1,7 @@ const express = require('express'); const router = express.Router(); const { body, validationResult } = require('express-validator'); -const User = require('../models/User'); -const Portfolio = require('../models/Portfolio'); -const Service = require('../models/Service'); -const Contact = require('../models/Contact'); -const SiteSettings = require('../models/SiteSettings'); +const { User, Portfolio, Service, Contact, SiteSettings, Banner } = require('../models'); // Authentication middleware const requireAuth = (req, res, next) => { @@ -22,8 +18,7 @@ router.get('/login', (req, res) => { } res.render('admin/login', { - title: 'Admin Login - SmartSolTech', - layout: 'admin/layout', + title: 'Admin Login', error: null }); }); @@ -33,19 +28,23 @@ router.post('/login', async (req, res) => { try { const { email, password } = req.body; - const user = await User.findOne({ email, isActive: true }); + const user = await User.findOne({ + where: { + email: email, + isActive: true + } + }); if (!user || !(await user.comparePassword(password))) { return res.render('admin/login', { - title: 'Admin Login - SmartSolTech', - layout: 'admin/layout', - error: 'Invalid email or password' + title: 'Admin Login', + error: 'Invalid credentials' }); } await user.updateLastLogin(); req.session.user = { - id: user._id, + id: user.id, email: user.email, name: user.name, role: user.role @@ -55,9 +54,8 @@ router.post('/login', async (req, res) => { } catch (error) { console.error('Admin login error:', error); res.render('admin/login', { - title: 'Admin Login - SmartSolTech', - layout: 'admin/layout', - error: 'An error occurred. Please try again.' + title: 'Admin Login', + error: 'Server error' }); } }); @@ -72,6 +70,11 @@ router.post('/logout', (req, res) => { }); }); +// Dashboard (default route) +router.get('/', requireAuth, async (req, res) => { + res.redirect('/admin/dashboard'); +}); + // Dashboard router.get('/dashboard', requireAuth, async (req, res) => { try { @@ -82,22 +85,29 @@ router.get('/dashboard', requireAuth, async (req, res) => { recentContacts, recentPortfolio ] = await Promise.all([ - Portfolio.countDocuments({ isPublished: true }), - Service.countDocuments({ isActive: true }), - Contact.countDocuments(), - Contact.find().sort({ createdAt: -1 }).limit(5), - Portfolio.find({ isPublished: true }).sort({ createdAt: -1 }).limit(5) + Portfolio.count({ where: { isPublished: true } }), + Service.count({ where: { isActive: true } }), + Contact.count(), + Contact.findAll({ + order: [['createdAt', 'DESC']], + limit: 5 + }), + Portfolio.findAll({ + where: { isPublished: true }, + order: [['createdAt', 'DESC']], + limit: 5 + }) ]); const stats = { - portfolio: portfolioCount, - services: servicesCount, - contacts: contactsCount, - unreadContacts: await Contact.countDocuments({ isRead: false }) + portfolioCount: portfolioCount, + servicesCount: servicesCount, + contactsCount: contactsCount, + usersCount: await User.count() }; res.render('admin/dashboard', { - title: 'Dashboard - Admin Panel', + title: 'Admin Dashboard', layout: 'admin/layout', user: req.session.user, stats, @@ -114,6 +124,346 @@ router.get('/dashboard', requireAuth, async (req, res) => { } }); +// Banner management +router.get('/banners', requireAuth, async (req, res) => { + try { + const page = parseInt(req.query.page) || 1; + const limit = 20; + const skip = (page - 1) * limit; + + const [banners, total] = await Promise.all([ + Banner.findAll({ + order: [['order', 'ASC'], ['createdAt', 'DESC']], + offset: skip, + limit: limit + }), + Banner.count() + ]); + + const totalPages = Math.ceil(total / limit); + + res.render('admin/banners/list', { + title: 'Banner Management - Admin Panel', + layout: 'admin/layout', + user: req.session.user, + banners, + pagination: { + current: page, + total: totalPages, + hasNext: page < totalPages, + hasPrev: page > 1 + } + }); + } catch (error) { + console.error('Banner list error:', error); + res.status(500).render('admin/error', { + title: 'Error - Admin Panel', + layout: 'admin/layout', + message: 'Error loading banners' + }); + } +}); + +// Add banner +router.get('/banners/add', requireAuth, (req, res) => { + res.render('admin/banners/add', { + title: 'Add Banner - Admin Panel', + layout: 'admin/layout', + user: req.session.user, + positions: [ + { value: 'hero', label: '메인 히어로' }, + { value: 'secondary', label: '보조 배너' }, + { value: 'footer', label: '푸터 배너' } + ], + animations: [ + { value: 'none', label: '없음' }, + { value: 'fade', label: '페이드' }, + { value: 'slide', label: '슬라이드' }, + { value: 'zoom', label: '줌' } + ] + }); +}); + +// Create banner +router.post('/banners/add', requireAuth, [ + body('title').notEmpty().withMessage('제목을 입력해주세요'), + body('position').isIn(['hero', 'secondary', 'footer']).withMessage('유효한 위치를 선택해주세요'), + body('order').isInt({ min: 0 }).withMessage('유효한 순서를 입력해주세요') +], async (req, res) => { + try { + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(400).json({ + success: false, + message: '입력 데이터를 확인해주세요', + errors: errors.array() + }); + } + + const { + title, + subtitle, + description, + buttonText, + buttonUrl, + image, + mobileImage, + position, + order, + isActive = true, + startDate, + endDate, + targetAudience = 'all', + backgroundColor, + textColor, + animation = 'none' + } = req.body; + + const banner = await Banner.create({ + title, + subtitle: subtitle || null, + description: description || null, + buttonText: buttonText || null, + buttonUrl: buttonUrl || null, + image: image || null, + mobileImage: mobileImage || null, + position, + order: parseInt(order), + isActive: Boolean(isActive), + startDate: startDate || null, + endDate: endDate || null, + targetAudience, + backgroundColor: backgroundColor || null, + textColor: textColor || null, + animation + }); + + res.json({ + success: true, + message: '배너가 성공적으로 생성되었습니다', + banner: { + id: banner.id, + title: banner.title, + position: banner.position + } + }); + } catch (error) { + console.error('Banner creation error:', error); + res.status(500).json({ + success: false, + message: '배너 생성 중 오류가 발생했습니다' + }); + } +}); + +// Edit banner +router.get('/banners/edit/:id', requireAuth, async (req, res) => { + try { + const banner = await Banner.findByPk(req.params.id); + + if (!banner) { + return res.status(404).render('admin/error', { + title: 'Error - Admin Panel', + layout: 'admin/layout', + message: 'Banner not found' + }); + } + + res.render('admin/banners/edit', { + title: 'Edit Banner - Admin Panel', + layout: 'admin/layout', + user: req.session.user, + banner, + positions: [ + { value: 'hero', label: '메인 히어로' }, + { value: 'secondary', label: '보조 배너' }, + { value: 'footer', label: '푸터 배너' } + ], + animations: [ + { value: 'none', label: '없음' }, + { value: 'fade', label: '페이드' }, + { value: 'slide', label: '슬라이드' }, + { value: 'zoom', label: '줌' } + ] + }); + } catch (error) { + console.error('Banner edit error:', error); + res.status(500).render('admin/error', { + title: 'Error - Admin Panel', + layout: 'admin/layout', + message: 'Error loading banner' + }); + } +}); + +// Update banner +router.put('/banners/:id', requireAuth, [ + body('title').notEmpty().withMessage('제목을 입력해주세요'), + body('position').isIn(['hero', 'secondary', 'footer']).withMessage('유효한 위치를 선택해주세요'), + body('order').isInt({ min: 0 }).withMessage('유효한 순서를 입력해주세요') +], async (req, res) => { + try { + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(400).json({ + success: false, + message: '입력 데이터를 확인해주세요', + errors: errors.array() + }); + } + + const banner = await Banner.findByPk(req.params.id); + if (!banner) { + return res.status(404).json({ + success: false, + message: '배너를 찾을 수 없습니다' + }); + } + + const { + title, + subtitle, + description, + buttonText, + buttonUrl, + image, + mobileImage, + position, + order, + isActive, + startDate, + endDate, + targetAudience, + backgroundColor, + textColor, + animation + } = req.body; + + await banner.update({ + title, + subtitle: subtitle || null, + description: description || null, + buttonText: buttonText || null, + buttonUrl: buttonUrl || null, + image: image || null, + mobileImage: mobileImage || null, + position, + order: parseInt(order), + isActive: Boolean(isActive), + startDate: startDate || null, + endDate: endDate || null, + targetAudience: targetAudience || 'all', + backgroundColor: backgroundColor || null, + textColor: textColor || null, + animation: animation || 'none' + }); + + res.json({ + success: true, + message: '배너가 성공적으로 업데이트되었습니다', + banner: { + id: banner.id, + title: banner.title, + position: banner.position + } + }); + } catch (error) { + console.error('Banner update error:', error); + res.status(500).json({ + success: false, + message: '배너 업데이트 중 오류가 발생했습니다' + }); + } +}); + +// Delete banner +router.delete('/banners/:id', requireAuth, async (req, res) => { + try { + const banner = await Banner.findByPk(req.params.id); + + if (!banner) { + return res.status(404).json({ + success: false, + message: '배너를 찾을 수 없습니다' + }); + } + + await banner.destroy(); + + res.json({ + success: true, + message: '배너가 성공적으로 삭제되었습니다' + }); + } catch (error) { + console.error('Banner deletion error:', error); + res.status(500).json({ + success: false, + message: '배너 삭제 중 오류가 발생했습니다' + }); + } +}); + +// Toggle banner active status +router.patch('/banners/:id/toggle-active', requireAuth, async (req, res) => { + try { + const banner = await Banner.findByPk(req.params.id); + + if (!banner) { + return res.status(404).json({ + success: false, + message: '배너를 찾을 수 없습니다' + }); + } + + const newStatus = !banner.isActive; + await banner.update({ isActive: newStatus }); + + res.json({ + success: true, + message: `배너가 ${newStatus ? '활성화' : '비활성화'}되었습니다`, + isActive: newStatus + }); + } catch (error) { + console.error('Banner toggle active error:', error); + res.status(500).json({ + success: false, + message: '상태 변경 중 오류가 발생했습니다' + }); + } +}); + +// Record banner click +router.post('/banners/:id/click', async (req, res) => { + try { + const banner = await Banner.findByPk(req.params.id); + + if (!banner) { + return res.status(404).json({ + success: false, + message: '배너를 찾을 수 없습니다' + }); + } + + await banner.recordClick(); + + res.json({ + success: true, + clickCount: banner.clickCount + }); + } catch (error) { + console.error('Banner click record error:', error); + res.status(500).json({ + success: false, + message: '클릭 기록 중 오류가 발생했습니다' + }); + } +}); + +// Banner Editor (legacy route) +router.get('/banner-editor', requireAuth, async (req, res) => { + res.redirect('/admin/banners'); +}); + // Portfolio management router.get('/portfolio', requireAuth, async (req, res) => { try { @@ -122,11 +472,12 @@ router.get('/portfolio', requireAuth, async (req, res) => { const skip = (page - 1) * limit; const [portfolio, total] = await Promise.all([ - Portfolio.find() - .sort({ createdAt: -1 }) - .skip(skip) - .limit(limit), - Portfolio.countDocuments() + Portfolio.findAll({ + order: [['createdAt', 'DESC']], + offset: skip, + limit: limit + }), + Portfolio.count() ]); const totalPages = Math.ceil(total / limit); @@ -153,19 +504,107 @@ router.get('/portfolio', requireAuth, async (req, res) => { } }); +// Utility function for category names +const getCategoryName = (category) => { + const categoryNames = { + 'web-development': 'Веб-разработка', + 'mobile-app': 'Мобильные приложения', + 'ui-ux-design': 'UI/UX дизайн', + 'e-commerce': 'Электронная коммерция', + 'enterprise': 'Корпоративное ПО', + 'other': 'Другое' + }; + return categoryNames[category] || category; +}; + // Add portfolio item router.get('/portfolio/add', requireAuth, (req, res) => { res.render('admin/portfolio/add', { title: 'Add Portfolio Item - Admin Panel', layout: 'admin/layout', - user: req.session.user + user: req.session.user, + categories: [ + 'web-development', + 'mobile-app', + 'ui-ux-design', + 'e-commerce', + 'enterprise', + 'other' + ], + getCategoryName: getCategoryName }); }); +// Create portfolio item +router.post('/portfolio/add', requireAuth, [ + body('title').notEmpty().withMessage('제목을 입력해주세요'), + body('shortDescription').notEmpty().withMessage('간단한 설명을 입력해주세요'), + body('description').notEmpty().withMessage('자세한 설명을 입력해주세요'), + body('category').notEmpty().withMessage('카테고리를 선택해주세요'), + body('technologies').isArray({ min: 1 }).withMessage('최소 한 개의 기술을 입력해주세요'), +], async (req, res) => { + try { + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(400).json({ + success: false, + message: '입력 데이터를 확인해주세요', + errors: errors.array() + }); + } + + const { + title, + shortDescription, + description, + category, + technologies, + demoUrl, + githubUrl, + clientName, + duration, + isPublished = false, + featured = false + } = req.body; + + const portfolio = await Portfolio.create({ + title, + shortDescription, + description, + category, + technologies: Array.isArray(technologies) ? technologies : technologies.split(',').map(t => t.trim()), + demoUrl: demoUrl || null, + githubUrl: githubUrl || null, + clientName: clientName || null, + duration: duration ? parseInt(duration) : null, + isPublished: Boolean(isPublished), + featured: Boolean(featured), + publishedAt: Boolean(isPublished) ? new Date() : null, + status: Boolean(isPublished) ? 'published' : 'draft' + }); + + res.json({ + success: true, + message: '포트폴리오가 성공적으로 생성되었습니다', + portfolio: { + id: portfolio.id, + title: portfolio.title, + category: portfolio.category + } + }); + } catch (error) { + console.error('Portfolio creation error:', error); + res.status(500).json({ + success: false, + message: '포트폴리오 생성 중 오류가 발생했습니다' + }); + } +}); + // Edit portfolio item router.get('/portfolio/edit/:id', requireAuth, async (req, res) => { try { - const portfolio = await Portfolio.findById(req.params.id); + const portfolio = await Portfolio.findByPk(req.params.id); if (!portfolio) { return res.status(404).render('admin/error', { @@ -179,7 +618,15 @@ router.get('/portfolio/edit/:id', requireAuth, async (req, res) => { title: 'Edit Portfolio Item - Admin Panel', layout: 'admin/layout', user: req.session.user, - portfolio + portfolio, + categories: [ + 'web-development', + 'mobile-app', + 'ui-ux-design', + 'e-commerce', + 'enterprise', + 'other' + ] }); } catch (error) { console.error('Portfolio edit error:', error); @@ -191,6 +638,140 @@ router.get('/portfolio/edit/:id', requireAuth, async (req, res) => { } }); +// Update portfolio item +router.put('/portfolio/:id', requireAuth, [ + body('title').notEmpty().withMessage('제목을 입력해주세요'), + body('shortDescription').notEmpty().withMessage('간단한 설명을 입력해주세요'), + body('description').notEmpty().withMessage('자세한 설명을 입력해주세요'), + body('category').notEmpty().withMessage('카테고리를 선택해주세요') +], async (req, res) => { + try { + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(400).json({ + success: false, + message: '입력 데이터를 확인해주세요', + errors: errors.array() + }); + } + + const portfolio = await Portfolio.findByPk(req.params.id); + if (!portfolio) { + return res.status(404).json({ + success: false, + message: '포트폴리오를 찾을 수 없습니다' + }); + } + + const { + title, + shortDescription, + description, + category, + technologies, + demoUrl, + githubUrl, + clientName, + duration, + isPublished, + featured + } = req.body; + + // Update portfolio + await portfolio.update({ + title, + shortDescription, + description, + category, + technologies: Array.isArray(technologies) ? technologies : technologies.split(',').map(t => t.trim()), + demoUrl: demoUrl || null, + githubUrl: githubUrl || null, + clientName: clientName || null, + duration: duration ? parseInt(duration) : null, + isPublished: Boolean(isPublished), + featured: Boolean(featured), + publishedAt: Boolean(isPublished) && !portfolio.publishedAt ? new Date() : portfolio.publishedAt, + status: Boolean(isPublished) ? 'published' : 'draft' + }); + + res.json({ + success: true, + message: '포트폴리오가 성공적으로 업데이트되었습니다', + portfolio: { + id: portfolio.id, + title: portfolio.title, + category: portfolio.category + } + }); + } catch (error) { + console.error('Portfolio update error:', error); + res.status(500).json({ + success: false, + message: '포트폴리오 업데이트 중 오류가 발생했습니다' + }); + } +}); + +// Delete portfolio item +router.delete('/portfolio/:id', requireAuth, async (req, res) => { + try { + const portfolio = await Portfolio.findByPk(req.params.id); + + if (!portfolio) { + return res.status(404).json({ + success: false, + message: '포트폴리오를 찾을 수 없습니다' + }); + } + + await portfolio.destroy(); + + res.json({ + success: true, + message: '포트폴리오가 성공적으로 삭제되었습니다' + }); + } catch (error) { + console.error('Portfolio deletion error:', error); + res.status(500).json({ + success: false, + message: '포트폴리오 삭제 중 오류가 발생했습니다' + }); + } +}); + +// Toggle portfolio publish status +router.patch('/portfolio/:id/toggle-publish', requireAuth, async (req, res) => { + try { + const portfolio = await Portfolio.findByPk(req.params.id); + + if (!portfolio) { + return res.status(404).json({ + success: false, + message: '포트폴리오를 찾을 수 없습니다' + }); + } + + const newStatus = !portfolio.isPublished; + await portfolio.update({ + isPublished: newStatus, + publishedAt: newStatus && !portfolio.publishedAt ? new Date() : portfolio.publishedAt, + status: newStatus ? 'published' : 'draft' + }); + + res.json({ + success: true, + message: `포트폴리오가 ${newStatus ? '게시' : '비공개'}되었습니다`, + isPublished: newStatus + }); + } catch (error) { + console.error('Portfolio toggle publish error:', error); + res.status(500).json({ + success: false, + message: '상태 변경 중 오류가 발생했습니다' + }); + } +}); + // Services management router.get('/services', requireAuth, async (req, res) => { try { @@ -199,11 +780,12 @@ router.get('/services', requireAuth, async (req, res) => { const skip = (page - 1) * limit; const [services, total] = await Promise.all([ - Service.find() - .sort({ createdAt: -1 }) - .skip(skip) - .limit(limit), - Service.countDocuments() + Service.findAll({ + order: [['createdAt', 'DESC']], + offset: skip, + limit: limit + }), + Service.count() ]); const totalPages = Math.ceil(total / limit); @@ -235,15 +817,84 @@ router.get('/services/add', requireAuth, (req, res) => { res.render('admin/services/add', { title: 'Add Service - Admin Panel', layout: 'admin/layout', - user: req.session.user + user: req.session.user, + serviceTypes: [ + 'web-development', + 'mobile-app', + 'ui-ux-design', + 'e-commerce', + 'seo', + 'maintenance', + 'consultation', + 'other' + ] }); }); +// Create service +router.post('/services/add', requireAuth, [ + body('name').notEmpty().withMessage('서비스명을 입력해주세요'), + body('shortDescription').notEmpty().withMessage('간단한 설명을 입력해주세요'), + body('description').notEmpty().withMessage('자세한 설명을 입력해주세요'), + body('category').notEmpty().withMessage('카테고리를 선택해주세요'), + body('basePrice').isNumeric().withMessage('기본 가격을 입력해주세요') +], async (req, res) => { + try { + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(400).json({ + success: false, + message: '입력 데이터를 확인해주세요', + errors: errors.array() + }); + } + + const { + name, + shortDescription, + description, + category, + basePrice, + features, + duration, + isActive = true, + featured = false + } = req.body; + + const service = await Service.create({ + name, + shortDescription, + description, + category, + basePrice: parseFloat(basePrice), + features: features || [], + duration: duration ? parseInt(duration) : null, + isActive: Boolean(isActive), + featured: Boolean(featured) + }); + + res.json({ + success: true, + message: '서비스가 성공적으로 생성되었습니다', + service: { + id: service.id, + name: service.name, + category: service.category + } + }); + } catch (error) { + console.error('Service creation error:', error); + res.status(500).json({ + success: false, + message: '서비스 생성 중 오류가 발생했습니다' + }); + } +}); + // Edit service router.get('/services/edit/:id', requireAuth, async (req, res) => { try { - const service = await Service.findById(req.params.id) - .populate('portfolio', 'title'); + const service = await Service.findByPk(req.params.id); if (!service) { return res.status(404).render('admin/error', { @@ -253,15 +904,27 @@ router.get('/services/edit/:id', requireAuth, async (req, res) => { }); } - const availablePortfolio = await Portfolio.find({ isPublished: true }) - .select('title category'); + const availablePortfolio = await Portfolio.findAll({ + where: { isPublished: true }, + attributes: ['id', 'title', 'category'] + }); res.render('admin/services/edit', { title: 'Edit Service - Admin Panel', layout: 'admin/layout', user: req.session.user, service, - availablePortfolio + availablePortfolio, + serviceTypes: [ + 'web-development', + 'mobile-app', + 'ui-ux-design', + 'e-commerce', + 'seo', + 'maintenance', + 'consultation', + 'other' + ] }); } catch (error) { console.error('Service edit error:', error); @@ -273,6 +936,130 @@ router.get('/services/edit/:id', requireAuth, async (req, res) => { } }); +// Update service +router.put('/services/:id', requireAuth, [ + body('name').notEmpty().withMessage('서비스명을 입력해주세요'), + body('shortDescription').notEmpty().withMessage('간단한 설명을 입력해주세요'), + body('description').notEmpty().withMessage('자세한 설명을 입력해주세요'), + body('category').notEmpty().withMessage('카테고리를 선택해주세요'), + body('basePrice').isNumeric().withMessage('기본 가격을 입력해주세요') +], async (req, res) => { + try { + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(400).json({ + success: false, + message: '입력 데이터를 확인해주세요', + errors: errors.array() + }); + } + + const service = await Service.findByPk(req.params.id); + if (!service) { + return res.status(404).json({ + success: false, + message: '서비스를 찾을 수 없습니다' + }); + } + + const { + name, + shortDescription, + description, + category, + basePrice, + features, + duration, + isActive, + featured + } = req.body; + + await service.update({ + name, + shortDescription, + description, + category, + basePrice: parseFloat(basePrice), + features: features || [], + duration: duration ? parseInt(duration) : null, + isActive: Boolean(isActive), + featured: Boolean(featured) + }); + + res.json({ + success: true, + message: '서비스가 성공적으로 업데이트되었습니다', + service: { + id: service.id, + name: service.name, + category: service.category + } + }); + } catch (error) { + console.error('Service update error:', error); + res.status(500).json({ + success: false, + message: '서비스 업데이트 중 오류가 발생했습니다' + }); + } +}); + +// Delete service +router.delete('/services/:id', requireAuth, async (req, res) => { + try { + const service = await Service.findByPk(req.params.id); + + if (!service) { + return res.status(404).json({ + success: false, + message: '서비스를 찾을 수 없습니다' + }); + } + + await service.destroy(); + + res.json({ + success: true, + message: '서비스가 성공적으로 삭제되었습니다' + }); + } catch (error) { + console.error('Service deletion error:', error); + res.status(500).json({ + success: false, + message: '서비스 삭제 중 오류가 발생했습니다' + }); + } +}); + +// Toggle service active status +router.patch('/services/:id/toggle-active', requireAuth, async (req, res) => { + try { + const service = await Service.findByPk(req.params.id); + + if (!service) { + return res.status(404).json({ + success: false, + message: '서비스를 찾을 수 없습니다' + }); + } + + const newStatus = !service.isActive; + await service.update({ isActive: newStatus }); + + res.json({ + success: true, + message: `서비스가 ${newStatus ? '활성화' : '비활성화'}되었습니다`, + isActive: newStatus + }); + } catch (error) { + console.error('Service toggle active error:', error); + res.status(500).json({ + success: false, + message: '상태 변경 중 오류가 발생했습니다' + }); + } +}); + // Contacts management router.get('/contacts', requireAuth, async (req, res) => { try { @@ -281,17 +1068,19 @@ router.get('/contacts', requireAuth, async (req, res) => { const skip = (page - 1) * limit; const status = req.query.status; - let query = {}; + let whereClause = {}; if (status && status !== 'all') { - query.status = status; + whereClause.status = status; } const [contacts, total] = await Promise.all([ - Contact.find(query) - .sort({ createdAt: -1 }) - .skip(skip) - .limit(limit), - Contact.countDocuments(query) + Contact.findAll({ + where: whereClause, + order: [['createdAt', 'DESC']], + offset: skip, + limit: limit + }), + Contact.count({ where: whereClause }) ]); const totalPages = Math.ceil(total / limit); @@ -322,7 +1111,7 @@ router.get('/contacts', requireAuth, async (req, res) => { // View contact details router.get('/contacts/:id', requireAuth, async (req, res) => { try { - const contact = await Contact.findById(req.params.id); + const contact = await Contact.findByPk(req.params.id); if (!contact) { return res.status(404).render('admin/error', { @@ -357,7 +1146,7 @@ router.get('/contacts/:id', requireAuth, async (req, res) => { // Settings router.get('/settings', requireAuth, async (req, res) => { try { - const settings = await SiteSettings.findOne() || new SiteSettings(); + const settings = await SiteSettings.findOne() || await SiteSettings.create({}); res.render('admin/settings', { title: 'Site Settings - Admin Panel', @@ -384,4 +1173,268 @@ router.get('/media', requireAuth, (req, res) => { }); }); +// Telegram bot configuration and testing +router.get('/telegram', requireAuth, async (req, res) => { + try { + const telegramService = require('../services/telegram'); + + // Get bot info and available chats if token is configured + let botInfo = null; + let availableChats = []; + + if (telegramService.botToken) { + const result = await telegramService.getBotInfo(); + if (result.success) { + botInfo = result.bot; + availableChats = telegramService.getAvailableChats(); + } + } + + res.render('admin/telegram', { + title: 'Telegram Bot - Admin Panel', + layout: 'admin/layout', + user: req.session.user, + botConfigured: telegramService.isEnabled, + botToken: telegramService.botToken || '', + chatId: telegramService.chatId || '', + botInfo, + availableChats + }); + } catch (error) { + console.error('Telegram page error:', error); + res.status(500).render('admin/error', { + title: 'Error - Admin Panel', + layout: 'admin/layout', + message: 'Error loading Telegram settings' + }); + } +}); + +// Update bot token +router.post('/telegram/configure', requireAuth, [ + body('botToken').notEmpty().withMessage('Bot token is required'), + body('chatId').optional().isNumeric().withMessage('Chat ID must be numeric') +], async (req, res) => { + try { + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(400).json({ + success: false, + message: 'Validation failed', + errors: errors.array() + }); + } + + const { botToken, chatId } = req.body; + const telegramService = require('../services/telegram'); + + // Update bot token + const result = await telegramService.updateBotToken(botToken); + + if (result.success) { + // Update chat ID if provided + if (chatId) { + telegramService.updateChatId(parseInt(chatId)); + } + + // Update environment variables (in production, this should update a config file) + process.env.TELEGRAM_BOT_TOKEN = botToken; + if (chatId) { + process.env.TELEGRAM_CHAT_ID = chatId; + } + + res.json({ + success: true, + message: 'Telegram bot configured successfully', + botInfo: result.bot, + availableChats: result.availableChats || [] + }); + } else { + res.status(400).json({ + success: false, + message: result.error || 'Failed to configure bot' + }); + } + } catch (error) { + console.error('Configure Telegram bot error:', error); + res.status(500).json({ + success: false, + message: 'Error configuring Telegram bot' + }); + } +}); + +// Get bot info and discover chats +router.get('/telegram/info', requireAuth, async (req, res) => { + try { + const telegramService = require('../services/telegram'); + const result = await telegramService.testConnection(); + + if (result.success) { + res.json({ + success: true, + botInfo: result.bot, + availableChats: result.availableChats || [], + isConfigured: telegramService.isEnabled + }); + } else { + res.status(400).json({ + success: false, + message: result.error || result.message || 'Failed to get bot info' + }); + } + } catch (error) { + console.error('Get Telegram info error:', error); + res.status(500).json({ + success: false, + message: 'Error getting bot information' + }); + } +}); + +// Get chat information +router.get('/telegram/chat/:chatId', requireAuth, async (req, res) => { + try { + const telegramService = require('../services/telegram'); + const result = await telegramService.getChat(req.params.chatId); + + if (result.success) { + res.json({ + success: true, + chat: result.chat + }); + } else { + res.status(400).json({ + success: false, + message: result.error || 'Failed to get chat info' + }); + } + } catch (error) { + console.error('Get chat info error:', error); + res.status(500).json({ + success: false, + message: 'Error getting chat information' + }); + } +}); + +// Test connection +router.post('/telegram/test', requireAuth, async (req, res) => { + try { + const telegramService = require('../services/telegram'); + const result = await telegramService.testConnection(); + + if (result.success) { + const testMessage = `🤖 Тест Telegram бота\n\n` + + `✅ Соединение успешно установлено!\n` + + `🤖 Бот: @${result.bot.username} (${result.bot.first_name})\n` + + `🆔 ID бота: ${result.bot.id}\n` + + `⏰ Время тестирования: ${new Date().toLocaleString('ru-RU')}\n` + + `🌐 Сайт: ${process.env.BASE_URL || 'http://localhost:3000'}\n\n` + + `Бот готов к отправке уведомлений! 🚀`; + + const sendResult = await telegramService.sendMessage(testMessage); + + if (sendResult.success) { + res.json({ + success: true, + message: 'Test message sent successfully!', + botInfo: result.bot, + availableChats: result.availableChats || [] + }); + } else { + res.status(400).json({ + success: false, + message: 'Bot connection successful, but failed to send test message: ' + (sendResult.error || sendResult.message) + }); + } + } else { + res.status(400).json({ + success: false, + message: result.message || result.error || 'Failed to connect to Telegram bot' + }); + } + } catch (error) { + console.error('Telegram test error:', error); + res.status(500).json({ + success: false, + message: 'Error testing Telegram bot' + }); + } +}); + +// Send custom message +router.post('/telegram/send', requireAuth, [ + body('message').notEmpty().withMessage('Message is required'), + body('chatIds').optional().isArray().withMessage('Chat IDs must be an array'), + body('parseMode').optional().isIn(['HTML', 'Markdown', 'MarkdownV2']).withMessage('Invalid parse mode') +], async (req, res) => { + try { + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(400).json({ + success: false, + message: 'Validation failed', + errors: errors.array() + }); + } + + const { + message, + chatIds = [], + parseMode = 'HTML', + disableWebPagePreview = false, + disableNotification = false + } = req.body; + + const telegramService = require('../services/telegram'); + + let result; + if (chatIds.length > 0) { + // Send to multiple chats + result = await telegramService.sendCustomMessage({ + text: message, + chatIds: chatIds.map(id => parseInt(id)), + parseMode, + disableWebPagePreview, + disableNotification + }); + + res.json({ + success: result.success, + message: result.success ? + `Message sent to ${result.totalSent} chat(s). ${result.totalFailed} failed.` : + 'Failed to send message', + results: result.results || [], + errors: result.errors || [] + }); + } else { + // Send to default chat + result = await telegramService.sendMessage(message, { + parse_mode: parseMode, + disable_web_page_preview: disableWebPagePreview, + disable_notification: disableNotification + }); + + if (result.success) { + res.json({ + success: true, + message: 'Message sent successfully!' + }); + } else { + res.status(400).json({ + success: false, + message: result.error || result.message || 'Failed to send message' + }); + } + } + } catch (error) { + console.error('Send Telegram message error:', error); + res.status(500).json({ + success: false, + message: 'Error sending message' + }); + } +}); + module.exports = router; \ No newline at end of file diff --git a/routes/api/admin.js b/routes/api/admin.js new file mode 100644 index 0000000..f7fc941 --- /dev/null +++ b/routes/api/admin.js @@ -0,0 +1,388 @@ +const express = require('express'); +const router = express.Router(); +const multer = require('multer'); +const path = require('path'); +const { Portfolio, Service, Contact, User } = require('../../models'); + +// Multer configuration for file uploads +const storage = multer.diskStorage({ + destination: (req, file, cb) => { + cb(null, 'public/uploads/'); + }, + filename: (req, file, cb) => { + const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9); + cb(null, file.fieldname + '-' + uniqueSuffix + path.extname(file.originalname)); + } +}); + +const upload = multer({ + storage: storage, + fileFilter: (req, file, cb) => { + if (file.mimetype.startsWith('image/')) { + cb(null, true); + } else { + cb(new Error('Only image files are allowed!'), false); + } + }, + limits: { + fileSize: 10 * 1024 * 1024 // 10MB + } +}); + +// Authentication middleware +const requireAuth = (req, res, next) => { + if (!req.session.user) { + return res.status(401).json({ success: false, message: 'Authentication required' }); + } + next(); +}; + +// Portfolio API Routes +router.post('/portfolio', requireAuth, upload.array('images', 10), async (req, res) => { + try { + const { + title, + shortDescription, + description, + category, + clientName, + projectUrl, + githubUrl, + technologies, + featured, + isPublished + } = req.body; + + // Process uploaded images + const images = req.files ? req.files.map((file, index) => ({ + url: `/uploads/${file.filename}`, + alt: `${title} image ${index + 1}`, + isPrimary: index === 0 + })) : []; + + // Parse technologies + let techArray = []; + if (technologies) { + try { + techArray = JSON.parse(technologies); + } catch (e) { + techArray = technologies.split(',').map(t => t.trim()); + } + } + + const portfolio = await Portfolio.create({ + title, + shortDescription, + description, + category, + clientName, + projectUrl: projectUrl || null, + githubUrl: githubUrl || null, + technologies: techArray, + images, + featured: featured === 'on', + isPublished: isPublished === 'on', + status: 'completed', + publishedAt: isPublished === 'on' ? new Date() : null + }); + + res.json({ success: true, portfolio }); + } catch (error) { + console.error('Portfolio creation error:', error); + res.status(500).json({ success: false, message: error.message }); + } +}); + +router.patch('/portfolio/:id', requireAuth, upload.array('images', 10), async (req, res) => { + try { + const portfolio = await Portfolio.findByPk(req.params.id); + if (!portfolio) { + return res.status(404).json({ success: false, message: 'Portfolio not found' }); + } + + const updates = { ...req.body }; + + // Handle checkboxes + updates.featured = updates.featured === 'on'; + updates.isPublished = updates.isPublished === 'on'; + + // Process technologies + if (updates.technologies) { + try { + updates.technologies = JSON.parse(updates.technologies); + } catch (e) { + updates.technologies = updates.technologies.split(',').map(t => t.trim()); + } + } + + // Process new images + if (req.files && req.files.length > 0) { + const newImages = req.files.map((file, index) => ({ + url: `/uploads/${file.filename}`, + alt: `${updates.title || portfolio.title} image ${index + 1}`, + isPrimary: index === 0 && (!portfolio.images || portfolio.images.length === 0) + })); + + updates.images = [...(portfolio.images || []), ...newImages]; + } + + await portfolio.update(updates); + res.json({ success: true, portfolio }); + } catch (error) { + console.error('Portfolio update error:', error); + res.status(500).json({ success: false, message: error.message }); + } +}); + +router.delete('/portfolio/:id', requireAuth, async (req, res) => { + try { + const portfolio = await Portfolio.findByPk(req.params.id); + if (!portfolio) { + return res.status(404).json({ success: false, message: 'Portfolio not found' }); + } + + await portfolio.destroy(); + res.json({ success: true }); + } catch (error) { + console.error('Portfolio deletion error:', error); + res.status(500).json({ success: false, message: error.message }); + } +}); + +// Services API Routes +router.post('/services', requireAuth, async (req, res) => { + try { + const { + name, + description, + shortDescription, + icon, + category, + features, + pricing, + estimatedTime, + isActive, + featured, + tags + } = req.body; + + // Parse arrays + let featuresArray = []; + let tagsArray = []; + let pricingObj = {}; + + if (features) { + try { + featuresArray = JSON.parse(features); + } catch (e) { + featuresArray = features.split(',').map(f => f.trim()); + } + } + + if (tags) { + try { + tagsArray = JSON.parse(tags); + } catch (e) { + tagsArray = tags.split(',').map(t => t.trim()); + } + } + + if (pricing) { + try { + pricingObj = JSON.parse(pricing); + } catch (e) { + pricingObj = { basePrice: pricing }; + } + } + + const service = await Service.create({ + name, + description, + shortDescription, + icon, + category, + features: featuresArray, + pricing: pricingObj, + estimatedTime, + isActive: isActive === 'on', + featured: featured === 'on', + tags: tagsArray + }); + + res.json({ success: true, service }); + } catch (error) { + console.error('Service creation error:', error); + res.status(500).json({ success: false, message: error.message }); + } +}); + +router.delete('/services/:id', requireAuth, async (req, res) => { + try { + const service = await Service.findByPk(req.params.id); + if (!service) { + return res.status(404).json({ success: false, message: 'Service not found' }); + } + + await service.destroy(); + res.json({ success: true }); + } catch (error) { + console.error('Service deletion error:', error); + res.status(500).json({ success: false, message: error.message }); + } +}); + +// Contacts API Routes +router.patch('/contacts/:id', requireAuth, async (req, res) => { + try { + const contact = await Contact.findByPk(req.params.id); + if (!contact) { + return res.status(404).json({ success: false, message: 'Contact not found' }); + } + + await contact.update(req.body); + res.json({ success: true, contact }); + } catch (error) { + console.error('Contact update error:', error); + res.status(500).json({ success: false, message: error.message }); + } +}); + +router.delete('/contacts/:id', requireAuth, async (req, res) => { + try { + const contact = await Contact.findByPk(req.params.id); + if (!contact) { + return res.status(404).json({ success: false, message: 'Contact not found' }); + } + + await contact.destroy(); + res.json({ success: true }); + } catch (error) { + console.error('Contact deletion error:', error); + res.status(500).json({ success: false, message: error.message }); + } +}); + +// Telegram notification for contact +router.post('/contacts/:id/telegram', requireAuth, async (req, res) => { + try { + const contact = await Contact.findByPk(req.params.id); + if (!contact) { + return res.status(404).json({ success: false, message: 'Contact not found' }); + } + + // Send Telegram notification + const telegramService = require('../../services/telegram'); + const result = await telegramService.sendContactNotification(contact); + + if (result.success) { + res.json({ success: true }); + } else { + res.status(500).json({ success: false, message: result.message || result.error }); + } + } catch (error) { + console.error('Telegram notification error:', error); + res.status(500).json({ success: false, message: error.message }); + } +}); + +// Test Telegram connection +router.post('/telegram/test', requireAuth, async (req, res) => { + try { + const { botToken, chatId } = req.body; + + // Temporarily set up telegram service with provided credentials + const axios = require('axios'); + + // Test bot info + const botResponse = await axios.get(`https://api.telegram.org/bot${botToken}/getMe`); + + // Test sending a message + const testMessage = '✅ Telegram bot подключен успешно!\n\nЭто тестовое сообщение от SmartSolTech Admin Panel.'; + await axios.post(`https://api.telegram.org/bot${botToken}/sendMessage`, { + chat_id: chatId, + text: testMessage, + parse_mode: 'Markdown' + }); + + res.json({ + success: true, + bot: botResponse.data.result, + message: 'Test message sent successfully' + }); + } catch (error) { + console.error('Telegram test error:', error); + let message = 'Connection failed'; + + if (error.response?.data?.description) { + message = error.response.data.description; + } else if (error.message) { + message = error.message; + } + + res.status(400).json({ success: false, message }); + } +}); + +// Settings API +const { SiteSettings } = require('../../models'); + +router.get('/settings', requireAuth, async (req, res) => { + try { + const settings = await SiteSettings.findOne() || {}; + res.json({ success: true, settings }); + } catch (error) { + console.error('Settings fetch error:', error); + res.status(500).json({ success: false, message: error.message }); + } +}); + +router.post('/settings', requireAuth, upload.fields([ + { name: 'logo', maxCount: 1 }, + { name: 'favicon', maxCount: 1 } +]), async (req, res) => { + try { + let settings = await SiteSettings.findOne(); + if (!settings) { + settings = await SiteSettings.create({}); + } + + const updates = {}; + + // Handle nested objects + Object.keys(req.body).forEach(key => { + if (key.includes('.')) { + const [parent, child] = key.split('.'); + if (!updates[parent]) updates[parent] = {}; + updates[parent][child] = req.body[key]; + } else { + updates[key] = req.body[key]; + } + }); + + // Handle file uploads + if (req.files.logo) { + updates.logo = `/uploads/${req.files.logo[0].filename}`; + } + if (req.files.favicon) { + updates.favicon = `/uploads/${req.files.favicon[0].filename}`; + } + + // Update existing settings with new values + Object.keys(updates).forEach(key => { + if (typeof updates[key] === 'object' && updates[key] !== null) { + settings[key] = { ...settings[key], ...updates[key] }; + } else { + settings[key] = updates[key]; + } + }); + + await settings.save(); + + res.json({ success: true, settings }); + } catch (error) { + console.error('Settings update error:', error); + res.status(500).json({ success: false, message: error.message }); + } +}); + +module.exports = router; \ No newline at end of file diff --git a/routes/auth.js b/routes/auth.js index de4d91e..7a71b05 100644 --- a/routes/auth.js +++ b/routes/auth.js @@ -2,7 +2,7 @@ const express = require('express'); const router = express.Router(); const jwt = require('jsonwebtoken'); const { body, validationResult } = require('express-validator'); -const User = require('../models/User'); +const { User } = require('../models'); // Login validation rules const loginValidation = [ @@ -25,7 +25,12 @@ router.post('/login', loginValidation, async (req, res) => { const { email, password } = req.body; // Find user - const user = await User.findOne({ email, isActive: true }); + const user = await User.findOne({ + where: { + email: email, + isActive: true + } + }); if (!user) { return res.status(401).json({ success: false, @@ -109,8 +114,9 @@ router.get('/me', async (req, res) => { }); } - const user = await User.findById(req.session.user.id) - .select('-password'); + const user = await User.findByPk(req.session.user.id, { + attributes: { exclude: ['password'] } + }); if (!user || !user.isActive) { req.session.destroy(); @@ -163,7 +169,7 @@ router.put('/change-password', [ } const { currentPassword, newPassword } = req.body; - const user = await User.findById(req.session.user.id); + const user = await User.findByPk(req.session.user.id); if (!user) { return res.status(404).json({ diff --git a/routes/calculator.js b/routes/calculator.js index 90559e4..d701cda 100644 --- a/routes/calculator.js +++ b/routes/calculator.js @@ -1,13 +1,15 @@ const express = require('express'); const router = express.Router(); -const Service = require('../models/Service'); +const { Service } = require('../models'); // Get all services for calculator router.get('/services', async (req, res) => { try { - const services = await Service.find({ isActive: true }) - .select('name pricing category features estimatedTime') - .sort({ category: 1, name: 1 }); + const services = await Service.findAll({ + where: { isActive: true }, + attributes: ['id', 'name', 'pricing', 'category', 'features', 'estimatedTime'], + order: [['category', 'ASC'], ['name', 'ASC']] + }); const servicesByCategory = services.reduce((acc, service) => { if (!acc[service.category]) { @@ -50,9 +52,11 @@ router.post('/calculate', async (req, res) => { } // Get selected services details - const services = await Service.find({ - _id: { $in: selectedServices }, - isActive: true + const services = await Service.findAll({ + where: { + id: selectedServices, + isActive: true + } }); if (services.length !== selectedServices.length) { diff --git a/routes/contact.js b/routes/contact.js index 3ffc6e6..6f336eb 100644 --- a/routes/contact.js +++ b/routes/contact.js @@ -2,14 +2,8 @@ const express = require('express'); const router = express.Router(); const { body, validationResult } = require('express-validator'); const nodemailer = require('nodemailer'); -const Contact = require('../models/Contact'); -const TelegramBot = require('node-telegram-bot-api'); - -// Initialize Telegram bot if token is provided -let bot = null; -if (process.env.TELEGRAM_BOT_TOKEN) { - bot = new TelegramBot(process.env.TELEGRAM_BOT_TOKEN, { polling: false }); -} +const { Contact } = require('../models'); +const telegramService = require('../services/telegram'); // Contact form validation const contactValidation = [ @@ -48,7 +42,7 @@ router.post('/submit', contactValidation, async (req, res) => { await sendEmailNotification(contact); // Send Telegram notification - await sendTelegramNotification(contact); + await telegramService.sendNewContactAlert(contact); res.json({ success: true, @@ -108,7 +102,10 @@ router.post('/estimate', [ // Send notifications await sendEmailNotification(contact); - await sendTelegramNotification(contact); + await telegramService.sendCalculatorQuote({ + ...contactData, + services: services.map(s => ({ name: s, price: 0 })) // Simplified for now + }); res.json({ success: true, @@ -170,42 +167,7 @@ async function sendEmailNotification(contact) { } } -// Helper function to send Telegram notification -async function sendTelegramNotification(contact) { - if (!bot || !process.env.TELEGRAM_CHAT_ID) { - console.log('Telegram configuration not provided, skipping Telegram notification'); - return; - } - - try { - const message = ` -🔔 *New Contact Form Submission* - -👤 *Name:* ${contact.name} -📧 *Email:* ${contact.email} -📱 *Phone:* ${contact.phone || 'Not provided'} -🏢 *Company:* ${contact.company || 'Not provided'} -📝 *Subject:* ${contact.subject} - -💬 *Message:* -${contact.message} - -📍 *Source:* ${contact.source} -🕐 *Time:* ${contact.createdAt.toLocaleString()} - -[View in Admin Panel](${process.env.SITE_URL}/admin/contacts/${contact._id}) - `; - - await bot.sendMessage(process.env.TELEGRAM_CHAT_ID, message, { - parse_mode: 'Markdown', - disable_web_page_preview: true - }); - - console.log('Telegram notification sent successfully'); - } catch (error) { - console.error('Telegram notification error:', error); - } -} +// Telegram notifications now handled by telegramService // Helper function to calculate project estimate function calculateProjectEstimate(services, projectType, timeline) { diff --git a/routes/index.js b/routes/index.js index 65d2ed0..cd5dfb4 100644 --- a/routes/index.js +++ b/routes/index.js @@ -1,20 +1,23 @@ const express = require('express'); const router = express.Router(); -const Portfolio = require('../models/Portfolio'); -const Service = require('../models/Service'); -const SiteSettings = require('../models/SiteSettings'); +const { Portfolio, Service, SiteSettings } = require('../models'); +const { Op } = require('sequelize'); // Home page router.get('/', async (req, res) => { try { const [settings, featuredPortfolio, featuredServices] = await Promise.all([ SiteSettings.findOne() || {}, - Portfolio.find({ featured: true, isPublished: true }) - .sort({ order: 1, createdAt: -1 }) - .limit(6), - Service.find({ featured: true, isActive: true }) - .sort({ order: 1 }) - .limit(4) + Portfolio.findAll({ + where: { featured: true, isPublished: true }, + order: [['order', 'ASC'], ['createdAt', 'DESC']], + limit: 6 + }), + Service.findAll({ + where: { featured: true, isActive: true }, + order: [['order', 'ASC']], + limit: 4 + }) ]); res.render('index', { @@ -28,6 +31,7 @@ router.get('/', async (req, res) => { console.error('Home page error:', error); res.status(500).render('error', { title: 'Error', + settings: {}, message: 'Something went wrong' }); } @@ -47,6 +51,7 @@ router.get('/about', async (req, res) => { console.error('About page error:', error); res.status(500).render('error', { title: 'Error', + settings: {}, message: 'Something went wrong' }); } @@ -65,20 +70,28 @@ router.get('/portfolio', async (req, res) => { query.category = category; } - const [portfolio, total, categories] = await Promise.all([ - Portfolio.find(query) - .sort({ featured: -1, publishedAt: -1 }) - .skip(skip) - .limit(limit), - Portfolio.countDocuments(query), - Portfolio.distinct('category', { isPublished: true }) + const [settings, portfolio, total, categories] = await Promise.all([ + SiteSettings.findOne() || {}, + Portfolio.findAll({ + where: query, + order: [['featured', 'DESC'], ['publishedAt', 'DESC']], + offset: skip, + limit: limit + }), + Portfolio.count({ where: query }), + Portfolio.findAll({ + where: { isPublished: true }, + attributes: ['category'], + group: ['category'] + }).then(results => results.map(r => r.category)) ]); const totalPages = Math.ceil(total / limit); res.render('portfolio', { title: 'Portfolio - SmartSolTech', - portfolio, + settings: settings || {}, + portfolioItems: portfolio, categories, currentCategory: category || 'all', pagination: { @@ -93,6 +106,7 @@ router.get('/portfolio', async (req, res) => { console.error('Portfolio page error:', error); res.status(500).render('error', { title: 'Error', + settings: {}, message: 'Something went wrong' }); } @@ -101,11 +115,15 @@ router.get('/portfolio', async (req, res) => { // Portfolio detail page router.get('/portfolio/:id', async (req, res) => { try { - const portfolio = await Portfolio.findById(req.params.id); + const [settings, portfolio] = await Promise.all([ + SiteSettings.findOne() || {}, + Portfolio.findByPk(req.params.id) + ]); if (!portfolio || !portfolio.isPublished) { return res.status(404).render('404', { title: '404 - Project Not Found', + settings: settings || {}, message: 'The requested project was not found' }); } @@ -115,14 +133,19 @@ router.get('/portfolio/:id', async (req, res) => { await portfolio.save(); // Get related projects - const relatedProjects = await Portfolio.find({ - _id: { $ne: portfolio._id }, - category: portfolio.category, - isPublished: true - }).limit(3); + const relatedProjects = await Portfolio.findAll({ + where: { + id: { [Op.ne]: portfolio.id }, + category: portfolio.category, + isPublished: true + }, + order: [['publishedAt', 'DESC']], + limit: 3 + }); res.render('portfolio-detail', { title: `${portfolio.title} - Portfolio - SmartSolTech`, + settings: settings || {}, portfolio, relatedProjects, currentPage: 'portfolio' @@ -131,6 +154,7 @@ router.get('/portfolio/:id', async (req, res) => { console.error('Portfolio detail error:', error); res.status(500).render('error', { title: 'Error', + settings: {}, message: 'Something went wrong' }); } @@ -139,14 +163,22 @@ router.get('/portfolio/:id', async (req, res) => { // Services page router.get('/services', async (req, res) => { try { - const services = await Service.find({ isActive: true }) - .sort({ featured: -1, order: 1 }) - .populate('portfolio', 'title images'); - - const categories = await Service.distinct('category', { isActive: true }); + const [settings, services, categories] = await Promise.all([ + SiteSettings.findOne() || {}, + Service.findAll({ + where: { isActive: true }, + order: [['featured', 'DESC'], ['order', 'ASC']] + }), + Service.findAll({ + where: { isActive: true }, + attributes: ['category'], + group: ['category'] + }) + ]); res.render('services', { title: 'Services - SmartSolTech', + settings: settings || {}, services, categories, currentPage: 'services' @@ -155,6 +187,7 @@ router.get('/services', async (req, res) => { console.error('Services page error:', error); res.status(500).render('error', { title: 'Error', + settings: {}, message: 'Something went wrong' }); } @@ -163,12 +196,18 @@ router.get('/services', async (req, res) => { // Calculator page router.get('/calculator', async (req, res) => { try { - const services = await Service.find({ isActive: true }) - .select('name pricing category') - .sort({ category: 1, name: 1 }); + const [settings, services] = await Promise.all([ + SiteSettings.findOne() || {}, + Service.findAll({ + where: { isActive: true }, + attributes: ['id', 'name', 'pricing', 'category'], + order: [['category', 'ASC'], ['name', 'ASC']] + }) + ]); res.render('calculator', { title: 'Project Calculator - SmartSolTech', + settings: settings || {}, services, currentPage: 'calculator' }); @@ -176,6 +215,7 @@ router.get('/calculator', async (req, res) => { console.error('Calculator page error:', error); res.status(500).render('error', { title: 'Error', + settings: {}, message: 'Something went wrong' }); } @@ -195,6 +235,7 @@ router.get('/contact', async (req, res) => { console.error('Contact page error:', error); res.status(500).render('error', { title: 'Error', + settings: {}, message: 'Something went wrong' }); } diff --git a/routes/media.js b/routes/media.js index d5b7100..dd066ec 100644 --- a/routes/media.js +++ b/routes/media.js @@ -280,50 +280,210 @@ router.delete('/:filename', requireAuth, async (req, res) => { } }); -// List uploaded images +// List uploaded images with advanced filtering and search router.get('/list', requireAuth, async (req, res) => { try { const uploadPath = path.join(__dirname, '../public/uploads'); + + // Create uploads directory if it doesn't exist + try { + await fs.mkdir(uploadPath, { recursive: true }); + } catch (mkdirError) { + // Directory might already exist, continue + } + const page = parseInt(req.query.page) || 1; - const limit = parseInt(req.query.limit) || 20; + const limit = parseInt(req.query.limit) || 24; + const search = req.query.search?.toLowerCase() || ''; + const sortBy = req.query.sortBy || 'date'; // date, name, size + const sortOrder = req.query.sortOrder || 'desc'; // asc, desc + const fileType = req.query.fileType || 'all'; // all, image, video, document - const files = await fs.readdir(uploadPath); - const imageFiles = files.filter(file => - /\.(jpg|jpeg|png|gif|webp)$/i.test(file) - ); + let files; + try { + files = await fs.readdir(uploadPath); + } catch (readdirError) { + if (readdirError.code === 'ENOENT') { + return res.json({ + success: true, + images: [], + pagination: { + current: 1, + total: 0, + limit, + totalItems: 0, + hasNext: false, + hasPrev: false + }, + filters: { + search: '', + sortBy: 'date', + sortOrder: 'desc', + fileType: 'all' + } + }); + } + throw readdirError; + } + + // Filter by file type + let filteredFiles = files; + if (fileType !== 'all') { + const typePatterns = { + image: /\.(jpg|jpeg|png|gif|webp|svg|bmp|tiff?)$/i, + video: /\.(mp4|webm|avi|mov|mkv|wmv|flv)$/i, + document: /\.(pdf|doc|docx|txt|rtf|odt)$/i + }; + + const pattern = typePatterns[fileType]; + if (pattern) { + filteredFiles = files.filter(file => pattern.test(file)); + } + } else { + // Only show supported media files + filteredFiles = files.filter(file => + /\.(jpg|jpeg|png|gif|webp|svg|bmp|tiff?|mp4|webm|avi|mov|mkv|pdf|doc|docx)$/i.test(file) + ); + } - const total = imageFiles.length; - const totalPages = Math.ceil(total / limit); - const start = (page - 1) * limit; - const end = start + limit; - - const paginatedFiles = imageFiles.slice(start, end); - - const imagesWithStats = await Promise.all( - paginatedFiles.map(async (file) => { + // Apply search filter + if (search) { + filteredFiles = filteredFiles.filter(file => + file.toLowerCase().includes(search) + ); + } + + // Get file stats and create file objects + const filesWithStats = await Promise.all( + filteredFiles.map(async (file) => { try { const filePath = path.join(uploadPath, file); const stats = await fs.stat(filePath); - - return { - filename: file, - url: `/uploads/${file}`, - size: stats.size, - modified: stats.mtime, - isImage: true - }; + return { file, stats, filePath }; } catch (error) { - console.error(`Error getting stats for ${file}:`, error); return null; } }) ); - const validImages = imagesWithStats.filter(img => img !== null); + const validFiles = filesWithStats.filter(item => item !== null); + + // Sort files + validFiles.sort((a, b) => { + let aValue, bValue; + + switch (sortBy) { + case 'name': + aValue = a.file.toLowerCase(); + bValue = b.file.toLowerCase(); + break; + case 'size': + aValue = a.stats.size; + bValue = b.stats.size; + break; + case 'date': + default: + aValue = a.stats.mtime; + bValue = b.stats.mtime; + break; + } + + if (sortOrder === 'asc') { + return aValue > bValue ? 1 : -1; + } else { + return aValue < bValue ? 1 : -1; + } + }); + + const total = validFiles.length; + const totalPages = Math.ceil(total / limit); + const start = (page - 1) * limit; + const end = start + limit; + + const paginatedFiles = validFiles.slice(start, end); + + const filesWithDetails = await Promise.all( + paginatedFiles.map(async ({ file, stats, filePath }) => { + try { + const ext = path.extname(file).toLowerCase(); + let fileDetails = { + filename: file, + url: `/uploads/${file}`, + size: stats.size, + uploadedAt: stats.birthtime || stats.mtime, + modifiedAt: stats.mtime, + extension: ext, + isImage: false, + isVideo: false, + isDocument: false + }; + + // Determine file type and get additional info + if (/\.(jpg|jpeg|png|gif|webp|svg|bmp|tiff?)$/i.test(file)) { + fileDetails.isImage = true; + fileDetails.mimetype = `image/${ext.replace('.', '')}`; + + // Get image dimensions + try { + const metadata = await sharp(filePath).metadata(); + fileDetails.dimensions = { + width: metadata.width, + height: metadata.height + }; + fileDetails.format = metadata.format; + } catch (sharpError) { + console.warn(`Could not get image metadata for ${file}`); + } + } else if (/\.(mp4|webm|avi|mov|mkv|wmv|flv)$/i.test(file)) { + fileDetails.isVideo = true; + fileDetails.mimetype = `video/${ext.replace('.', '')}`; + } else if (/\.(pdf|doc|docx|txt|rtf|odt)$/i.test(file)) { + fileDetails.isDocument = true; + fileDetails.mimetype = `application/${ext.replace('.', '')}`; + } + + // Generate thumbnail for images + if (fileDetails.isImage && !file.includes('-thumbnail.')) { + const thumbnailPath = path.join(uploadPath, `${path.parse(file).name}-thumbnail.webp`); + try { + await fs.access(thumbnailPath); + fileDetails.thumbnail = `/uploads/${path.basename(thumbnailPath)}`; + } catch { + // Thumbnail doesn't exist, create it + try { + await sharp(filePath) + .resize(200, 150, { + fit: 'cover', + withoutEnlargement: false + }) + .webp({ quality: 80 }) + .toFile(thumbnailPath); + fileDetails.thumbnail = `/uploads/${path.basename(thumbnailPath)}`; + } catch (thumbError) { + console.warn(`Could not create thumbnail for ${file}`); + } + } + } + + return fileDetails; + } catch (error) { + console.error(`Error getting details for ${file}:`, error); + return null; + } + }) + ); + + const validMedia = filesWithDetails.filter(item => item !== null); + + // Calculate storage stats + const totalSize = validFiles.reduce((sum, file) => sum + file.stats.size, 0); + const imageCount = validMedia.filter(f => f.isImage).length; + const videoCount = validMedia.filter(f => f.isVideo).length; + const documentCount = validMedia.filter(f => f.isDocument).length; res.json({ success: true, - images: validImages, + files: validMedia, pagination: { current: page, total: totalPages, @@ -331,15 +491,243 @@ router.get('/list', requireAuth, async (req, res) => { totalItems: total, hasNext: page < totalPages, hasPrev: page > 1 + }, + filters: { + search, + sortBy, + sortOrder, + fileType + }, + stats: { + totalFiles: total, + totalSize, + imageCount, + videoCount, + documentCount, + formattedSize: formatFileSize(totalSize) } }); } catch (error) { - console.error('List images error:', error); + console.error('List media error:', error); res.status(500).json({ success: false, - message: 'Error listing images' + message: 'Error listing media files' }); } }); +// Create folder structure +router.post('/folder', requireAuth, async (req, res) => { + try { + const { folderName } = req.body; + + if (!folderName || !folderName.trim()) { + return res.status(400).json({ + success: false, + message: 'Folder name is required' + }); + } + + // Sanitize folder name + const sanitizedName = folderName.trim().replace(/[^a-zA-Z0-9-_]/g, '-'); + const folderPath = path.join(__dirname, '../public/uploads', sanitizedName); + + try { + await fs.mkdir(folderPath, { recursive: true }); + + res.json({ + success: true, + message: 'Folder created successfully', + folderName: sanitizedName, + folderPath: `/uploads/${sanitizedName}` + }); + } catch (error) { + if (error.code === 'EEXIST') { + return res.status(400).json({ + success: false, + message: 'Folder already exists' + }); + } + throw error; + } + } catch (error) { + console.error('Create folder error:', error); + res.status(500).json({ + success: false, + message: 'Error creating folder' + }); + } +}); + +// Get media file info +router.get('/info/:filename', requireAuth, async (req, res) => { + try { + const filename = req.params.filename; + + // Security check + if (filename.includes('..') || filename.includes('/') || filename.includes('\\')) { + return res.status(400).json({ + success: false, + message: 'Invalid filename' + }); + } + + const filePath = path.join(__dirname, '../public/uploads', filename); + + try { + const stats = await fs.stat(filePath); + const ext = path.extname(filename).toLowerCase(); + + let fileInfo = { + filename, + url: `/uploads/${filename}`, + size: stats.size, + formattedSize: formatFileSize(stats.size), + uploadedAt: stats.birthtime || stats.mtime, + modifiedAt: stats.mtime, + extension: ext, + mimetype: getMimeType(ext) + }; + + // Get additional info for images + if (/\.(jpg|jpeg|png|gif|webp|svg|bmp|tiff?)$/i.test(filename)) { + try { + const metadata = await sharp(filePath).metadata(); + fileInfo.dimensions = { + width: metadata.width, + height: metadata.height + }; + fileInfo.format = metadata.format; + fileInfo.hasAlpha = metadata.hasAlpha; + fileInfo.density = metadata.density; + } catch (sharpError) { + console.warn(`Could not get image metadata for ${filename}`); + } + } + + res.json({ + success: true, + fileInfo + }); + } catch (error) { + if (error.code === 'ENOENT') { + return res.status(404).json({ + success: false, + message: 'File not found' + }); + } + throw error; + } + } catch (error) { + console.error('Get file info error:', error); + res.status(500).json({ + success: false, + message: 'Error getting file information' + }); + } +}); + +// Resize image +router.post('/resize/:filename', requireAuth, async (req, res) => { + try { + const filename = req.params.filename; + const { width, height, quality = 85 } = req.body; + + if (!width && !height) { + return res.status(400).json({ + success: false, + message: 'Width or height must be specified' + }); + } + + // Security check + if (filename.includes('..') || filename.includes('/') || filename.includes('\\')) { + return res.status(400).json({ + success: false, + message: 'Invalid filename' + }); + } + + const originalPath = path.join(__dirname, '../public/uploads', filename); + const nameWithoutExt = path.parse(filename).name; + const resizedPath = path.join( + path.dirname(originalPath), + `${nameWithoutExt}-${width || 'auto'}x${height || 'auto'}.webp` + ); + + try { + let sharpInstance = sharp(originalPath); + + if (width && height) { + sharpInstance = sharpInstance.resize(parseInt(width), parseInt(height), { + fit: 'cover' + }); + } else if (width) { + sharpInstance = sharpInstance.resize(parseInt(width)); + } else { + sharpInstance = sharpInstance.resize(null, parseInt(height)); + } + + await sharpInstance + .webp({ quality: parseInt(quality) }) + .toFile(resizedPath); + + res.json({ + success: true, + message: 'Image resized successfully', + originalFile: filename, + resizedFile: path.basename(resizedPath), + resizedUrl: `/uploads/${path.basename(resizedPath)}` + }); + } catch (error) { + if (error.code === 'ENOENT') { + return res.status(404).json({ + success: false, + message: 'Original file not found' + }); + } + throw error; + } + } catch (error) { + console.error('Resize image error:', error); + res.status(500).json({ + success: false, + message: 'Error resizing image' + }); + } +}); + +// Utility functions +function formatFileSize(bytes) { + if (bytes === 0) return '0 Bytes'; + const k = 1024; + const sizes = ['Bytes', 'KB', 'MB', 'GB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; +} + +function getMimeType(ext) { + const mimeTypes = { + '.jpg': 'image/jpeg', + '.jpeg': 'image/jpeg', + '.png': 'image/png', + '.gif': 'image/gif', + '.webp': 'image/webp', + '.svg': 'image/svg+xml', + '.bmp': 'image/bmp', + '.tiff': 'image/tiff', + '.tif': 'image/tiff', + '.mp4': 'video/mp4', + '.webm': 'video/webm', + '.avi': 'video/x-msvideo', + '.mov': 'video/quicktime', + '.mkv': 'video/x-matroska', + '.pdf': 'application/pdf', + '.doc': 'application/msword', + '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' + }; + + return mimeTypes[ext.toLowerCase()] || 'application/octet-stream'; +} + module.exports = router; \ No newline at end of file diff --git a/routes/portfolio.js b/routes/portfolio.js index 0164537..7c637ae 100644 --- a/routes/portfolio.js +++ b/routes/portfolio.js @@ -1,6 +1,7 @@ const express = require('express'); const router = express.Router(); -const Portfolio = require('../models/Portfolio'); +const { Portfolio } = require('../models'); +const { Op } = require('sequelize'); // Get all portfolio items router.get('/', async (req, res) => { @@ -13,28 +14,37 @@ router.get('/', async (req, res) => { const featured = req.query.featured; // Build query - let query = { isPublished: true }; + let whereClause = { isPublished: true }; if (category && category !== 'all') { - query.category = category; + whereClause.category = category; } if (featured === 'true') { - query.featured = true; + whereClause.featured = true; } if (search) { - query.$text = { $search: search }; + whereClause = { + ...whereClause, + [Op.or]: [ + { title: { [Op.iLike]: `%${search}%` } }, + { description: { [Op.iLike]: `%${search}%` } }, + { shortDescription: { [Op.iLike]: `%${search}%` } } + ] + }; } // Get portfolio items const [portfolio, total] = await Promise.all([ - Portfolio.find(query) - .sort({ featured: -1, publishedAt: -1 }) - .skip(skip) - .limit(limit) - .select('title shortDescription category technologies images status publishedAt viewCount'), - Portfolio.countDocuments(query) + Portfolio.findAll({ + where: whereClause, + order: [['featured', 'DESC'], ['publishedAt', 'DESC']], + offset: skip, + limit: limit, + attributes: ['id', 'title', 'shortDescription', 'category', 'technologies', 'images', 'status', 'publishedAt', 'viewCount'] + }), + Portfolio.count({ where: whereClause }) ]); const totalPages = Math.ceil(total / limit); @@ -63,7 +73,7 @@ router.get('/', async (req, res) => { // Get single portfolio item router.get('/:id', async (req, res) => { try { - const portfolio = await Portfolio.findById(req.params.id); + const portfolio = await Portfolio.findByPk(req.params.id); if (!portfolio || !portfolio.isPublished) { return res.status(404).json({ @@ -77,13 +87,15 @@ router.get('/:id', async (req, res) => { await portfolio.save(); // Get related projects - const relatedProjects = await Portfolio.find({ - _id: { $ne: portfolio._id }, - category: portfolio.category, - isPublished: true - }) - .select('title shortDescription images') - .limit(4); + const relatedProjects = await Portfolio.findAll({ + where: { + id: { [Op.ne]: portfolio.id }, + category: portfolio.category, + isPublished: true + }, + attributes: ['id', 'title', 'shortDescription', 'images'], + limit: 3 + }); res.json({ success: true, @@ -131,7 +143,7 @@ router.get('/meta/categories', async (req, res) => { // Like portfolio item router.post('/:id/like', async (req, res) => { try { - const portfolio = await Portfolio.findById(req.params.id); + const portfolio = await Portfolio.findByPk(req.params.id); if (!portfolio || !portfolio.isPublished) { return res.status(404).json({ @@ -162,21 +174,22 @@ router.get('/search/:term', async (req, res) => { const searchTerm = req.params.term; const limit = parseInt(req.query.limit) || 10; - const portfolio = await Portfolio.find({ - $and: [ - { isPublished: true }, - { - $or: [ - { title: { $regex: searchTerm, $options: 'i' } }, - { description: { $regex: searchTerm, $options: 'i' } }, - { technologies: { $in: [new RegExp(searchTerm, 'i')] } } - ] - } - ] - }) - .select('title shortDescription category images') - .sort({ featured: -1, publishedAt: -1 }) - .limit(limit); + const portfolio = await Portfolio.findAll({ + where: { + [Op.and]: [ + { isPublished: true }, + { + [Op.or]: [ + { title: { [Op.iLike]: `%${searchTerm}%` } }, + { description: { [Op.iLike]: `%${searchTerm}%` } }, + { technologies: { [Op.contains]: [searchTerm] } } + ] + } + ] + }, + attributes: ['id', 'title', 'shortDescription', 'images', 'category'], + limit: limit + }); res.json({ success: true, diff --git a/routes/services.js b/routes/services.js index d6ae97c..601fa05 100644 --- a/routes/services.js +++ b/routes/services.js @@ -1,7 +1,7 @@ const express = require('express'); const router = express.Router(); -const Service = require('../models/Service'); -const Portfolio = require('../models/Portfolio'); +const { Service, Portfolio } = require('../models'); +const { Op } = require('sequelize'); // Get all services router.get('/', async (req, res) => { @@ -9,19 +9,20 @@ router.get('/', async (req, res) => { const category = req.query.category; const featured = req.query.featured; - let query = { isActive: true }; + let whereClause = { isActive: true }; if (category && category !== 'all') { - query.category = category; + whereClause.category = category; } if (featured === 'true') { - query.featured = true; + whereClause.featured = true; } - const services = await Service.find(query) - .populate('portfolio', 'title images') - .sort({ featured: -1, order: 1 }); + const services = await Service.findAll({ + where: whereClause, + order: [['featured', 'DESC'], ['order', 'ASC']] + }); res.json({ success: true, @@ -39,8 +40,7 @@ router.get('/', async (req, res) => { // Get single service router.get('/:id', async (req, res) => { try { - const service = await Service.findById(req.params.id) - .populate('portfolio', 'title shortDescription images category'); + const service = await Service.findByPk(req.params.id); if (!service || !service.isActive) { return res.status(404).json({ @@ -50,13 +50,14 @@ router.get('/:id', async (req, res) => { } // Get related services - const relatedServices = await Service.find({ - _id: { $ne: service._id }, - category: service.category, - isActive: true - }) - .select('name shortDescription icon pricing') - .limit(3); + const relatedServices = await Service.findAll({ + where: { + id: { [Op.ne]: service.id }, + category: service.category, + isActive: true + }, + limit: 3 + }); res.json({ success: true, @@ -107,21 +108,21 @@ router.get('/search/:term', async (req, res) => { const searchTerm = req.params.term; const limit = parseInt(req.query.limit) || 10; - const services = await Service.find({ - $and: [ - { isActive: true }, - { - $or: [ - { name: { $regex: searchTerm, $options: 'i' } }, - { description: { $regex: searchTerm, $options: 'i' } }, - { tags: { $in: [new RegExp(searchTerm, 'i')] } } - ] - } - ] - }) - .select('name shortDescription icon pricing category') - .sort({ featured: -1, order: 1 }) - .limit(limit); + const services = await Service.findAll({ + where: { + [Op.and]: [ + { isActive: true }, + { + [Op.or]: [ + { name: { [Op.iLike]: `%${searchTerm}%` } }, + { description: { [Op.iLike]: `%${searchTerm}%` } }, + { tags: { [Op.contains]: [searchTerm] } } + ] + } + ] + }, + limit: limit + }); res.json({ success: true, diff --git a/scripts/dev.js b/scripts/dev.js index 0a9d859..3c1a5fc 100644 --- a/scripts/dev.js +++ b/scripts/dev.js @@ -29,7 +29,7 @@ function startDevelopmentServer() { console.log(''); const nodemonArgs = [ - '--script', NODEMON_CONFIG.script, + NODEMON_CONFIG.script, '--ext', NODEMON_CONFIG.ext, '--ignore', NODEMON_CONFIG.ignore.join(','), '--watch', NODEMON_CONFIG.watch.join(','), diff --git a/scripts/init-db.js b/scripts/init-db.js index 5c4d272..6ee7fc9 100644 --- a/scripts/init-db.js +++ b/scripts/init-db.js @@ -2,32 +2,29 @@ /** * Database initialization script for SmartSolTech - * Creates initial admin user and sample data + * Creates initial admin user and sample data for PostgreSQL */ -const mongoose = require('mongoose'); -const bcrypt = require('bcryptjs'); +const { sequelize } = require('../config/database'); require('dotenv').config(); // Import models -const User = require('../models/User'); -const Service = require('../models/Service'); -const Portfolio = require('../models/Portfolio'); -const SiteSettings = require('../models/SiteSettings'); +const { User, Service, Portfolio, SiteSettings } = require('../models'); // Configuration -const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://localhost:27017/smartsoltech'; const ADMIN_EMAIL = process.env.ADMIN_EMAIL || 'admin@smartsoltech.kr'; const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD || 'admin123456'; async function initializeDatabase() { try { - console.log('🔄 Connecting to MongoDB...'); - await mongoose.connect(MONGODB_URI, { - useNewUrlParser: true, - useUnifiedTopology: true, - }); - console.log('✅ Connected to MongoDB'); + console.log('🔄 Connecting to PostgreSQL...'); + await sequelize.authenticate(); + console.log('✅ Connected to PostgreSQL'); + + // Sync database (create tables) + console.log('🔄 Syncing database schema...'); + await sequelize.sync({ force: false }); + console.log('✅ Database schema synchronized'); // Create admin user await createAdminUser(); @@ -50,7 +47,7 @@ async function initializeDatabase() { console.error('❌ Database initialization failed:', error); process.exit(1); } finally { - await mongoose.connection.close(); + await sequelize.close(); console.log('🔌 Database connection closed'); process.exit(0); } @@ -58,14 +55,16 @@ async function initializeDatabase() { async function createAdminUser() { try { - const existingAdmin = await User.findOne({ email: ADMIN_EMAIL }); + const existingAdmin = await User.findOne({ + where: { email: ADMIN_EMAIL } + }); if (existingAdmin) { console.log('👤 Admin user already exists, skipping...'); return; } - const adminUser = new User({ + const adminUser = await User.create({ name: 'Administrator', email: ADMIN_EMAIL, password: ADMIN_PASSWORD, @@ -73,7 +72,6 @@ async function createAdminUser() { isActive: true }); - await adminUser.save(); console.log('✅ Admin user created successfully'); } catch (error) { console.error('❌ Error creating admin user:', error); @@ -83,7 +81,7 @@ async function createAdminUser() { async function createSampleServices() { try { - const existingServices = await Service.countDocuments(); + const existingServices = await Service.count(); if (existingServices > 0) { console.log('🛠️ Services already exist, skipping...'); @@ -243,7 +241,7 @@ async function createSampleServices() { } ]; - await Service.insertMany(services); + await Service.bulkCreate(services); console.log('✅ Sample services created successfully'); } catch (error) { console.error('❌ Error creating sample services:', error); @@ -253,7 +251,7 @@ async function createSampleServices() { async function createSamplePortfolio() { try { - const existingPortfolio = await Portfolio.countDocuments(); + const existingPortfolio = await Portfolio.count(); if (existingPortfolio > 0) { console.log('🎨 Portfolio items already exist, skipping...'); @@ -416,7 +414,7 @@ async function createSamplePortfolio() { } ]; - await Portfolio.insertMany(portfolioItems); + await Portfolio.bulkCreate(portfolioItems); console.log('✅ Sample portfolio items created successfully'); } catch (error) { console.error('❌ Error creating sample portfolio:', error); @@ -433,7 +431,7 @@ async function createSiteSettings() { return; } - const settings = new SiteSettings({ + const settings = await SiteSettings.create({ siteName: 'SmartSolTech', siteDescription: '혁신적인 기술 솔루션으로 비즈니스의 성장을 지원합니다', logo: '/images/logo.png', @@ -476,7 +474,6 @@ async function createSiteSettings() { } }); - await settings.save(); console.log('✅ Site settings created successfully'); } catch (error) { console.error('❌ Error creating site settings:', error); diff --git a/scripts/sync-locales.js b/scripts/sync-locales.js new file mode 100644 index 0000000..e3f4eee --- /dev/null +++ b/scripts/sync-locales.js @@ -0,0 +1,115 @@ +const fs = require('fs'); +const path = require('path'); + +const localesDir = path.join(__dirname, '..', 'locales'); +const files = fs.readdirSync(localesDir).filter(f => f.endsWith('.json')); + +// priority order for falling back when filling missing translations +const priority = ['en.json', 'ko.json', 'ru.json', 'kk.json']; + +function readJSON(file) { + try { + return JSON.parse(fs.readFileSync(path.join(localesDir, file), 'utf8')); + } catch (e) { + console.error('Failed to read', file, e.message); + return {}; + } +} + +function flatten(obj, prefix = '') { + const res = {}; + for (const k of Object.keys(obj)) { + const val = obj[k]; + const key = prefix ? `${prefix}.${k}` : k; + if (val && typeof val === 'object' && !Array.isArray(val)) { + Object.assign(res, flatten(val, key)); + } else { + res[key] = val; + } + } + return res; +} + +function unflatten(flat) { + const res = {}; + for (const flatKey of Object.keys(flat)) { + const parts = flatKey.split('.'); + let cur = res; + for (let i = 0; i < parts.length; i++) { + const p = parts[i]; + if (i === parts.length - 1) { + cur[p] = flat[flatKey]; + } else { + cur[p] = cur[p] || {}; + cur = cur[p]; + } + } + } + return res; +} + +// load all +const data = {}; +for (const f of files) { + data[f] = readJSON(f); +} + +// use en.json as canonical key set if exists, else merge all keys +let canonical = {}; +if (files.includes('en.json')) { + canonical = flatten(data['en.json']); +} else { + for (const f of files) { + canonical = Object.assign(canonical, flatten(data[f])); + } +} + +function getFallback(key, currentFile) { + for (const p of priority) { + if (p === currentFile) continue; + if (!files.includes(p)) continue; + const flat = flatten(data[p]); + if (flat[key] && typeof flat[key] === 'string' && flat[key].trim()) return flat[key]; + } + return null; +} + +let report = {}; +for (const f of files) { + const flat = flatten(data[f]); + report[f] = { added: 0, marked: 0 }; + const out = {}; + for (const key of Object.keys(canonical)) { + if (flat.hasOwnProperty(key)) { + out[key] = flat[key]; + } else { + const fb = getFallback(key, f); + if (fb) { + out[key] = fb; + } else { + out[key] = '[TRANSLATE] ' + canonical[key]; + report[f].marked++; + } + report[f].added++; + } + } + // also keep any extra keys present in this file but not in canonical + for (const key of Object.keys(flat)) { + if (!out.hasOwnProperty(key)) { + out[key] = flat[key]; + } + } + // write back + const nested = unflatten(out); + try { + fs.writeFileSync(path.join(localesDir, f), JSON.stringify(nested, null, 2), 'utf8'); + } catch (e) { + console.error('Failed to write', f, e.message); + } +} + +console.log('Locale sync report:'); +for (const f of files) { + console.log(`- ${f}: added ${report[f].added} keys, ${report[f].marked} marked for translation`); +} +console.log('Done. Review files in locales/ and replace [TRANSLATE] placeholders with correct translations.'); diff --git a/server.js b/server.js index d17df23..4c12c09 100644 --- a/server.js +++ b/server.js @@ -1,27 +1,54 @@ const express = require('express'); -const mongoose = require('mongoose'); +const { sequelize, testConnection } = require('./config/database'); const session = require('express-session'); -const MongoStore = require('connect-mongo'); +const SequelizeStore = require('connect-session-sequelize')(session.Store); const path = require('path'); const helmet = require('helmet'); const compression = require('compression'); const cors = require('cors'); const morgan = require('morgan'); const rateLimit = require('express-rate-limit'); +const i18n = require('i18n'); require('dotenv').config(); const app = express(); +// Настройка i18n +i18n.configure({ + locales: ['ko', 'en', 'ru', 'kk'], + defaultLocale: 'ru', + directory: path.join(__dirname, 'locales'), + objectNotation: true, + updateFiles: false, + syncFiles: false +}); + +// i18n middleware +app.use(i18n.init); + +// Middleware для передачи переменных в шаблоны +app.use((req, res, next) => { + const currentLang = req.session?.language || req.getLocale() || 'ru'; + req.setLocale(currentLang); + + res.locals.locale = currentLang; + res.locals.__ = res.__; + res.locals.theme = req.session?.theme || 'light'; + res.locals.currentLanguage = currentLang; + res.locals.currentPage = req.path.split('/')[1] || 'home'; + next(); +}); + // Security middleware app.use(helmet({ contentSecurityPolicy: { directives: { defaultSrc: ["'self'"], - styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com", "https://cdnjs.cloudflare.com"], - fontSrc: ["'self'", "https://fonts.gstatic.com"], - scriptSrc: ["'self'", "'unsafe-inline'", "https://cdnjs.cloudflare.com"], + styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com", "https://cdnjs.cloudflare.com", "https://cdn.jsdelivr.net", "https://unpkg.com", "https://cdn.tailwindcss.com"], + fontSrc: ["'self'", "https://fonts.gstatic.com", "https://cdnjs.cloudflare.com"], + scriptSrc: ["'self'", "'unsafe-inline'", "https://cdnjs.cloudflare.com", "https://unpkg.com", "https://cdn.tailwindcss.com"], imgSrc: ["'self'", "data:", "https:"], - connectSrc: ["'self'", "ws:", "wss:"] + connectSrc: ["'self'", "ws:", "wss:", "https://cdnjs.cloudflare.com", "https://cdn.jsdelivr.net", "https://unpkg.com", "https://fonts.googleapis.com", "https://fonts.gstatic.com", "https://cdn.tailwindcss.com"] } } })); @@ -48,23 +75,30 @@ app.use('/uploads', express.static(path.join(__dirname, 'public/uploads'))); app.set('view engine', 'ejs'); app.set('views', path.join(__dirname, 'views')); -// Database connection -mongoose.connect(process.env.MONGODB_URI || 'mongodb://localhost:27017/smartsoltech', { - useNewUrlParser: true, - useUnifiedTopology: true, -}) -.then(() => console.log('✓ MongoDB connected')) -.catch(err => console.error('✗ MongoDB connection error:', err)); +// Layout engine +const expressLayouts = require('express-ejs-layouts'); +app.use(expressLayouts); +app.set('layout', 'layout'); // Default layout for main site +app.set('layout extractScripts', true); +app.set('layout extractStyles', true); + +// Database connection and testing +testConnection(); + +// Session store configuration +const sessionStore = new SequelizeStore({ + db: sequelize, + tableName: 'sessions', + checkExpirationInterval: 15 * 60 * 1000, // 15 minutes + expiration: 7 * 24 * 60 * 60 * 1000 // 7 days +}); // Session configuration app.use(session({ secret: process.env.SESSION_SECRET || 'your-secret-key', resave: false, saveUninitialized: false, - store: MongoStore.create({ - mongoUrl: process.env.MONGODB_URI || 'mongodb://localhost:27017/smartsoltech', - touchAfter: 24 * 3600 // lazy session update - }), + store: sessionStore, cookie: { secure: process.env.NODE_ENV === 'production', httpOnly: true, @@ -80,8 +114,32 @@ app.use('/api/services', require('./routes/services')); app.use('/api/calculator', require('./routes/calculator')); app.use('/api/contact', require('./routes/contact')); app.use('/api/media', require('./routes/media')); +app.use('/api/admin', require('./routes/api/admin')); app.use('/admin', require('./routes/admin')); +// Language switching routes +app.get('/lang/:language', (req, res) => { + const { language } = req.params; + const supportedLanguages = ['ko', 'en', 'ru', 'kk']; + + if (supportedLanguages.includes(language)) { + req.setLocale(language); + req.session.language = language; + } + + const referer = req.get('Referer') || '/'; + res.redirect(referer); +}); + +// Theme switching routes +app.get('/theme/:theme', (req, res) => { + const { theme } = req.params; + if (['light', 'dark'].includes(theme)) { + req.session.theme = theme; + } + res.json({ success: true, theme: req.session.theme }); +}); + // PWA Service Worker app.get('/sw.js', (req, res) => { res.sendFile(path.join(__dirname, 'public', 'sw.js')); @@ -95,27 +153,47 @@ app.get('/manifest.json', (req, res) => { // Error handling middleware app.use((err, req, res, next) => { console.error(err.stack); - res.status(500).json({ - success: false, + res.status(500).render('error', { + title: 'Error', + settings: {}, message: process.env.NODE_ENV === 'production' ? 'Something went wrong!' - : err.message + : err.message, + currentPage: 'error' }); }); // 404 handler app.use((req, res) => { - res.status(404).render('404', { - title: '404 - Страница не найдена', - message: 'Запрашиваемая страница не найдена' + res.status(404).render('error', { + title: '404 - 페이지를 찾을 수 없습니다', + settings: {}, + message: '요청하신 페이지를 찾을 수 없습니다', + currentPage: 'error' }); }); const PORT = process.env.PORT || 3000; -app.listen(PORT, () => { - console.log(`🚀 Server running on port ${PORT}`); - console.log(`🌐 Visit: http://localhost:${PORT}`); -}); +// Sync database and start server +async function startServer() { + try { + // Sync all models with database + await sequelize.sync({ force: false }); + console.log('✓ Database synchronized'); + + // Create session table + await sessionStore.sync(); + console.log('✓ Session store synchronized'); + + app.listen(PORT, () => { + console.log(`🚀 Server running on port ${PORT}`); + console.log(`🌐 Visit: http://localhost:${PORT}`); + }); + } catch (error) { + console.error('✗ Failed to start server:', error); + process.exit(1); + } +} -module.exports = app; \ No newline at end of file +startServer(); \ No newline at end of file diff --git a/services/telegram.js b/services/telegram.js new file mode 100644 index 0000000..1b5cbac --- /dev/null +++ b/services/telegram.js @@ -0,0 +1,374 @@ +const axios = require('axios'); + +class TelegramService { + constructor() { + this.botToken = process.env.TELEGRAM_BOT_TOKEN; + this.chatId = process.env.TELEGRAM_CHAT_ID; + this.baseUrl = `https://api.telegram.org/bot${this.botToken}`; + this.isEnabled = !!(this.botToken && this.chatId); + this.chats = new Map(); // Store chat information + this.botInfo = null; // Store bot information + } + + // Update bot token and reinitialize + updateBotToken(newToken) { + this.botToken = newToken; + this.baseUrl = `https://api.telegram.org/bot${this.botToken}`; + this.isEnabled = !!(this.botToken && this.chatId); + this.botInfo = null; // Reset bot info + return this.testConnection(); + } + + // Update default chat ID + updateChatId(newChatId) { + this.chatId = newChatId; + this.isEnabled = !!(this.botToken && this.chatId); + } + + // Get bot information + async getBotInfo() { + if (this.botInfo) return { success: true, bot: this.botInfo }; + + if (!this.botToken) { + return { success: false, message: 'Bot token not configured' }; + } + + try { + const response = await axios.get(`${this.baseUrl}/getMe`); + this.botInfo = response.data.result; + return { success: true, bot: this.botInfo }; + } catch (error) { + console.error('Get bot info error:', error.response?.data || error.message); + return { success: false, error: error.message }; + } + } + + // Get updates (for getting chat IDs and managing chats) + async getUpdates() { + if (!this.botToken) { + return { success: false, message: 'Bot token not configured' }; + } + + try { + const response = await axios.get(`${this.baseUrl}/getUpdates`); + const updates = response.data.result; + + // Extract unique chats + const chats = new Map(); + updates.forEach(update => { + if (update.message) { + const chat = update.message.chat; + chats.set(chat.id, { + id: chat.id, + type: chat.type, + title: chat.title || `${chat.first_name || ''} ${chat.last_name || ''}`.trim(), + username: chat.username || null, + first_name: chat.first_name || null, + last_name: chat.last_name || null, + description: chat.description || null, + invite_link: chat.invite_link || null, + pinned_message: chat.pinned_message || null, + permissions: chat.permissions || null, + slow_mode_delay: chat.slow_mode_delay || null + }); + } + }); + + // Update internal chat storage + chats.forEach((chat, id) => { + this.chats.set(id, chat); + }); + + return { + success: true, + updates, + chats: Array.from(chats.values()), + totalUpdates: updates.length + }; + } catch (error) { + console.error('Get updates error:', error.response?.data || error.message); + return { success: false, error: error.message }; + } + } + + // Get chat information + async getChat(chatId) { + if (!this.botToken) { + return { success: false, message: 'Bot token not configured' }; + } + + try { + const response = await axios.get(`${this.baseUrl}/getChat`, { + params: { chat_id: chatId } + }); + const chat = response.data.result; + + // Store in local cache + this.chats.set(chatId, chat); + + return { success: true, chat }; + } catch (error) { + console.error('Get chat error:', error.response?.data || error.message); + return { success: false, error: error.message }; + } + } + + // Get chat administrators + async getChatAdministrators(chatId) { + if (!this.botToken) { + return { success: false, message: 'Bot token not configured' }; + } + + try { + const response = await axios.get(`${this.baseUrl}/getChatAdministrators`, { + params: { chat_id: chatId } + }); + return { success: true, administrators: response.data.result }; + } catch (error) { + console.error('Get chat administrators error:', error.response?.data || error.message); + return { success: false, error: error.message }; + } + } + + // Get available chats (cached) + getAvailableChats() { + return Array.from(this.chats.values()).map(chat => ({ + id: chat.id, + title: chat.title || chat.username || `${chat.first_name || ''} ${chat.last_name || ''}`.trim(), + type: chat.type, + username: chat.username + })); + } + + async sendMessage(text, options = {}) { + const chatId = options.chat_id || this.chatId; + + if (!this.botToken) { + console.warn('Telegram bot token is not configured'); + return { success: false, message: 'Telegram bot token not configured' }; + } + + if (!chatId) { + console.warn('Telegram chat ID is not specified'); + return { success: false, message: 'Telegram chat ID not specified' }; + } + + try { + const response = await axios.post(`${this.baseUrl}/sendMessage`, { + chat_id: chatId, + text: text, + parse_mode: 'HTML', + disable_web_page_preview: false, + disable_notification: false, + ...options + }); + + return { success: true, data: response.data }; + } catch (error) { + console.error('Telegram send message error:', error.response?.data || error.message); + return { success: false, error: error.message }; + } + } + + // Send message to multiple chats + async sendBroadcastMessage(text, chatIds = [], options = {}) { + if (!this.botToken) { + return { success: false, message: 'Telegram bot token not configured' }; + } + + if (!chatIds || chatIds.length === 0) { + return { success: false, message: 'No chat IDs specified' }; + } + + const results = []; + const errors = []; + + for (const chatId of chatIds) { + try { + const result = await this.sendMessage(text, { + ...options, + chat_id: chatId + }); + + if (result.success) { + results.push({ chatId, success: true, messageId: result.data.result.message_id }); + } else { + errors.push({ chatId, error: result.error || result.message }); + } + + // Add delay between messages to avoid rate limiting + await new Promise(resolve => setTimeout(resolve, 100)); + } catch (error) { + errors.push({ chatId, error: error.message }); + } + } + + return { + success: errors.length === 0, + results, + errors, + totalSent: results.length, + totalFailed: errors.length + }; + } + + // Send custom message with advanced options + async sendCustomMessage({ + text, + chatIds = [], + parseMode = 'HTML', + disableWebPagePreview = false, + disableNotification = false, + replyMarkup = null + }) { + const targetChats = chatIds.length > 0 ? chatIds : [this.chatId]; + + return await this.sendBroadcastMessage(text, targetChats, { + parse_mode: parseMode, + disable_web_page_preview: disableWebPagePreview, + disable_notification: disableNotification, + reply_markup: replyMarkup + }); + } + + async sendContactNotification(contact) { + const message = this.formatContactMessage(contact); + return await this.sendMessage(message); + } + + async sendNewContactAlert(contact) { + const message = `🔔 Новый запрос с сайта!\n\n` + + `👤 Клиент: ${contact.name}\n` + + `📧 Email: ${contact.email}\n` + + `📱 Телефон: ${contact.phone || 'Не указан'}\n` + + `💼 Услуга: ${contact.serviceInterest || 'Общий запрос'}\n` + + `💰 Бюджет: ${contact.budget || 'Не указан'}\n` + + `⏱️ Сроки: ${contact.timeline || 'Не указаны'}\n\n` + + `💬 Сообщение:\n${contact.message}\n\n` + + `🕐 Время: ${new Date(contact.createdAt).toLocaleString('ru-RU')}\n\n` + + `🔗 Открыть в админ-панели`; + + return await this.sendMessage(message); + } + + async sendPortfolioNotification(portfolio) { + const message = `📁 Новый проект добавлен в портфолио\n\n` + + `🏷️ Название: ${portfolio.title}\n` + + `📂 Категория: ${portfolio.category}\n` + + `👤 Клиент: ${portfolio.clientName || 'Не указан'}\n` + + `🌐 URL: ${portfolio.projectUrl || 'Не указан'}\n` + + `⭐ Рекомендуемый: ${portfolio.featured ? 'Да' : 'Нет'}\n` + + `📅 Время: ${new Date(portfolio.createdAt).toLocaleString('ru-RU')}\n\n` + + `🔗 Посмотреть проект`; + + return await this.sendMessage(message); + } + + async sendServiceNotification(service) { + const message = `⚙️ Новая услуга добавлена\n\n` + + `🏷️ Название: ${service.name}\n` + + `📂 Категория: ${service.category}\n` + + `💰 Стоимость: ${service.pricing?.basePrice ? `от $${service.pricing.basePrice}` : 'По запросу'}\n` + + `⏱️ Время выполнения: ${service.estimatedTime || 'Не указано'}\n` + + `⭐ Рекомендуемая: ${service.featured ? 'Да' : 'Нет'}\n` + + `📅 Время: ${new Date(service.createdAt).toLocaleString('ru-RU')}\n\n` + + `🔗 Посмотреть услуги`; + + return await this.sendMessage(message); + } + + async sendCalculatorQuote(calculatorData) { + const totalCost = calculatorData.services?.reduce((sum, service) => sum + (service.price || 0), 0) || 0; + + const message = `💰 Новый расчет стоимости\n\n` + + `👤 Клиент: ${calculatorData.name || 'Не указан'}\n` + + `📧 Email: ${calculatorData.email || 'Не указан'}\n` + + `📱 Телефон: ${calculatorData.phone || 'Не указан'}\n\n` + + `🛠️ Выбранные услуги:\n${this.formatServices(calculatorData.services)}\n` + + `💵 Общая стоимость: $${totalCost}\n\n` + + `📅 Время: ${new Date().toLocaleString('ru-RU')}`; + + return await this.sendMessage(message); + } + + formatContactMessage(contact) { + return `📞 Уведомление о контакте\n\n` + + `👤 Клиент: ${contact.name}\n` + + `📧 Email: ${contact.email}\n` + + `📱 Телефон: ${contact.phone || 'Не указан'}\n` + + `💼 Услуга: ${contact.serviceInterest || 'Общий запрос'}\n` + + `📊 Статус: ${this.getStatusText(contact.status)}\n` + + `⚡ Приоритет: ${this.getPriorityText(contact.priority)}\n\n` + + `💬 Сообщение:\n${contact.message}\n\n` + + `🔗 Открыть в админ-панели`; + } + + formatServices(services) { + if (!services || services.length === 0) return 'Не выбрано'; + + return services.map(service => + `• ${service.name} - $${service.price || 0}` + ).join('\n'); + } + + getStatusText(status) { + const statusMap = { + 'new': '🆕 Новое', + 'in_progress': '⏳ В работе', + 'completed': '✅ Завершено' + }; + return statusMap[status] || status; + } + + getPriorityText(priority) { + const priorityMap = { + 'low': '🟢 Низкий', + 'medium': '🟡 Средний', + 'high': '🔴 Высокий' + }; + return priorityMap[priority] || priority; + } + + // Test connection and update bot info + async testConnection() { + if (!this.botToken) { + return { success: false, message: 'Telegram bot token not configured' }; + } + + try { + const response = await axios.get(`${this.baseUrl}/getMe`); + this.botInfo = response.data.result; + + // Also get updates to discover available chats + await this.getUpdates(); + + return { + success: true, + bot: this.botInfo, + availableChats: this.getAvailableChats() + }; + } catch (error) { + console.error('Telegram connection test error:', error.response?.data || error.message); + return { success: false, error: error.message }; + } + } + + // Webhook setup (for future use) + async setWebhook(webhookUrl) { + if (!this.isEnabled) { + return { success: false, message: 'Telegram bot not configured' }; + } + + try { + const response = await axios.post(`${this.baseUrl}/setWebhook`, { + url: webhookUrl + }); + return { success: true, data: response.data }; + } catch (error) { + console.error('Telegram webhook setup error:', error.response?.data || error.message); + return { success: false, error: error.message }; + } + } +} + +module.exports = new TelegramService(); \ No newline at end of file diff --git a/views/about.ejs b/views/about.ejs index 6954758..8a673aa 100644 --- a/views/about.ejs +++ b/views/about.ejs @@ -18,14 +18,14 @@ <%- include('partials/navigation') %> - -
+ +
-

+

<%- __('about.hero.title') %>

-

+

<%- __('about.hero.subtitle') %>

diff --git a/views/admin/banner-editor.ejs b/views/admin/banner-editor.ejs new file mode 100644 index 0000000..b0853ff --- /dev/null +++ b/views/admin/banner-editor.ejs @@ -0,0 +1,664 @@ + + + + + + Редактор Баннеров - SmartSolTech Admin + + + + + + + + + + + + + + + +
+
+
+
+

+ + SmartSolTech Admin +

+
+
+ + Добро пожаловать, <%= user ? user.name : 'Admin' %>! + + + + Посмотреть сайт + +
+ +
+
+
+
+
+ +
+ + + + +
+
+ +
+

+ + Редактор Баннеров +

+

Создание и редактирование баннеров для сайта

+
+ + +
+
+

Инструменты

+
+ + +
+
+
+ + +
+ +
+
+ +
+
+ + +
+

+ Текущий баннер: Главная страница +

+
+
+
+

Текущий Баннер

+

Нажмите на изображение ниже, чтобы заменить

+
+
+
+
+ +
+ +
+
+
+ + +
+

+ Галерея изображений +

+ + + +
+ +
+ + +
+
+
+
+ + + + + + + + + + + + \ No newline at end of file diff --git a/views/admin/contacts/list.ejs b/views/admin/contacts/list.ejs new file mode 100644 index 0000000..430dffd --- /dev/null +++ b/views/admin/contacts/list.ejs @@ -0,0 +1,117 @@ + +
+
+
+

+ + Управление сообщениями +

+
+ + +
+
+
+ + + + + <% if (pagination && pagination.total > 1) { %> +
+
+ <% if (pagination.hasPrev) { %> + + Предыдущая + + <% } %> + <% if (pagination.hasNext) { %> + + Следующая + + <% } %> +
+
+ <% } %> +
+ + \ No newline at end of file diff --git a/views/admin/contacts/view.ejs b/views/admin/contacts/view.ejs new file mode 100644 index 0000000..f24bee0 --- /dev/null +++ b/views/admin/contacts/view.ejs @@ -0,0 +1,219 @@ + +
+
+
+

+ + Детали сообщения +

+ + + Назад к списку + +
+
+ +
+
+ +
+

Информация о контакте

+ +
+
+
Имя
+
<%= contact.name %>
+
+ + + + <% if (contact.phone) { %> +
+
Телефон
+
+ + <%= contact.phone %> + +
+
+ <% } %> + +
+
Дата создания
+
+ <%= new Date(contact.createdAt).toLocaleString('ru-RU') %> +
+
+
+
+ + +
+

Детали проекта

+ +
+ <% if (contact.serviceInterest) { %> +
+
Интересующая услуга
+
+ + <%= contact.serviceInterest %> + +
+
+ <% } %> + + <% if (contact.budget) { %> +
+
Бюджет
+
<%= contact.budget %>
+
+ <% } %> + + <% if (contact.timeline) { %> +
+
Временные рамки
+
<%= contact.timeline %>
+
+ <% } %> + +
+
Статус
+
+ +
+
+ +
+
Приоритет
+
+ +
+
+
+
+
+ + +
+

Сообщение

+
+

<%= contact.message %>

+
+
+ + +
+ + + + + Ответить по email + + + +
+
+
+ + \ No newline at end of file diff --git a/views/admin/dashboard.ejs b/views/admin/dashboard.ejs new file mode 100644 index 0000000..a72cc3c --- /dev/null +++ b/views/admin/dashboard.ejs @@ -0,0 +1,323 @@ + + + + + + <%= title %> - SmartSolTech Admin + + + + + + + + + + + + + +
+
+
+
+

+ + SmartSolTech Admin +

+
+
+ + Добро пожаловать, <%= user ? user.name : 'Admin' %>! + + + + Посмотреть сайт + +
+ +
+
+
+
+
+ +
+ + + + +
+
+ +
+

+ + Панель управления +

+

Обзор основных показателей сайта

+
+ + +
+ +
+
+
+
+ +
+
+
+
+ Проекты +
+
+ <%= stats.portfolioCount || 0 %> +
+
+
+
+
+ +
+ + +
+
+
+
+ +
+
+
+
+ Услуги +
+
+ <%= stats.servicesCount || 0 %> +
+
+
+
+
+ +
+ + +
+
+
+
+ +
+
+
+
+ Сообщения +
+
+ <%= stats.contactsCount || 0 %> +
+
+
+
+
+ +
+ + +
+
+
+
+ +
+
+
+
+ Пользователи +
+
+ <%= stats.usersCount || 0 %> +
+
+
+
+
+ +
+
+ + +
+ +
+
+

+ Последние проекты +

+
+
+ <% if (recentPortfolio && recentPortfolio.length > 0) { %> +
+ <% recentPortfolio.forEach(function(project) { %> +
+
+ +
+
+

+ <%= project.title %> +

+

+ <%= project.category %> +

+
+
+ + <%= project.status %> + +
+
+ <% }); %> +
+ <% } else { %> +

Нет недавних проектов

+ <% } %> +
+
+ + +
+
+

+ Последние сообщения +

+
+
+ <% if (recentContacts && recentContacts.length > 0) { %> +
+ <% recentContacts.forEach(function(contact) { %> +
+
+ +
+
+

+ <%= contact.name %> +

+

+ <%= contact.email %> +

+
+
+ + <%= contact.status %> + +
+
+ <% }); %> +
+ <% } else { %> +

Нет недавних сообщений

+ <% } %> +
+
+
+ + + +
+
+
+ + + + + \ No newline at end of file diff --git a/views/admin/error.ejs b/views/admin/error.ejs new file mode 100644 index 0000000..d238c31 --- /dev/null +++ b/views/admin/error.ejs @@ -0,0 +1,23 @@ +<%- include('layout', { title: title, user: user }) %> + + \ No newline at end of file diff --git a/views/admin/layout.ejs b/views/admin/layout.ejs new file mode 100644 index 0000000..dab51d1 --- /dev/null +++ b/views/admin/layout.ejs @@ -0,0 +1,104 @@ + + + + + + <%= title %> - SmartSolTech Admin + + + + + + + + + + + + + + + +
+
+
+
+

+ + SmartSolTech Admin +

+
+
+ + Добро пожаловать, <%= user ? user.name : 'Admin' %>! + + + + Посмотреть сайт + +
+ +
+
+
+
+
+ + + + + + + \ No newline at end of file diff --git a/views/admin/login.ejs b/views/admin/login.ejs new file mode 100644 index 0000000..5509b45 --- /dev/null +++ b/views/admin/login.ejs @@ -0,0 +1,81 @@ +о + + + + + Вход в админ панель - SmartSolTech + + + + + + + + + + + + +
+
+
+ +
+

+ Вход в админ панель +

+

+ Войдите в свой аккаунт для управления сайтом +

+
+ +
+ <% if (typeof error !== 'undefined') { %> +
+ + <%= error %> +
+ <% } %> + +
+
+ + +
+
+ + +
+
+ +
+ +
+ + +
+
+ + + + + \ No newline at end of file diff --git a/views/admin/media.ejs b/views/admin/media.ejs new file mode 100644 index 0000000..b27d34e --- /dev/null +++ b/views/admin/media.ejs @@ -0,0 +1,848 @@ + + + + + + Медиа Галерея - SmartSolTech Admin + + + + + + + + + + + + + +
+
+
+
+

+ + SmartSolTech Admin +

+
+
+ + Добро пожаловать, <%= user ? user.name : 'Admin' %>! + + + + Посмотреть сайт + +
+ +
+
+
+
+
+ +
+ + + + +
+
+ +
+
+
+

+ + Медиа Галерея +

+

Управление изображениями и файлами сайта

+
+
+ + +
+
+
+ + + + + + + + +
+
+
+
+ + +
+
+ + +
+
+
+ +
+ + +
+
+
+
+ + +
+
+

Файлы

+
+ Загрузка... +
+ + +
+
+
+ + +
+
+

Загрузка медиа файлов...

+
+ + + + + +
+ +
+ + + + + + +
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/views/admin/portfolio/add.ejs b/views/admin/portfolio/add.ejs new file mode 100644 index 0000000..6032e4d --- /dev/null +++ b/views/admin/portfolio/add.ejs @@ -0,0 +1,776 @@ + +
+
+
+
+
+

+ + Добавить новый проект +

+

Заполните информацию о проекте для добавления в портфолио

+
+
+ + + + Назад к списку + +
+
+
+ +
+ +
+
+
+
1
+ Основная информация +
+
+
+
2
+ Медиа и изображения +
+
+
+
3
+ Публикация +
+
+
+ +
+ +
+

+ + Основная информация о проекте +

+ +
+ +
+ + +

Используйте описательное название, которое четко передает суть проекта

+
+ + +
+ + +
+ Отображается в превью проекта + 0/200 +
+
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+
+
+ + +
+

+ + Описание и технические детали +

+ + +
+ +
+ +

Опишите задачи, решения и результаты проекта

+
+ + +
+ +
+ +
+ +
+
+
+ + + +
+

Популярные технологии:

+
+ <% const popularTechs = ['React', 'Vue.js', 'Node.js', 'Express.js', 'MongoDB', 'PostgreSQL', 'MySQL', 'JavaScript', 'TypeScript', 'HTML5', 'CSS3', 'Tailwind CSS', 'Bootstrap', 'Webpack', 'Docker', 'Git', 'AWS', 'Figma', 'Photoshop']; %> + <% popularTechs.forEach(tech => { %> + + <% }); %> +
+
+
+
+ + +
+

+ + Изображения и медиа контент +

+ +
+ +
+ +
+
+
+ + + +
+
+ + или перетащите файлы сюда +
+

+ PNG, JPG, WEBP до 10MB каждый. Первое изображение будет использоваться как главное. +

+
+
+
+ + + +
+
+ + +
+

+ + Настройки публикации +

+ +
+ +
+
+
+ +
+
+ +

Проект будет виден посетителям сайта

+
+
+ +
+
+ +
+
+ +

Проект будет показан в топе портфолио

+
+
+
+ + +
+
SEO настройки (необязательно)
+
+
+ + +

Рекомендуется до 60 символов

+
+ +
+ + +

Рекомендуется до 160 символов

+
+
+
+
+
+ + +
+
+ +
+ +
+ + Отмена + + + +
+
+ +
+ + + + + + \ No newline at end of file diff --git a/views/admin/portfolio/list.ejs b/views/admin/portfolio/list.ejs new file mode 100644 index 0000000..7c7031a --- /dev/null +++ b/views/admin/portfolio/list.ejs @@ -0,0 +1,358 @@ + +
+
+
+
+

+ + Управление портфолио +

+

+ Всего проектов: <%= portfolio ? portfolio.length : 0 %> +

+
+
+
+ + +
+ + + + Добавить проект + +
+
+
+ +
+
    + <% if (portfolio && portfolio.length > 0) { %> + <% portfolio.forEach(item => { %> +
  • +
    +
    +
    +
    + <% if (item.images && item.images.length > 0) { %> + <%= item.title %> + <% } else { %> +
    + +
    + <% } %> +
    +
    +
    +

    <%= item.title %>

    +
    + <% if (item.featured) { %> + + + Рекомендуемое + + <% } %> + + + <%= item.isPublished ? 'Опубликовано' : 'Черновик' %> + +
    +
    +

    <%= item.shortDescription || 'Описание не указано' %>

    +
    +
    + + <%= item.category.replace('-', ' ') %> +
    +
    + + <%= new Date(item.createdAt).toLocaleDateString('ru-RU') %> +
    + <% if (item.viewCount && item.viewCount > 0) { %> +
    + + <%= item.viewCount %> просмотров +
    + <% } %> + <% if (item.technologies && item.technologies.length > 0) { %> +
    + + <%= item.technologies.slice(0, 2).join(', ') %><%= item.technologies.length > 2 ? '...' : '' %> +
    + <% } %> +
    +
    +
    +
    + <% if (item.isPublished) { %> + + + + <% } %> + + + + + + +
    +
    +
    +
  • + <% }) %> + <% } else { %> +
  • +
    + +

    Проекты не найдены

    + + Добавить первый проект + +
    +
  • + <% } %> +
+
+ + + <% if (pagination && pagination.total > 1) { %> +
+
+ <% if (pagination.hasPrev) { %> + + Предыдущая + + <% } %> + <% if (pagination.hasNext) { %> + + Следующая + + <% } %> +
+
+ <% } %> +
+ + \ No newline at end of file diff --git a/views/admin/services/list.ejs b/views/admin/services/list.ejs new file mode 100644 index 0000000..5d2add4 --- /dev/null +++ b/views/admin/services/list.ejs @@ -0,0 +1,121 @@ + +
+
+
+

+ + Управление услугами +

+ + + Добавить услугу + +
+
+ +
+
    + <% if (services && services.length > 0) { %> + <% services.forEach(service => { %> +
  • +
    +
    +
    +
    + +
    +
    +
    +
    +
    <%= service.name %>
    + <% if (service.featured) { %> + + + Рекомендуемая + + <% } %> + <% if (!service.isActive) { %> + + Неактивна + + <% } %> +
    +
    + <%= service.category %> • + <% if (service.pricing && service.pricing.basePrice) { %> + от $<%= service.pricing.basePrice %> + <% } %> +
    +
    +
    +
    + + + + + + + +
    +
    +
  • + <% }) %> + <% } else { %> +
  • +
    + +

    Услуги не найдены

    + + Добавить первую услугу + +
    +
  • + <% } %> +
+
+ + + <% if (pagination && pagination.total > 1) { %> +
+
+ <% if (pagination.hasPrev) { %> + + Предыдущая + + <% } %> + <% if (pagination.hasNext) { %> + + Следующая + + <% } %> +
+
+ <% } %> +
+ + \ No newline at end of file diff --git a/views/admin/settings.ejs b/views/admin/settings.ejs new file mode 100644 index 0000000..581ae50 --- /dev/null +++ b/views/admin/settings.ejs @@ -0,0 +1,350 @@ + + + + + + Настройки сайта - SmartSolTech Admin + + + + + + + + + + + + + +
+
+
+
+

+ + SmartSolTech Admin +

+
+
+ + Добро пожаловать, <%= user ? user.name : 'Admin' %>! + + + + Посмотреть сайт + +
+ +
+
+
+
+
+ +
+ + + + +
+
+ +
+

+ + Настройки сайта +

+

Управление основными параметрами сайта

+
+ + +
+
+

+ + Настройки сайта +

+
+ +
+
+ +
+

Основные настройки

+
+
+ + +
+ +
+ + +
+ +
+ + + <% if (settings.logo) { %> + Current logo + <% } %> +
+ +
+ + + <% if (settings.favicon) { %> + Current favicon + <% } %> +
+
+
+ + +
+

Контактная информация

+
+
+ + +
+ +
+ + +
+ +
+ + +
+
+
+ + +
+

Социальные сети

+
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+
+ + +
+

Telegram Bot

+
+
+ + +

Получите токен у @BotFather

+
+ +
+ + +

ID чата для уведомлений

+
+ +
+ +
+
+
+
+ + +
+

SEO настройки

+
+
+ + +
+ +
+ + +
+ +
+ + +
+
+
+
+ + +
+ +
+
+
+ + +
+
+
+ + + + + \ No newline at end of file diff --git a/views/admin/telegram.ejs b/views/admin/telegram.ejs new file mode 100644 index 0000000..5a53154 --- /dev/null +++ b/views/admin/telegram.ejs @@ -0,0 +1,885 @@ + + + + + + Telegram Bot - SmartSolTech Admin + + + + + + + + + + + + + +
+
+
+
+

+ + SmartSolTech Admin +

+
+
+ + Добро пожаловать, <%= user ? user.name : 'Admin' %>! + + + + Посмотреть сайт + +
+ +
+
+
+
+
+ +
+ + + + +
+
+ +
+
+
+

+ + Telegram Bot +

+

Настройка и управление уведомлениями через Telegram

+
+
+
+
+ + <%= botConfigured ? 'Подключен' : 'Не настроен' %> + +
+
+
+
+ + +
+

+ + Конфигурация бота +

+ +
+
+ +
+ +
+ + +
+

+ Получите токен от @BotFather +

+
+ + +
+ + +

+ Оставьте пустым, если будете выбирать чат из списка +

+
+
+ +
+ + +
+
+ + +
+ + <% if (botConfigured) { %> + +
+
+

+ + Информация о боте +

+ +
+ +
+ <% if (botInfo) { %> +
+
+
Имя бота
+
@<%= botInfo.username %>
+
+
+
Отображаемое имя
+
<%= botInfo.first_name %>
+
+
+
ID бота
+
<%= botInfo.id %>
+
+
+
Может читать сообщения
+
+ <%= botInfo.can_read_all_group_messages ? 'Да' : 'Нет' %> +
+
+
+ <% } else { %> +
+ +

Настройте токен бота для получения информации

+
+ <% } %> +
+
+ + +
+
+

+ + Доступные чаты +

+ +
+ +
+ <% if (availableChats && availableChats.length > 0) { %> +
+ <% availableChats.forEach(chat => { %> +
+
+
+ +
+
+
<%= chat.title %>
+
+ <%= chat.type %> • ID: <%= chat.id %> + <% if (chat.username) { %>• @<%= chat.username %><% } %> +
+
+
+ +
+ <% }); %> +
+ <% } else { %> +
+ +

Чаты не найдены

+

Отправьте боту сообщение или добавьте его в группу, затем нажмите "Найти чаты"

+
+ <% } %> +
+
+ <% } %> + + +
+

+ + Отправить сообщение +

+ +
+ +
+ + +
+ + +
+ +
+ <% if (availableChats && availableChats.length > 0) { %> + <% availableChats.forEach(chat => { %> + + <% }); %> + <% } else { %> +
+ + Сообщение будет отправлено в чат по умолчанию +
+ <% } %> +
+
+ + +
+

Настройки сообщения

+
+ + +
+
+ + +
+
+ + +
+ +
+ + +
+
+
+ + +
+ +
+ +
+

+ + Проверка подключения +

+

+ Отправить тестовое сообщение для проверки работоспособности бота. +

+ + +
+ + +
+

+ + Отправить сообщение +

+
+
+ + +
+ +
+ +
+
+ + +
+

+ + Настройки уведомлений +

+ +
+ +
+

Типы уведомлений

+
+
+
+ + Новые обращения +
+
+
+
+
+ + Расчеты стоимости +
+
+
+
+
+ + Новые проекты +
+
+
+
+
+ + Новые услуги +
+
+
+
+
+ + +
+

Информация о боте

+
+
+ Статус: + Активен +
+
+ Токен: + •••••••••• +
+
+ Chat ID: + •••••••••• +
+
+ Последнее уведомление: + Недавно +
+
+
+
+
+ + +
+

+ + Недавние уведомления +

+ +
+ +

Уведомления будут отображаться здесь после отправки

+
+
+ <% } %> +
+
+
+ + + + + + + \ No newline at end of file diff --git a/views/calculator.ejs b/views/calculator.ejs index f876cf5..1b57f80 100644 --- a/views/calculator.ejs +++ b/views/calculator.ejs @@ -19,7 +19,7 @@ <%- include('partials/navigation') %> -
+

diff --git a/views/contact.ejs b/views/contact.ejs index b7efc4e..cd4dbb3 100644 --- a/views/contact.ejs +++ b/views/contact.ejs @@ -19,7 +19,7 @@ <%- include('partials/navigation') %> -
+

diff --git a/views/error.ejs b/views/error.ejs index 39b8900..772066c 100644 --- a/views/error.ejs +++ b/views/error.ejs @@ -1,9 +1,9 @@ - + - 오류 - SmartSolTech + <%= title || __('errors.title') %> @@ -14,9 +14,11 @@ + + - - <%- include('partials/navigation') %> + + <%- include('partials/navigation', { settings: settings || {}, currentPage: 'error' }) %>
@@ -30,12 +32,12 @@

- <%= title || '오류가 발생했습니다' %> + <%= title || __('errors.default_title') %>

- <%= message || '요청을 처리하는 중 문제가 발생했습니다.' %> + <%= message || __('errors.default_message') %>

@@ -43,37 +45,53 @@ - 홈으로 돌아가기 + <%= __('errors.back_home') %>

-

도움이 필요하신가요?

+

<%= __('errors.need_help') %>

- 문제가 지속되면 언제든지 저희에게 연락해 주세요. + <%= __('errors.help_message') %>

- 문의하기 + <%= __('errors.contact_support') %> + <% if (settings && settings.contact && settings.contact.email) { %> + + + <%= settings.contact.email %> + + <% } else { %> info@smartsoltech.kr + <% } %> + <% if (settings && settings.contact && settings.contact.phone) { %> + + + <%= settings.contact.phone %> + + <% } else { %> +82-10-1234-5678 + <% } %>

diff --git a/views/index.ejs b/views/index.ejs index 2a60a82..f6fcb0c 100644 --- a/views/index.ejs +++ b/views/index.ejs @@ -3,7 +3,7 @@ - <%- __(title || 'meta.title') %> + <%- title || 'SmartSolTech - Innovative Technology Solutions' %> @@ -17,15 +17,39 @@ - + - - - - + + - + + + + + + + + + + + + <%- include('partials/navigation') %> @@ -337,64 +361,34 @@ + + + + + + \ No newline at end of file diff --git a/views/layout.ejs b/views/layout.ejs index a1a4954..93d1e0b 100644 --- a/views/layout.ejs +++ b/views/layout.ejs @@ -1,5 +1,5 @@ - + @@ -35,14 +35,39 @@ - - - + + - - + + + + + + + + + + + + + - + <%- include('partials/navigation') %> diff --git a/views/partials/footer.ejs b/views/partials/footer.ejs index 7200dad..1e0ae0a 100644 --- a/views/partials/footer.ejs +++ b/views/partials/footer.ejs @@ -8,7 +8,7 @@ SmartSolTech

- <%- __('footer.company.description') %> + <%= __('footer.company.description') %>

@@ -70,34 +70,34 @@
-

<%- __('footer.links.title') %>

+

<%= __('footer.links.title') %>

-

<%- __('footer.contact.title') %>

+

<%= __('footer.contact.title') %>

@@ -114,11 +114,11 @@

- <%- __('footer.copyright', { year: new Date().getFullYear() }) %> + <%= __('footer.copyright').replace('{{year}}', new Date().getFullYear()) %>

diff --git a/views/partials/navigation.ejs b/views/partials/navigation.ejs index fc18f45..669611d 100644 --- a/views/partials/navigation.ejs +++ b/views/partials/navigation.ejs @@ -13,88 +13,69 @@
- - - - - -
@@ -88,7 +90,7 @@ <% if (item.featured) { %>
- FEATURED + <%- __('portfolio_page.labels.featured') %>
<% } %> @@ -96,7 +98,7 @@ @@ -149,7 +151,7 @@ @@ -158,8 +160,8 @@ <% } else { %>
-

아직 포트폴리오가 없습니다

-

곧 멋진 프로젝트들을 공개할 예정입니다!

+

<%- __('portfolio_page.empty.title') %>

+

<%- __('portfolio_page.empty.subtitle') %>

<% } %> @@ -168,7 +170,7 @@ <% if (portfolioItems && portfolioItems.length >= 9) { %>
<% } %> @@ -179,17 +181,17 @@

- 다음 프로젝트의 주인공이 되어보세요 + <%- __('portfolio_page.cta.title') %>

- 우리와 함께 혁신적인 디지털 솔루션을 만들어보세요 + <%- __('portfolio_page.cta.subtitle') %>

@@ -266,30 +268,30 @@ } }); - // Category name mapping + // Category name mapping - uses server-side localization function getCategoryName(category) { const categoryNames = { - 'web-development': '웹 개발', - 'mobile-app': '모바일 앱', - 'ui-ux-design': 'UI/UX 디자인', - 'branding': '브랜딩', - 'marketing': '디지털 마케팅' + 'web-development': '<%- __("portfolio_page.categories.web-development") %>', + 'mobile-app': '<%- __("portfolio_page.categories.mobile-app") %>', + 'ui-ux-design': '<%- __("portfolio_page.categories.ui-ux-design") %>', + 'branding': '<%- __("portfolio_page.categories.branding") %>', + 'marketing': '<%- __("portfolio_page.categories.marketing") %>' }; return categoryNames[category] || category; } <% - // Helper function for category names + // Helper function for category names - uses i18n function getCategoryName(category) { - const categoryNames = { - 'web-development': '웹 개발', - 'mobile-app': '모바일 앱', - 'ui-ux-design': 'UI/UX 디자인', - 'branding': '브랜딩', - 'marketing': '디지털 마케팅' + const categoryMap = { + 'web-development': 'portfolio_page.categories.web-development', + 'mobile-app': 'portfolio_page.categories.mobile-app', + 'ui-ux-design': 'portfolio_page.categories.ui-ux-design', + 'branding': 'portfolio_page.categories.branding', + 'marketing': 'portfolio_page.categories.marketing' }; - return categoryNames[category] || category; + return categoryMap[category] ? __(categoryMap[category]) : category; } %> diff --git a/views/services.ejs b/views/services.ejs index 02f0191..4cf1f8d 100644 --- a/views/services.ejs +++ b/views/services.ejs @@ -1,13 +1,13 @@ - + - 서비스 - SmartSolTech + <%- __('services.meta.title') %> - SmartSolTech - - + + @@ -19,18 +19,20 @@ + + - + <%- include('partials/navigation') %> - -
-
-

- 우리의 서비스 + +
+
+

+ <%- __('services.hero.title') %> <%- __('services.hero.title_highlight') %>

- 혁신적인 기술로 비즈니스의 성장을 지원합니다 + <%- __('services.hero.subtitle') %>

@@ -62,9 +64,9 @@ <% if (service.pricing) { %>
-
시작가격
+
<%- __('services.cards.starting_price') %>
- <%= service.pricing.basePrice ? service.pricing.basePrice.toLocaleString() : '상담' %> + <%= service.pricing.basePrice ? service.pricing.basePrice.toLocaleString() : __('services.cards.consultation') %> <% if (service.pricing.basePrice) { %> 원~ <% } %> @@ -82,11 +84,11 @@ @@ -94,7 +96,7 @@ <% if (service.featured) { %>
- 인기 + <%- __('services.cards.popular') %>
<% } %> @@ -103,8 +105,8 @@ <% } else { %>
-

서비스 준비 중

-

곧 다양한 서비스를 제공할 예정입니다!

+

<%- __('services.cards.coming_soon') %>

+

<%- __('services.cards.coming_soon_desc') %>

<% } %>
@@ -116,10 +118,10 @@

- 프로젝트 진행 과정 + <%- __('services.process.title') %>

-

- 체계적이고 전문적인 프로세스로 프로젝트를 진행합니다 +

+ <%- __('services.process.subtitle') %>