init commit
This commit is contained in:
34
.env.example
Normal file
34
.env.example
Normal file
@@ -0,0 +1,34 @@
|
||||
# Environment Configuration
|
||||
NODE_ENV=development
|
||||
PORT=3000
|
||||
|
||||
# Database
|
||||
MONGODB_URI=mongodb://localhost:27017/smartsoltech
|
||||
|
||||
# JWT Secret
|
||||
JWT_SECRET=your_super_secret_jwt_key_here_change_in_production
|
||||
|
||||
# Session Secret
|
||||
SESSION_SECRET=your_session_secret_here_change_in_production
|
||||
|
||||
# Telegram Bot
|
||||
TELEGRAM_BOT_TOKEN=your_telegram_bot_token_here
|
||||
TELEGRAM_CHAT_ID=your_telegram_chat_id_here
|
||||
|
||||
# Email Configuration (for contact forms)
|
||||
EMAIL_HOST=smtp.gmail.com
|
||||
EMAIL_PORT=587
|
||||
EMAIL_USER=your_email@gmail.com
|
||||
EMAIL_PASS=your_email_password
|
||||
|
||||
# Admin Credentials (default)
|
||||
ADMIN_EMAIL=admin@smartsoltech.kr
|
||||
ADMIN_PASSWORD=admin123456
|
||||
|
||||
# File Upload
|
||||
MAX_FILE_SIZE=10485760
|
||||
UPLOAD_PATH=./public/uploads
|
||||
|
||||
# Site Configuration
|
||||
SITE_URL=https://smartsoltech.kr
|
||||
SITE_NAME=SmartSolTech
|
||||
20
.gitignore
vendored
Normal file
20
.gitignore
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
node_modules/
|
||||
.env
|
||||
.env.local
|
||||
.env.production
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.DS_Store
|
||||
dist/
|
||||
build/
|
||||
uploads/
|
||||
*.log
|
||||
.vscode/
|
||||
.idea/
|
||||
coverage/
|
||||
.nyc_output/
|
||||
*.tgz
|
||||
*.tar.gz
|
||||
.cache/
|
||||
.parcel-cache/
|
||||
34
.history/.env_20251019160502.example
Normal file
34
.history/.env_20251019160502.example
Normal file
@@ -0,0 +1,34 @@
|
||||
# Environment Configuration
|
||||
NODE_ENV=development
|
||||
PORT=3000
|
||||
|
||||
# Database
|
||||
MONGODB_URI=mongodb://localhost:27017/smartsoltech
|
||||
|
||||
# JWT Secret
|
||||
JWT_SECRET=your_super_secret_jwt_key_here_change_in_production
|
||||
|
||||
# Session Secret
|
||||
SESSION_SECRET=your_session_secret_here_change_in_production
|
||||
|
||||
# Telegram Bot
|
||||
TELEGRAM_BOT_TOKEN=your_telegram_bot_token_here
|
||||
TELEGRAM_CHAT_ID=your_telegram_chat_id_here
|
||||
|
||||
# Email Configuration (for contact forms)
|
||||
EMAIL_HOST=smtp.gmail.com
|
||||
EMAIL_PORT=587
|
||||
EMAIL_USER=your_email@gmail.com
|
||||
EMAIL_PASS=your_email_password
|
||||
|
||||
# Admin Credentials (default)
|
||||
ADMIN_EMAIL=admin@smartsoltech.kr
|
||||
ADMIN_PASSWORD=admin123456
|
||||
|
||||
# File Upload
|
||||
MAX_FILE_SIZE=10485760
|
||||
UPLOAD_PATH=./public/uploads
|
||||
|
||||
# Site Configuration
|
||||
SITE_URL=https://smartsoltech.kr
|
||||
SITE_NAME=SmartSolTech
|
||||
34
.history/.env_20251019162544.example
Normal file
34
.history/.env_20251019162544.example
Normal file
@@ -0,0 +1,34 @@
|
||||
# Environment Configuration
|
||||
NODE_ENV=development
|
||||
PORT=3000
|
||||
|
||||
# Database
|
||||
MONGODB_URI=mongodb://localhost:27017/smartsoltech
|
||||
|
||||
# JWT Secret
|
||||
JWT_SECRET=your_super_secret_jwt_key_here_change_in_production
|
||||
|
||||
# Session Secret
|
||||
SESSION_SECRET=your_session_secret_here_change_in_production
|
||||
|
||||
# Telegram Bot
|
||||
TELEGRAM_BOT_TOKEN=your_telegram_bot_token_here
|
||||
TELEGRAM_CHAT_ID=your_telegram_chat_id_here
|
||||
|
||||
# Email Configuration (for contact forms)
|
||||
EMAIL_HOST=smtp.gmail.com
|
||||
EMAIL_PORT=587
|
||||
EMAIL_USER=your_email@gmail.com
|
||||
EMAIL_PASS=your_email_password
|
||||
|
||||
# Admin Credentials (default)
|
||||
ADMIN_EMAIL=admin@smartsoltech.kr
|
||||
ADMIN_PASSWORD=admin123456
|
||||
|
||||
# File Upload
|
||||
MAX_FILE_SIZE=10485760
|
||||
UPLOAD_PATH=./public/uploads
|
||||
|
||||
# Site Configuration
|
||||
SITE_URL=https://smartsoltech.kr
|
||||
SITE_NAME=SmartSolTech
|
||||
20
.history/.gitignore_20251019160509
Normal file
20
.history/.gitignore_20251019160509
Normal file
@@ -0,0 +1,20 @@
|
||||
node_modules/
|
||||
.env
|
||||
.env.local
|
||||
.env.production
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.DS_Store
|
||||
dist/
|
||||
build/
|
||||
uploads/
|
||||
*.log
|
||||
.vscode/
|
||||
.idea/
|
||||
coverage/
|
||||
.nyc_output/
|
||||
*.tgz
|
||||
*.tar.gz
|
||||
.cache/
|
||||
.parcel-cache/
|
||||
20
.history/.gitignore_20251019162544
Normal file
20
.history/.gitignore_20251019162544
Normal file
@@ -0,0 +1,20 @@
|
||||
node_modules/
|
||||
.env
|
||||
.env.local
|
||||
.env.production
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.DS_Store
|
||||
dist/
|
||||
build/
|
||||
uploads/
|
||||
*.log
|
||||
.vscode/
|
||||
.idea/
|
||||
coverage/
|
||||
.nyc_output/
|
||||
*.tgz
|
||||
*.tar.gz
|
||||
.cache/
|
||||
.parcel-cache/
|
||||
280
.history/README_20251019162701.md
Normal file
280
.history/README_20251019162701.md
Normal file
@@ -0,0 +1,280 @@
|
||||
# SmartSolTech Website
|
||||
|
||||
Современный PWA сайт для SmartSolTech с админ-панелью, управлением портфолио, интеграцией с Telegram ботом и калькулятором стоимости услуг.
|
||||
|
||||
## 🚀 Особенности
|
||||
|
||||
- **Современный дизайн**: Отзывчивый интерфейс с анимациями и современным UI/UX
|
||||
- **PWA**: Прогрессивное веб-приложение с поддержкой офлайн-режима
|
||||
- **Админ-панель**: Управление портфолио, услугами, медиа-контентом и настройками сайта
|
||||
- **Интеграция с Telegram**: Подключение к Telegram боту для уведомлений
|
||||
- **Калькулятор стоимости**: Интерактивный калькулятор расчета стоимости услуг
|
||||
- **Система аутентификации**: Безопасная авторизация с JWT токенами
|
||||
- **Загрузка файлов**: Оптимизация изображений и управление медиа-контентом
|
||||
- **SEO оптимизация**: Настроенные мета-теги и структурированные данные
|
||||
|
||||
## 🛠️ Технологии
|
||||
|
||||
### Backend
|
||||
- **Node.js** - Серверная платформа
|
||||
- **Express.js** - Веб-фреймворк
|
||||
- **MongoDB** - База данных
|
||||
- **Mongoose** - ODM для MongoDB
|
||||
- **JWT** - Токены аутентификации
|
||||
- **bcrypt** - Хеширование паролей
|
||||
- **Multer** - Загрузка файлов
|
||||
- **Sharp** - Обработка изображений
|
||||
|
||||
### Frontend
|
||||
- **EJS** - Шаблонизатор
|
||||
- **Tailwind CSS** - CSS фреймворк
|
||||
- **AOS** - Библиотека анимаций
|
||||
- **Font Awesome** - Иконки
|
||||
- **Service Worker** - PWA функциональность
|
||||
|
||||
### Дополнительно
|
||||
- **node-telegram-bot-api** - Интеграция с Telegram
|
||||
- **Nodemailer** - Отправка email
|
||||
- **Helmet** - Безопасность
|
||||
- **express-rate-limit** - Ограничение запросов
|
||||
|
||||
## 📦 Установка
|
||||
|
||||
1. **Клонирование репозитория**
|
||||
```bash
|
||||
git clone <repository-url>
|
||||
cd sst_site
|
||||
```
|
||||
|
||||
2. **Установка зависимостей**
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
3. **Настройка переменных окружения**
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
Отредактируйте файл `.env` с вашими настройками:
|
||||
```env
|
||||
NODE_ENV=development
|
||||
PORT=3000
|
||||
|
||||
# Database
|
||||
MONGODB_URI=mongodb://localhost:27017/smartsoltech
|
||||
|
||||
# Security
|
||||
SESSION_SECRET=your-super-secret-session-key
|
||||
JWT_SECRET=your-super-secret-jwt-key
|
||||
|
||||
# File Upload
|
||||
UPLOAD_PATH=./uploads
|
||||
MAX_FILE_SIZE=10485760
|
||||
|
||||
# Email Configuration
|
||||
SMTP_HOST=smtp.gmail.com
|
||||
SMTP_PORT=587
|
||||
SMTP_USER=your-email@gmail.com
|
||||
SMTP_PASS=your-app-password
|
||||
|
||||
# Telegram Bot (Optional)
|
||||
TELEGRAM_BOT_TOKEN=your-telegram-bot-token
|
||||
|
||||
# Admin Account
|
||||
ADMIN_EMAIL=admin@smartsoltech.kr
|
||||
ADMIN_PASSWORD=change-this-password
|
||||
```
|
||||
|
||||
4. **Инициализация базы данных**
|
||||
```bash
|
||||
npm run init-db
|
||||
```
|
||||
|
||||
5. **Запуск в режиме разработки**
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Сайт будет доступен по адресу: `http://localhost:3000`
|
||||
|
||||
## 🗂️ Структура проекта
|
||||
|
||||
```
|
||||
sst_site/
|
||||
├── models/ # Модели данных (MongoDB)
|
||||
│ ├── User.js
|
||||
│ ├── Portfolio.js
|
||||
│ ├── Service.js
|
||||
│ ├── Contact.js
|
||||
│ └── SiteSettings.js
|
||||
├── routes/ # Маршруты API
|
||||
│ ├── index.js
|
||||
│ ├── auth.js
|
||||
│ ├── contact.js
|
||||
│ ├── calculator.js
|
||||
│ ├── portfolio.js
|
||||
│ ├── services.js
|
||||
│ ├── media.js
|
||||
│ └── admin.js
|
||||
├── views/ # Шаблоны EJS
|
||||
│ ├── layout.ejs
|
||||
│ ├── index.ejs
|
||||
│ ├── calculator.ejs
|
||||
│ └── partials/
|
||||
├── public/ # Статические файлы
|
||||
│ ├── css/
|
||||
│ ├── js/
|
||||
│ ├── images/
|
||||
│ ├── manifest.json
|
||||
│ └── sw.js
|
||||
├── middleware/ # Промежуточное ПО
|
||||
├── scripts/ # Служебные скрипты
|
||||
│ ├── init-db.js
|
||||
│ ├── dev.js
|
||||
│ └── build.js
|
||||
├── uploads/ # Загруженные файлы
|
||||
├── server.js # Главный файл сервера
|
||||
├── package.json
|
||||
└── README.md
|
||||
```
|
||||
|
||||
## 📋 Доступные команды
|
||||
|
||||
```bash
|
||||
# Разработка
|
||||
npm run dev # Запуск в режиме разработки с hot reload
|
||||
|
||||
# Продакшн
|
||||
npm start # Запуск в продакшн режиме
|
||||
npm run build # Сборка для продакшна
|
||||
|
||||
# База данных
|
||||
npm run init-db # Инициализация БД с тестовыми данными
|
||||
|
||||
# Тестирование
|
||||
npm test # Запуск тестов (пока не реализовано)
|
||||
```
|
||||
|
||||
## 🔐 Админ-панель
|
||||
|
||||
После инициализации базы данных вы можете войти в админ-панель:
|
||||
|
||||
- **URL**: `http://localhost:3000/admin`
|
||||
- **Email**: `admin@smartsoltech.kr` (или из .env)
|
||||
- **Пароль**: Указанный в .env файле
|
||||
|
||||
### Возможности админ-панели:
|
||||
- Управление портфолио проектами
|
||||
- Редактирование услуг и их стоимости
|
||||
- Загрузка и управление медиа-файлами
|
||||
- Просмотр и управление контактными формами
|
||||
- Настройки сайта и SEO
|
||||
- Управление пользователями
|
||||
|
||||
## 🤖 Интеграция с Telegram
|
||||
|
||||
Для настройки Telegram бота:
|
||||
|
||||
1. Создайте бота через [@BotFather](https://t.me/BotFather)
|
||||
2. Получите токен бота
|
||||
3. Добавьте токен в `.env` файл: `TELEGRAM_BOT_TOKEN=your-token`
|
||||
4. Перезапустите сервер
|
||||
|
||||
Бот будет отправлять уведомления о:
|
||||
- Новых контактных формах
|
||||
- Заказах через калькулятор
|
||||
- Новых комментариях
|
||||
|
||||
## 💰 Калькулятор стоимости
|
||||
|
||||
Интерактивный калькулятор позволяет клиентам:
|
||||
- Выбрать тип услуги
|
||||
- Указать дополнительные параметры
|
||||
- Получить примерную стоимость
|
||||
- Отправить заявку на расчет
|
||||
|
||||
Настройки калькулятора можно изменить в админ-панели.
|
||||
|
||||
## 🔒 Безопасность
|
||||
|
||||
Проект включает следующие меры безопасности:
|
||||
- Хеширование паролей с bcrypt
|
||||
- JWT токены для аутентификации
|
||||
- Защита от CSRF атак
|
||||
- Ограничение количества запросов
|
||||
- Валидация входных данных
|
||||
- Безопасные HTTP заголовки
|
||||
|
||||
## 📱 PWA функции
|
||||
|
||||
- Установка на устройство
|
||||
- Офлайн работа
|
||||
- Push уведомления
|
||||
- Фоновая синхронизация
|
||||
- Адаптивные иконки
|
||||
- Splash screen
|
||||
|
||||
## 🚀 Деплой
|
||||
|
||||
### Для продакшна:
|
||||
|
||||
1. **Сборка приложения**
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
2. **Настройка сервера**
|
||||
- Установите Node.js и MongoDB
|
||||
- Настройте переменные окружения
|
||||
- Настройте прокси-сервер (nginx)
|
||||
|
||||
3. **Запуск**
|
||||
```bash
|
||||
cd dist
|
||||
npm install --production
|
||||
npm start
|
||||
```
|
||||
|
||||
### Docker деплой:
|
||||
|
||||
```dockerfile
|
||||
FROM node:18-alpine
|
||||
WORKDIR /app
|
||||
COPY package*.json ./
|
||||
RUN npm ci --only=production
|
||||
COPY . .
|
||||
EXPOSE 3000
|
||||
CMD ["npm", "start"]
|
||||
```
|
||||
|
||||
## 🤝 Участие в разработке
|
||||
|
||||
1. Форкните репозиторий
|
||||
2. Создайте ветку для фичи (`git checkout -b feature/AmazingFeature`)
|
||||
3. Сделайте коммит (`git commit -m 'Add some AmazingFeature'`)
|
||||
4. Отправьте в ветку (`git push origin feature/AmazingFeature`)
|
||||
5. Откройте Pull Request
|
||||
|
||||
## 📝 Лицензия
|
||||
|
||||
Этот проект лицензирован под MIT License - подробности в файле [LICENSE](LICENSE).
|
||||
|
||||
## 📞 Поддержка
|
||||
|
||||
Если у вас есть вопросы или проблемы:
|
||||
|
||||
- Email: info@smartsoltech.kr
|
||||
- GitHub Issues: [Create Issue](../../issues)
|
||||
- Telegram: @smartsoltech
|
||||
|
||||
## 🙏 Благодарности
|
||||
|
||||
- [Express.js](https://expressjs.com/) - За отличный веб-фреймворк
|
||||
- [MongoDB](https://www.mongodb.com/) - За гибкую базу данных
|
||||
- [Tailwind CSS](https://tailwindcss.com/) - За удобный CSS фреймворк
|
||||
- [AOS](https://michalsnik.github.io/aos/) - За красивые анимации
|
||||
|
||||
---
|
||||
|
||||
**SmartSolTech** - Умные решения для вашего бизнеса 🚀
|
||||
280
.history/README_20251019163806.md
Normal file
280
.history/README_20251019163806.md
Normal file
@@ -0,0 +1,280 @@
|
||||
# SmartSolTech Website
|
||||
|
||||
Современный PWA сайт для SmartSolTech с админ-панелью, управлением портфолио, интеграцией с Telegram ботом и калькулятором стоимости услуг.
|
||||
|
||||
## 🚀 Особенности
|
||||
|
||||
- **Современный дизайн**: Отзывчивый интерфейс с анимациями и современным UI/UX
|
||||
- **PWA**: Прогрессивное веб-приложение с поддержкой офлайн-режима
|
||||
- **Админ-панель**: Управление портфолио, услугами, медиа-контентом и настройками сайта
|
||||
- **Интеграция с Telegram**: Подключение к Telegram боту для уведомлений
|
||||
- **Калькулятор стоимости**: Интерактивный калькулятор расчета стоимости услуг
|
||||
- **Система аутентификации**: Безопасная авторизация с JWT токенами
|
||||
- **Загрузка файлов**: Оптимизация изображений и управление медиа-контентом
|
||||
- **SEO оптимизация**: Настроенные мета-теги и структурированные данные
|
||||
|
||||
## 🛠️ Технологии
|
||||
|
||||
### Backend
|
||||
- **Node.js** - Серверная платформа
|
||||
- **Express.js** - Веб-фреймворк
|
||||
- **MongoDB** - База данных
|
||||
- **Mongoose** - ODM для MongoDB
|
||||
- **JWT** - Токены аутентификации
|
||||
- **bcrypt** - Хеширование паролей
|
||||
- **Multer** - Загрузка файлов
|
||||
- **Sharp** - Обработка изображений
|
||||
|
||||
### Frontend
|
||||
- **EJS** - Шаблонизатор
|
||||
- **Tailwind CSS** - CSS фреймворк
|
||||
- **AOS** - Библиотека анимаций
|
||||
- **Font Awesome** - Иконки
|
||||
- **Service Worker** - PWA функциональность
|
||||
|
||||
### Дополнительно
|
||||
- **node-telegram-bot-api** - Интеграция с Telegram
|
||||
- **Nodemailer** - Отправка email
|
||||
- **Helmet** - Безопасность
|
||||
- **express-rate-limit** - Ограничение запросов
|
||||
|
||||
## 📦 Установка
|
||||
|
||||
1. **Клонирование репозитория**
|
||||
```bash
|
||||
git clone <repository-url>
|
||||
cd sst_site
|
||||
```
|
||||
|
||||
2. **Установка зависимостей**
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
3. **Настройка переменных окружения**
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
Отредактируйте файл `.env` с вашими настройками:
|
||||
```env
|
||||
NODE_ENV=development
|
||||
PORT=3000
|
||||
|
||||
# Database
|
||||
MONGODB_URI=mongodb://localhost:27017/smartsoltech
|
||||
|
||||
# Security
|
||||
SESSION_SECRET=your-super-secret-session-key
|
||||
JWT_SECRET=your-super-secret-jwt-key
|
||||
|
||||
# File Upload
|
||||
UPLOAD_PATH=./uploads
|
||||
MAX_FILE_SIZE=10485760
|
||||
|
||||
# Email Configuration
|
||||
SMTP_HOST=smtp.gmail.com
|
||||
SMTP_PORT=587
|
||||
SMTP_USER=your-email@gmail.com
|
||||
SMTP_PASS=your-app-password
|
||||
|
||||
# Telegram Bot (Optional)
|
||||
TELEGRAM_BOT_TOKEN=your-telegram-bot-token
|
||||
|
||||
# Admin Account
|
||||
ADMIN_EMAIL=admin@smartsoltech.kr
|
||||
ADMIN_PASSWORD=change-this-password
|
||||
```
|
||||
|
||||
4. **Инициализация базы данных**
|
||||
```bash
|
||||
npm run init-db
|
||||
```
|
||||
|
||||
5. **Запуск в режиме разработки**
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Сайт будет доступен по адресу: `http://localhost:3000`
|
||||
|
||||
## 🗂️ Структура проекта
|
||||
|
||||
```
|
||||
sst_site/
|
||||
├── models/ # Модели данных (MongoDB)
|
||||
│ ├── User.js
|
||||
│ ├── Portfolio.js
|
||||
│ ├── Service.js
|
||||
│ ├── Contact.js
|
||||
│ └── SiteSettings.js
|
||||
├── routes/ # Маршруты API
|
||||
│ ├── index.js
|
||||
│ ├── auth.js
|
||||
│ ├── contact.js
|
||||
│ ├── calculator.js
|
||||
│ ├── portfolio.js
|
||||
│ ├── services.js
|
||||
│ ├── media.js
|
||||
│ └── admin.js
|
||||
├── views/ # Шаблоны EJS
|
||||
│ ├── layout.ejs
|
||||
│ ├── index.ejs
|
||||
│ ├── calculator.ejs
|
||||
│ └── partials/
|
||||
├── public/ # Статические файлы
|
||||
│ ├── css/
|
||||
│ ├── js/
|
||||
│ ├── images/
|
||||
│ ├── manifest.json
|
||||
│ └── sw.js
|
||||
├── middleware/ # Промежуточное ПО
|
||||
├── scripts/ # Служебные скрипты
|
||||
│ ├── init-db.js
|
||||
│ ├── dev.js
|
||||
│ └── build.js
|
||||
├── uploads/ # Загруженные файлы
|
||||
├── server.js # Главный файл сервера
|
||||
├── package.json
|
||||
└── README.md
|
||||
```
|
||||
|
||||
## 📋 Доступные команды
|
||||
|
||||
```bash
|
||||
# Разработка
|
||||
npm run dev # Запуск в режиме разработки с hot reload
|
||||
|
||||
# Продакшн
|
||||
npm start # Запуск в продакшн режиме
|
||||
npm run build # Сборка для продакшна
|
||||
|
||||
# База данных
|
||||
npm run init-db # Инициализация БД с тестовыми данными
|
||||
|
||||
# Тестирование
|
||||
npm test # Запуск тестов (пока не реализовано)
|
||||
```
|
||||
|
||||
## 🔐 Админ-панель
|
||||
|
||||
После инициализации базы данных вы можете войти в админ-панель:
|
||||
|
||||
- **URL**: `http://localhost:3000/admin`
|
||||
- **Email**: `admin@smartsoltech.kr` (или из .env)
|
||||
- **Пароль**: Указанный в .env файле
|
||||
|
||||
### Возможности админ-панели:
|
||||
- Управление портфолио проектами
|
||||
- Редактирование услуг и их стоимости
|
||||
- Загрузка и управление медиа-файлами
|
||||
- Просмотр и управление контактными формами
|
||||
- Настройки сайта и SEO
|
||||
- Управление пользователями
|
||||
|
||||
## 🤖 Интеграция с Telegram
|
||||
|
||||
Для настройки Telegram бота:
|
||||
|
||||
1. Создайте бота через [@BotFather](https://t.me/BotFather)
|
||||
2. Получите токен бота
|
||||
3. Добавьте токен в `.env` файл: `TELEGRAM_BOT_TOKEN=your-token`
|
||||
4. Перезапустите сервер
|
||||
|
||||
Бот будет отправлять уведомления о:
|
||||
- Новых контактных формах
|
||||
- Заказах через калькулятор
|
||||
- Новых комментариях
|
||||
|
||||
## 💰 Калькулятор стоимости
|
||||
|
||||
Интерактивный калькулятор позволяет клиентам:
|
||||
- Выбрать тип услуги
|
||||
- Указать дополнительные параметры
|
||||
- Получить примерную стоимость
|
||||
- Отправить заявку на расчет
|
||||
|
||||
Настройки калькулятора можно изменить в админ-панели.
|
||||
|
||||
## 🔒 Безопасность
|
||||
|
||||
Проект включает следующие меры безопасности:
|
||||
- Хеширование паролей с bcrypt
|
||||
- JWT токены для аутентификации
|
||||
- Защита от CSRF атак
|
||||
- Ограничение количества запросов
|
||||
- Валидация входных данных
|
||||
- Безопасные HTTP заголовки
|
||||
|
||||
## 📱 PWA функции
|
||||
|
||||
- Установка на устройство
|
||||
- Офлайн работа
|
||||
- Push уведомления
|
||||
- Фоновая синхронизация
|
||||
- Адаптивные иконки
|
||||
- Splash screen
|
||||
|
||||
## 🚀 Деплой
|
||||
|
||||
### Для продакшна:
|
||||
|
||||
1. **Сборка приложения**
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
2. **Настройка сервера**
|
||||
- Установите Node.js и MongoDB
|
||||
- Настройте переменные окружения
|
||||
- Настройте прокси-сервер (nginx)
|
||||
|
||||
3. **Запуск**
|
||||
```bash
|
||||
cd dist
|
||||
npm install --production
|
||||
npm start
|
||||
```
|
||||
|
||||
### Docker деплой:
|
||||
|
||||
```dockerfile
|
||||
FROM node:18-alpine
|
||||
WORKDIR /app
|
||||
COPY package*.json ./
|
||||
RUN npm ci --only=production
|
||||
COPY . .
|
||||
EXPOSE 3000
|
||||
CMD ["npm", "start"]
|
||||
```
|
||||
|
||||
## 🤝 Участие в разработке
|
||||
|
||||
1. Форкните репозиторий
|
||||
2. Создайте ветку для фичи (`git checkout -b feature/AmazingFeature`)
|
||||
3. Сделайте коммит (`git commit -m 'Add some AmazingFeature'`)
|
||||
4. Отправьте в ветку (`git push origin feature/AmazingFeature`)
|
||||
5. Откройте Pull Request
|
||||
|
||||
## 📝 Лицензия
|
||||
|
||||
Этот проект лицензирован под MIT License - подробности в файле [LICENSE](LICENSE).
|
||||
|
||||
## 📞 Поддержка
|
||||
|
||||
Если у вас есть вопросы или проблемы:
|
||||
|
||||
- Email: info@smartsoltech.kr
|
||||
- GitHub Issues: [Create Issue](../../issues)
|
||||
- Telegram: @smartsoltech
|
||||
|
||||
## 🙏 Благодарности
|
||||
|
||||
- [Express.js](https://expressjs.com/) - За отличный веб-фреймворк
|
||||
- [MongoDB](https://www.mongodb.com/) - За гибкую базу данных
|
||||
- [Tailwind CSS](https://tailwindcss.com/) - За удобный CSS фреймворк
|
||||
- [AOS](https://michalsnik.github.io/aos/) - За красивые анимации
|
||||
|
||||
---
|
||||
|
||||
**SmartSolTech** - Умные решения для вашего бизнеса 🚀
|
||||
173
.history/locales/en_20251019171415.json
Normal file
173
.history/locales/en_20251019171415.json
Normal file
@@ -0,0 +1,173 @@
|
||||
{
|
||||
"navigation": {
|
||||
"home": "Home",
|
||||
"about": "About",
|
||||
"services": "Services",
|
||||
"portfolio": "Portfolio",
|
||||
"contact": "Contact",
|
||||
"calculator": "Calculator",
|
||||
"admin": "Admin"
|
||||
},
|
||||
"hero": {
|
||||
"title": "Smart Technology",
|
||||
"subtitle": "Solutions",
|
||||
"description": "Innovative web development, mobile apps, UI/UX design leading your business digital transformation",
|
||||
"cta_primary": "Start Project",
|
||||
"cta_secondary": "View Portfolio"
|
||||
},
|
||||
"services": {
|
||||
"title": "Our",
|
||||
"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"
|
||||
},
|
||||
"portfolio": {
|
||||
"title": "Recent",
|
||||
"title_highlight": "Projects",
|
||||
"description": "Check out the projects completed for customer success",
|
||||
"view_details": "View Details",
|
||||
"view_all": "View All Portfolio"
|
||||
},
|
||||
"calculator": {
|
||||
"title": "Check Your Project Estimate",
|
||||
"description": "Select your desired services and requirements to calculate estimates in real time",
|
||||
"cta": "Use Estimate Calculator"
|
||||
},
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"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": "SmartSolTech",
|
||||
"description": "Digital solution specialist leading innovation",
|
||||
"quick_links": "Quick Links",
|
||||
"services": "Services",
|
||||
"contact_info": "Contact Information",
|
||||
"follow_us": "Follow Us",
|
||||
"rights": "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"
|
||||
}
|
||||
}
|
||||
173
.history/locales/en_20251019171645.json
Normal file
173
.history/locales/en_20251019171645.json
Normal file
@@ -0,0 +1,173 @@
|
||||
{
|
||||
"navigation": {
|
||||
"home": "Home",
|
||||
"about": "About",
|
||||
"services": "Services",
|
||||
"portfolio": "Portfolio",
|
||||
"contact": "Contact",
|
||||
"calculator": "Calculator",
|
||||
"admin": "Admin"
|
||||
},
|
||||
"hero": {
|
||||
"title": "Smart Technology",
|
||||
"subtitle": "Solutions",
|
||||
"description": "Innovative web development, mobile apps, UI/UX design leading your business digital transformation",
|
||||
"cta_primary": "Start Project",
|
||||
"cta_secondary": "View Portfolio"
|
||||
},
|
||||
"services": {
|
||||
"title": "Our",
|
||||
"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"
|
||||
},
|
||||
"portfolio": {
|
||||
"title": "Recent",
|
||||
"title_highlight": "Projects",
|
||||
"description": "Check out the projects completed for customer success",
|
||||
"view_details": "View Details",
|
||||
"view_all": "View All Portfolio"
|
||||
},
|
||||
"calculator": {
|
||||
"title": "Check Your Project Estimate",
|
||||
"description": "Select your desired services and requirements to calculate estimates in real time",
|
||||
"cta": "Use Estimate Calculator"
|
||||
},
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"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": "SmartSolTech",
|
||||
"description": "Digital solution specialist leading innovation",
|
||||
"quick_links": "Quick Links",
|
||||
"services": "Services",
|
||||
"contact_info": "Contact Information",
|
||||
"follow_us": "Follow Us",
|
||||
"rights": "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"
|
||||
}
|
||||
}
|
||||
223
.history/locales/en_20251019181515.json
Normal file
223
.history/locales/en_20251019181515.json
Normal file
@@ -0,0 +1,223 @@
|
||||
{
|
||||
"navigation": {
|
||||
"home": "Home",
|
||||
"about": "About",
|
||||
"services": "Services",
|
||||
"portfolio": "Portfolio",
|
||||
"contact": "Contact",
|
||||
"calculator": "Calculator",
|
||||
"admin": "Admin"
|
||||
},
|
||||
"hero": {
|
||||
"title": "Smart Technology",
|
||||
"subtitle": "Solutions",
|
||||
"description": "Innovative web development, mobile apps, UI/UX design leading your business digital transformation",
|
||||
"cta_primary": "Start Project",
|
||||
"cta_secondary": "View Portfolio"
|
||||
},
|
||||
"services": {
|
||||
"title": "Our",
|
||||
"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"
|
||||
},
|
||||
"portfolio": {
|
||||
"title": "Recent",
|
||||
"title_highlight": "Projects",
|
||||
"description": "Check out the projects completed for customer success",
|
||||
"view_details": "View Details",
|
||||
"view_all": "View All Portfolio"
|
||||
},
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"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": "SmartSolTech",
|
||||
"description": "Digital solution specialist leading innovation",
|
||||
"quick_links": "Quick Links",
|
||||
"services": "Services",
|
||||
"contact_info": "Contact Information",
|
||||
"follow_us": "Follow Us",
|
||||
"rights": "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"
|
||||
}
|
||||
}
|
||||
223
.history/locales/en_20251019181629.json
Normal file
223
.history/locales/en_20251019181629.json
Normal file
@@ -0,0 +1,223 @@
|
||||
{
|
||||
"navigation": {
|
||||
"home": "Home",
|
||||
"about": "About",
|
||||
"services": "Services",
|
||||
"portfolio": "Portfolio",
|
||||
"contact": "Contact",
|
||||
"calculator": "Calculator",
|
||||
"admin": "Admin"
|
||||
},
|
||||
"hero": {
|
||||
"title": "Smart Technology",
|
||||
"subtitle": "Solutions",
|
||||
"description": "Innovative web development, mobile apps, UI/UX design leading your business digital transformation",
|
||||
"cta_primary": "Start Project",
|
||||
"cta_secondary": "View Portfolio"
|
||||
},
|
||||
"services": {
|
||||
"title": "Our",
|
||||
"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"
|
||||
},
|
||||
"portfolio": {
|
||||
"title": "Recent",
|
||||
"title_highlight": "Projects",
|
||||
"description": "Check out the projects completed for customer success",
|
||||
"view_details": "View Details",
|
||||
"view_all": "View All Portfolio"
|
||||
},
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"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": "SmartSolTech",
|
||||
"description": "Digital solution specialist leading innovation",
|
||||
"quick_links": "Quick Links",
|
||||
"services": "Services",
|
||||
"contact_info": "Contact Information",
|
||||
"follow_us": "Follow Us",
|
||||
"rights": "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"
|
||||
}
|
||||
}
|
||||
173
.history/locales/kk_20251019171626.json
Normal file
173
.history/locales/kk_20251019171626.json
Normal file
@@ -0,0 +1,173 @@
|
||||
{
|
||||
"navigation": {
|
||||
"home": "Басты бет",
|
||||
"about": "Біз туралы",
|
||||
"services": "Қызметтер",
|
||||
"portfolio": "Портфолио",
|
||||
"contact": "Байланыс",
|
||||
"calculator": "Калькулятор",
|
||||
"admin": "Админ"
|
||||
},
|
||||
"hero": {
|
||||
"title": "Ақылды Технологиялық",
|
||||
"subtitle": "Шешімдер",
|
||||
"description": "Инновациялық веб-әзірлеу, мобильді қосымшалар, UI/UX дизайн арқылы бизнестің цифрлық трансформациясын жүргіземіз",
|
||||
"cta_primary": "Жобаны бастау",
|
||||
"cta_secondary": "Портфолионы көру"
|
||||
},
|
||||
"services": {
|
||||
"title": "Біздің",
|
||||
"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": "Барлық қызметтерді көру"
|
||||
},
|
||||
"portfolio": {
|
||||
"title": "Соңғы",
|
||||
"title_highlight": "Жобалар",
|
||||
"description": "Тұтынушылардың табысы үшін аяқталған жобаларды тексеріңіз",
|
||||
"view_details": "Толығырақ",
|
||||
"view_all": "Барлық портфолионы көру"
|
||||
},
|
||||
"calculator": {
|
||||
"title": "Жобаңыздың бағасын тексеріңіз",
|
||||
"description": "Қажетті қызметтер мен талаптарды таңдап, нақты уақытта бағаны есептеңіз",
|
||||
"cta": "Баға калькуляторын пайдалану"
|
||||
},
|
||||
"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": "Кеңес беру үшін өтініш беру"
|
||||
}
|
||||
},
|
||||
"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": "SmartSolTech",
|
||||
"description": "Инновацияны басқаратын цифрлық шешімдер маманы",
|
||||
"quick_links": "Жылдам сілтемелер",
|
||||
"services": "Қызметтер",
|
||||
"contact_info": "Байланыс ақпараты",
|
||||
"follow_us": "Бізді іздеңіз",
|
||||
"rights": "Барлық құқықтар сақталған."
|
||||
},
|
||||
"theme": {
|
||||
"light": "Ашық тема",
|
||||
"dark": "Қараңғы тема",
|
||||
"toggle": "Теманы ауыстыру"
|
||||
},
|
||||
"language": {
|
||||
"english": "English",
|
||||
"korean": "한국어",
|
||||
"russian": "Русский",
|
||||
"kazakh": "Қазақша"
|
||||
},
|
||||
"common": {
|
||||
"loading": "Жүктелуде...",
|
||||
"error": "Қате орын алды",
|
||||
"success": "Сәтті",
|
||||
"view_more": "Көбірек көру",
|
||||
"back": "Артқа",
|
||||
"next": "Келесі",
|
||||
"previous": "Алдыңғы"
|
||||
}
|
||||
}
|
||||
173
.history/locales/kk_20251019171645.json
Normal file
173
.history/locales/kk_20251019171645.json
Normal file
@@ -0,0 +1,173 @@
|
||||
{
|
||||
"navigation": {
|
||||
"home": "Басты бет",
|
||||
"about": "Біз туралы",
|
||||
"services": "Қызметтер",
|
||||
"portfolio": "Портфолио",
|
||||
"contact": "Байланыс",
|
||||
"calculator": "Калькулятор",
|
||||
"admin": "Админ"
|
||||
},
|
||||
"hero": {
|
||||
"title": "Ақылды Технологиялық",
|
||||
"subtitle": "Шешімдер",
|
||||
"description": "Инновациялық веб-әзірлеу, мобильді қосымшалар, UI/UX дизайн арқылы бизнестің цифрлық трансформациясын жүргіземіз",
|
||||
"cta_primary": "Жобаны бастау",
|
||||
"cta_secondary": "Портфолионы көру"
|
||||
},
|
||||
"services": {
|
||||
"title": "Біздің",
|
||||
"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": "Барлық қызметтерді көру"
|
||||
},
|
||||
"portfolio": {
|
||||
"title": "Соңғы",
|
||||
"title_highlight": "Жобалар",
|
||||
"description": "Тұтынушылардың табысы үшін аяқталған жобаларды тексеріңіз",
|
||||
"view_details": "Толығырақ",
|
||||
"view_all": "Барлық портфолионы көру"
|
||||
},
|
||||
"calculator": {
|
||||
"title": "Жобаңыздың бағасын тексеріңіз",
|
||||
"description": "Қажетті қызметтер мен талаптарды таңдап, нақты уақытта бағаны есептеңіз",
|
||||
"cta": "Баға калькуляторын пайдалану"
|
||||
},
|
||||
"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": "Кеңес беру үшін өтініш беру"
|
||||
}
|
||||
},
|
||||
"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": "SmartSolTech",
|
||||
"description": "Инновацияны басқаратын цифрлық шешімдер маманы",
|
||||
"quick_links": "Жылдам сілтемелер",
|
||||
"services": "Қызметтер",
|
||||
"contact_info": "Байланыс ақпараты",
|
||||
"follow_us": "Бізді іздеңіз",
|
||||
"rights": "Барлық құқықтар сақталған."
|
||||
},
|
||||
"theme": {
|
||||
"light": "Ашық тема",
|
||||
"dark": "Қараңғы тема",
|
||||
"toggle": "Теманы ауыстыру"
|
||||
},
|
||||
"language": {
|
||||
"english": "English",
|
||||
"korean": "한국어",
|
||||
"russian": "Русский",
|
||||
"kazakh": "Қазақша"
|
||||
},
|
||||
"common": {
|
||||
"loading": "Жүктелуде...",
|
||||
"error": "Қате орын алды",
|
||||
"success": "Сәтті",
|
||||
"view_more": "Көбірек көру",
|
||||
"back": "Артқа",
|
||||
"next": "Келесі",
|
||||
"previous": "Алдыңғы"
|
||||
}
|
||||
}
|
||||
223
.history/locales/kk_20251019181611.json
Normal file
223
.history/locales/kk_20251019181611.json
Normal file
@@ -0,0 +1,223 @@
|
||||
{
|
||||
"navigation": {
|
||||
"home": "Басты бет",
|
||||
"about": "Біз туралы",
|
||||
"services": "Қызметтер",
|
||||
"portfolio": "Портфолио",
|
||||
"contact": "Байланыс",
|
||||
"calculator": "Калькулятор",
|
||||
"admin": "Админ"
|
||||
},
|
||||
"hero": {
|
||||
"title": "Ақылды Технологиялық",
|
||||
"subtitle": "Шешімдер",
|
||||
"description": "Инновациялық веб-әзірлеу, мобильді қосымшалар, UI/UX дизайн арқылы бизнестің цифрлық трансформациясын жүргіземіз",
|
||||
"cta_primary": "Жобаны бастау",
|
||||
"cta_secondary": "Портфолионы көру"
|
||||
},
|
||||
"services": {
|
||||
"title": "Біздің",
|
||||
"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": "Барлық қызметтерді көру"
|
||||
},
|
||||
"portfolio": {
|
||||
"title": "Соңғы",
|
||||
"title_highlight": "Жобалар",
|
||||
"description": "Тұтынушылардың табысы үшін аяқталған жобаларды тексеріңіз",
|
||||
"view_details": "Толығырақ",
|
||||
"view_all": "Барлық портфолионы көру"
|
||||
},
|
||||
"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": "Кеңес беру үшін өтініш беру"
|
||||
}
|
||||
},
|
||||
"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": "SmartSolTech",
|
||||
"description": "Инновацияны басқаратын цифрлық шешімдер маманы",
|
||||
"quick_links": "Жылдам сілтемелер",
|
||||
"services": "Қызметтер",
|
||||
"contact_info": "Байланыс ақпараты",
|
||||
"follow_us": "Бізді іздеңіз",
|
||||
"rights": "Барлық құқықтар сақталған."
|
||||
},
|
||||
"theme": {
|
||||
"light": "Ашық тема",
|
||||
"dark": "Қараңғы тема",
|
||||
"toggle": "Теманы ауыстыру"
|
||||
},
|
||||
"language": {
|
||||
"english": "English",
|
||||
"korean": "한국어",
|
||||
"russian": "Русский",
|
||||
"kazakh": "Қазақша"
|
||||
},
|
||||
"common": {
|
||||
"loading": "Жүктелуде...",
|
||||
"error": "Қате орын алды",
|
||||
"success": "Сәтті",
|
||||
"view_more": "Көбірек көру",
|
||||
"back": "Артқа",
|
||||
"next": "Келесі",
|
||||
"previous": "Алдыңғы"
|
||||
}
|
||||
}
|
||||
223
.history/locales/kk_20251019181629.json
Normal file
223
.history/locales/kk_20251019181629.json
Normal file
@@ -0,0 +1,223 @@
|
||||
{
|
||||
"navigation": {
|
||||
"home": "Басты бет",
|
||||
"about": "Біз туралы",
|
||||
"services": "Қызметтер",
|
||||
"portfolio": "Портфолио",
|
||||
"contact": "Байланыс",
|
||||
"calculator": "Калькулятор",
|
||||
"admin": "Админ"
|
||||
},
|
||||
"hero": {
|
||||
"title": "Ақылды Технологиялық",
|
||||
"subtitle": "Шешімдер",
|
||||
"description": "Инновациялық веб-әзірлеу, мобильді қосымшалар, UI/UX дизайн арқылы бизнестің цифрлық трансформациясын жүргіземіз",
|
||||
"cta_primary": "Жобаны бастау",
|
||||
"cta_secondary": "Портфолионы көру"
|
||||
},
|
||||
"services": {
|
||||
"title": "Біздің",
|
||||
"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": "Барлық қызметтерді көру"
|
||||
},
|
||||
"portfolio": {
|
||||
"title": "Соңғы",
|
||||
"title_highlight": "Жобалар",
|
||||
"description": "Тұтынушылардың табысы үшін аяқталған жобаларды тексеріңіз",
|
||||
"view_details": "Толығырақ",
|
||||
"view_all": "Барлық портфолионы көру"
|
||||
},
|
||||
"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": "Кеңес беру үшін өтініш беру"
|
||||
}
|
||||
},
|
||||
"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": "SmartSolTech",
|
||||
"description": "Инновацияны басқаратын цифрлық шешімдер маманы",
|
||||
"quick_links": "Жылдам сілтемелер",
|
||||
"services": "Қызметтер",
|
||||
"contact_info": "Байланыс ақпараты",
|
||||
"follow_us": "Бізді іздеңіз",
|
||||
"rights": "Барлық құқықтар сақталған."
|
||||
},
|
||||
"theme": {
|
||||
"light": "Ашық тема",
|
||||
"dark": "Қараңғы тема",
|
||||
"toggle": "Теманы ауыстыру"
|
||||
},
|
||||
"language": {
|
||||
"english": "English",
|
||||
"korean": "한국어",
|
||||
"russian": "Русский",
|
||||
"kazakh": "Қазақша"
|
||||
},
|
||||
"common": {
|
||||
"loading": "Жүктелуде...",
|
||||
"error": "Қате орын алды",
|
||||
"success": "Сәтті",
|
||||
"view_more": "Көбірек көру",
|
||||
"back": "Артқа",
|
||||
"next": "Келесі",
|
||||
"previous": "Алдыңғы"
|
||||
}
|
||||
}
|
||||
173
.history/locales/ko_20251019171450.json
Normal file
173
.history/locales/ko_20251019171450.json
Normal file
@@ -0,0 +1,173 @@
|
||||
{
|
||||
"navigation": {
|
||||
"home": "홈",
|
||||
"about": "회사소개",
|
||||
"services": "서비스",
|
||||
"portfolio": "포트폴리오",
|
||||
"contact": "연락처",
|
||||
"calculator": "견적계산기",
|
||||
"admin": "관리자"
|
||||
},
|
||||
"hero": {
|
||||
"title": "Smart Technology",
|
||||
"subtitle": "Solutions",
|
||||
"description": "혁신적인 웹 개발, 모바일 앱, UI/UX 디자인으로 비즈니스의 디지털 전환을 이끌어갑니다",
|
||||
"cta_primary": "프로젝트 시작하기",
|
||||
"cta_secondary": "포트폴리오 보기"
|
||||
},
|
||||
"services": {
|
||||
"title": "Our",
|
||||
"title_highlight": "Services",
|
||||
"description": "최신 기술과 창의적인 아이디어로 완성하는 디지털 솔루션",
|
||||
"web_development": {
|
||||
"title": "웹 개발",
|
||||
"description": "현대적이고 반응형 웹사이트 및 웹 애플리케이션 개발",
|
||||
"price": "₩500,000~"
|
||||
},
|
||||
"mobile_app": {
|
||||
"title": "모바일 앱",
|
||||
"description": "iOS와 Android를 위한 네이티브 및 크로스플랫폼 앱",
|
||||
"price": "₩800,000~"
|
||||
},
|
||||
"ui_ux_design": {
|
||||
"title": "UI/UX 디자인",
|
||||
"description": "사용자 중심의 직관적이고 아름다운 인터페이스 디자인",
|
||||
"price": "₩300,000~"
|
||||
},
|
||||
"digital_marketing": {
|
||||
"title": "디지털 마케팅",
|
||||
"description": "SEO, 소셜미디어, 온라인 광고를 통한 디지털 마케팅",
|
||||
"price": "₩200,000~"
|
||||
},
|
||||
"view_all": "모든 서비스 보기"
|
||||
},
|
||||
"portfolio": {
|
||||
"title": "Recent",
|
||||
"title_highlight": "Projects",
|
||||
"description": "고객의 성공을 위해 완성한 프로젝트들을 확인해보세요",
|
||||
"view_details": "자세히 보기",
|
||||
"view_all": "전체 포트폴리오 보기"
|
||||
},
|
||||
"calculator": {
|
||||
"title": "프로젝트 견적을 확인해보세요",
|
||||
"description": "원하는 서비스와 요구사항을 선택하면 실시간으로 견적을 계산해드립니다",
|
||||
"cta": "견적 계산기 사용하기"
|
||||
},
|
||||
"contact": {
|
||||
"ready_title": "프로젝트를 시작할 준비가 되셨나요?",
|
||||
"ready_description": "아이디어를 현실로 만들어보세요. 전문가들이 최고의 솔루션을 제공합니다.",
|
||||
"phone_consultation": "전화 상담",
|
||||
"email_inquiry": "이메일 문의",
|
||||
"telegram_chat": "텔레그램 채팅",
|
||||
"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": "상담 신청하기"
|
||||
}
|
||||
},
|
||||
"about": {
|
||||
"hero_title": "About",
|
||||
"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": "Core",
|
||||
"title_highlight": "Values",
|
||||
"description": "SmartSolTech가 추구하는 핵심 가치들",
|
||||
"innovation": {
|
||||
"title": "혁신 (Innovation)",
|
||||
"description": "끊임없는 연구개발과 최신 기술 도입으로 혁신적인 솔루션을 제공합니다."
|
||||
},
|
||||
"collaboration": {
|
||||
"title": "협력 (Collaboration)",
|
||||
"description": "고객과의 긴밀한 소통과 협력을 통해 최상의 결과를 만들어냅니다."
|
||||
},
|
||||
"quality": {
|
||||
"title": "품질 (Quality)",
|
||||
"description": "높은 품질 기준을 유지하며 고객이 만족할 수 있는 완성도 높은 제품을 제공합니다."
|
||||
},
|
||||
"growth": {
|
||||
"title": "성장 (Growth)",
|
||||
"description": "고객과 함께 성장하며, 지속적인 학습과 발전을 추구합니다."
|
||||
}
|
||||
},
|
||||
"team": {
|
||||
"title": "Our",
|
||||
"title_highlight": "Team",
|
||||
"description": "전문성과 열정을 갖춘 SmartSolTech 팀을 소개합니다"
|
||||
},
|
||||
"tech_stack": {
|
||||
"title": "Technology",
|
||||
"title_highlight": "Stack",
|
||||
"description": "최신 기술과 검증된 도구들로 최고의 솔루션을 제공합니다",
|
||||
"frontend": "Frontend",
|
||||
"backend": "Backend",
|
||||
"mobile": "Mobile"
|
||||
},
|
||||
"cta": {
|
||||
"title": "함께 성공하는 파트너가 되어보세요",
|
||||
"description": "SmartSolTech와 함께 여러분의 비즈니스를 다음 단계로 발전시켜보세요",
|
||||
"partnership": "파트너십 문의",
|
||||
"portfolio": "포트폴리오 보기"
|
||||
}
|
||||
},
|
||||
"footer": {
|
||||
"company": "SmartSolTech",
|
||||
"description": "혁신을 이끌어가는 디지털 솔루션 전문 기업",
|
||||
"quick_links": "빠른 링크",
|
||||
"services": "서비스",
|
||||
"contact_info": "연락처 정보",
|
||||
"follow_us": "팔로우하기",
|
||||
"rights": "모든 권리 보유."
|
||||
},
|
||||
"theme": {
|
||||
"light": "라이트 테마",
|
||||
"dark": "다크 테마",
|
||||
"toggle": "테마 전환"
|
||||
},
|
||||
"language": {
|
||||
"english": "English",
|
||||
"korean": "한국어",
|
||||
"russian": "Русский",
|
||||
"kazakh": "Қазақша"
|
||||
},
|
||||
"common": {
|
||||
"loading": "로딩 중...",
|
||||
"error": "오류가 발생했습니다",
|
||||
"success": "성공",
|
||||
"view_more": "더 보기",
|
||||
"back": "뒤로",
|
||||
"next": "다음",
|
||||
"previous": "이전"
|
||||
}
|
||||
}
|
||||
173
.history/locales/ko_20251019171645.json
Normal file
173
.history/locales/ko_20251019171645.json
Normal file
@@ -0,0 +1,173 @@
|
||||
{
|
||||
"navigation": {
|
||||
"home": "홈",
|
||||
"about": "회사소개",
|
||||
"services": "서비스",
|
||||
"portfolio": "포트폴리오",
|
||||
"contact": "연락처",
|
||||
"calculator": "견적계산기",
|
||||
"admin": "관리자"
|
||||
},
|
||||
"hero": {
|
||||
"title": "Smart Technology",
|
||||
"subtitle": "Solutions",
|
||||
"description": "혁신적인 웹 개발, 모바일 앱, UI/UX 디자인으로 비즈니스의 디지털 전환을 이끌어갑니다",
|
||||
"cta_primary": "프로젝트 시작하기",
|
||||
"cta_secondary": "포트폴리오 보기"
|
||||
},
|
||||
"services": {
|
||||
"title": "Our",
|
||||
"title_highlight": "Services",
|
||||
"description": "최신 기술과 창의적인 아이디어로 완성하는 디지털 솔루션",
|
||||
"web_development": {
|
||||
"title": "웹 개발",
|
||||
"description": "현대적이고 반응형 웹사이트 및 웹 애플리케이션 개발",
|
||||
"price": "₩500,000~"
|
||||
},
|
||||
"mobile_app": {
|
||||
"title": "모바일 앱",
|
||||
"description": "iOS와 Android를 위한 네이티브 및 크로스플랫폼 앱",
|
||||
"price": "₩800,000~"
|
||||
},
|
||||
"ui_ux_design": {
|
||||
"title": "UI/UX 디자인",
|
||||
"description": "사용자 중심의 직관적이고 아름다운 인터페이스 디자인",
|
||||
"price": "₩300,000~"
|
||||
},
|
||||
"digital_marketing": {
|
||||
"title": "디지털 마케팅",
|
||||
"description": "SEO, 소셜미디어, 온라인 광고를 통한 디지털 마케팅",
|
||||
"price": "₩200,000~"
|
||||
},
|
||||
"view_all": "모든 서비스 보기"
|
||||
},
|
||||
"portfolio": {
|
||||
"title": "Recent",
|
||||
"title_highlight": "Projects",
|
||||
"description": "고객의 성공을 위해 완성한 프로젝트들을 확인해보세요",
|
||||
"view_details": "자세히 보기",
|
||||
"view_all": "전체 포트폴리오 보기"
|
||||
},
|
||||
"calculator": {
|
||||
"title": "프로젝트 견적을 확인해보세요",
|
||||
"description": "원하는 서비스와 요구사항을 선택하면 실시간으로 견적을 계산해드립니다",
|
||||
"cta": "견적 계산기 사용하기"
|
||||
},
|
||||
"contact": {
|
||||
"ready_title": "프로젝트를 시작할 준비가 되셨나요?",
|
||||
"ready_description": "아이디어를 현실로 만들어보세요. 전문가들이 최고의 솔루션을 제공합니다.",
|
||||
"phone_consultation": "전화 상담",
|
||||
"email_inquiry": "이메일 문의",
|
||||
"telegram_chat": "텔레그램 채팅",
|
||||
"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": "상담 신청하기"
|
||||
}
|
||||
},
|
||||
"about": {
|
||||
"hero_title": "About",
|
||||
"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": "Core",
|
||||
"title_highlight": "Values",
|
||||
"description": "SmartSolTech가 추구하는 핵심 가치들",
|
||||
"innovation": {
|
||||
"title": "혁신 (Innovation)",
|
||||
"description": "끊임없는 연구개발과 최신 기술 도입으로 혁신적인 솔루션을 제공합니다."
|
||||
},
|
||||
"collaboration": {
|
||||
"title": "협력 (Collaboration)",
|
||||
"description": "고객과의 긴밀한 소통과 협력을 통해 최상의 결과를 만들어냅니다."
|
||||
},
|
||||
"quality": {
|
||||
"title": "품질 (Quality)",
|
||||
"description": "높은 품질 기준을 유지하며 고객이 만족할 수 있는 완성도 높은 제품을 제공합니다."
|
||||
},
|
||||
"growth": {
|
||||
"title": "성장 (Growth)",
|
||||
"description": "고객과 함께 성장하며, 지속적인 학습과 발전을 추구합니다."
|
||||
}
|
||||
},
|
||||
"team": {
|
||||
"title": "Our",
|
||||
"title_highlight": "Team",
|
||||
"description": "전문성과 열정을 갖춘 SmartSolTech 팀을 소개합니다"
|
||||
},
|
||||
"tech_stack": {
|
||||
"title": "Technology",
|
||||
"title_highlight": "Stack",
|
||||
"description": "최신 기술과 검증된 도구들로 최고의 솔루션을 제공합니다",
|
||||
"frontend": "Frontend",
|
||||
"backend": "Backend",
|
||||
"mobile": "Mobile"
|
||||
},
|
||||
"cta": {
|
||||
"title": "함께 성공하는 파트너가 되어보세요",
|
||||
"description": "SmartSolTech와 함께 여러분의 비즈니스를 다음 단계로 발전시켜보세요",
|
||||
"partnership": "파트너십 문의",
|
||||
"portfolio": "포트폴리오 보기"
|
||||
}
|
||||
},
|
||||
"footer": {
|
||||
"company": "SmartSolTech",
|
||||
"description": "혁신을 이끌어가는 디지털 솔루션 전문 기업",
|
||||
"quick_links": "빠른 링크",
|
||||
"services": "서비스",
|
||||
"contact_info": "연락처 정보",
|
||||
"follow_us": "팔로우하기",
|
||||
"rights": "모든 권리 보유."
|
||||
},
|
||||
"theme": {
|
||||
"light": "라이트 테마",
|
||||
"dark": "다크 테마",
|
||||
"toggle": "테마 전환"
|
||||
},
|
||||
"language": {
|
||||
"english": "English",
|
||||
"korean": "한국어",
|
||||
"russian": "Русский",
|
||||
"kazakh": "Қазақша"
|
||||
},
|
||||
"common": {
|
||||
"loading": "로딩 중...",
|
||||
"error": "오류가 발생했습니다",
|
||||
"success": "성공",
|
||||
"view_more": "더 보기",
|
||||
"back": "뒤로",
|
||||
"next": "다음",
|
||||
"previous": "이전"
|
||||
}
|
||||
}
|
||||
227
.history/locales/ko_20251019181428.json
Normal file
227
.history/locales/ko_20251019181428.json
Normal file
@@ -0,0 +1,227 @@
|
||||
{
|
||||
"navigation": {
|
||||
"home": "홈",
|
||||
"about": "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": "계산하기"
|
||||
},",
|
||||
"services": "서비스",
|
||||
"portfolio": "포트폴리오",
|
||||
"contact": "연락처",
|
||||
"calculator": "견적계산기",
|
||||
"admin": "관리자"
|
||||
},
|
||||
"hero": {
|
||||
"title": "Smart Technology",
|
||||
"subtitle": "Solutions",
|
||||
"description": "혁신적인 웹 개발, 모바일 앱, UI/UX 디자인으로 비즈니스의 디지털 전환을 이끌어갑니다",
|
||||
"cta_primary": "프로젝트 시작하기",
|
||||
"cta_secondary": "포트폴리오 보기"
|
||||
},
|
||||
"services": {
|
||||
"title": "Our",
|
||||
"title_highlight": "Services",
|
||||
"description": "최신 기술과 창의적인 아이디어로 완성하는 디지털 솔루션",
|
||||
"web_development": {
|
||||
"title": "웹 개발",
|
||||
"description": "현대적이고 반응형 웹사이트 및 웹 애플리케이션 개발",
|
||||
"price": "₩500,000~"
|
||||
},
|
||||
"mobile_app": {
|
||||
"title": "모바일 앱",
|
||||
"description": "iOS와 Android를 위한 네이티브 및 크로스플랫폼 앱",
|
||||
"price": "₩800,000~"
|
||||
},
|
||||
"ui_ux_design": {
|
||||
"title": "UI/UX 디자인",
|
||||
"description": "사용자 중심의 직관적이고 아름다운 인터페이스 디자인",
|
||||
"price": "₩300,000~"
|
||||
},
|
||||
"digital_marketing": {
|
||||
"title": "디지털 마케팅",
|
||||
"description": "SEO, 소셜미디어, 온라인 광고를 통한 디지털 마케팅",
|
||||
"price": "₩200,000~"
|
||||
},
|
||||
"view_all": "모든 서비스 보기"
|
||||
},
|
||||
"portfolio": {
|
||||
"title": "Recent",
|
||||
"title_highlight": "Projects",
|
||||
"description": "고객의 성공을 위해 완성한 프로젝트들을 확인해보세요",
|
||||
"view_details": "자세히 보기",
|
||||
"view_all": "전체 포트폴리오 보기"
|
||||
},
|
||||
"calculator": {
|
||||
"title": "프로젝트 견적을 확인해보세요",
|
||||
"description": "원하는 서비스와 요구사항을 선택하면 실시간으로 견적을 계산해드립니다",
|
||||
"cta": "견적 계산기 사용하기"
|
||||
},
|
||||
"contact": {
|
||||
"ready_title": "프로젝트를 시작할 준비가 되셨나요?",
|
||||
"ready_description": "아이디어를 현실로 만들어보세요. 전문가들이 최고의 솔루션을 제공합니다.",
|
||||
"phone_consultation": "전화 상담",
|
||||
"email_inquiry": "이메일 문의",
|
||||
"telegram_chat": "텔레그램 채팅",
|
||||
"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": "상담 신청하기"
|
||||
}
|
||||
},
|
||||
"about": {
|
||||
"hero_title": "About",
|
||||
"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": "Core",
|
||||
"title_highlight": "Values",
|
||||
"description": "SmartSolTech가 추구하는 핵심 가치들",
|
||||
"innovation": {
|
||||
"title": "혁신 (Innovation)",
|
||||
"description": "끊임없는 연구개발과 최신 기술 도입으로 혁신적인 솔루션을 제공합니다."
|
||||
},
|
||||
"collaboration": {
|
||||
"title": "협력 (Collaboration)",
|
||||
"description": "고객과의 긴밀한 소통과 협력을 통해 최상의 결과를 만들어냅니다."
|
||||
},
|
||||
"quality": {
|
||||
"title": "품질 (Quality)",
|
||||
"description": "높은 품질 기준을 유지하며 고객이 만족할 수 있는 완성도 높은 제품을 제공합니다."
|
||||
},
|
||||
"growth": {
|
||||
"title": "성장 (Growth)",
|
||||
"description": "고객과 함께 성장하며, 지속적인 학습과 발전을 추구합니다."
|
||||
}
|
||||
},
|
||||
"team": {
|
||||
"title": "Our",
|
||||
"title_highlight": "Team",
|
||||
"description": "전문성과 열정을 갖춘 SmartSolTech 팀을 소개합니다"
|
||||
},
|
||||
"tech_stack": {
|
||||
"title": "Technology",
|
||||
"title_highlight": "Stack",
|
||||
"description": "최신 기술과 검증된 도구들로 최고의 솔루션을 제공합니다",
|
||||
"frontend": "Frontend",
|
||||
"backend": "Backend",
|
||||
"mobile": "Mobile"
|
||||
},
|
||||
"cta": {
|
||||
"title": "함께 성공하는 파트너가 되어보세요",
|
||||
"description": "SmartSolTech와 함께 여러분의 비즈니스를 다음 단계로 발전시켜보세요",
|
||||
"partnership": "파트너십 문의",
|
||||
"portfolio": "포트폴리오 보기"
|
||||
}
|
||||
},
|
||||
"footer": {
|
||||
"company": "SmartSolTech",
|
||||
"description": "혁신을 이끌어가는 디지털 솔루션 전문 기업",
|
||||
"quick_links": "빠른 링크",
|
||||
"services": "서비스",
|
||||
"contact_info": "연락처 정보",
|
||||
"follow_us": "팔로우하기",
|
||||
"rights": "모든 권리 보유."
|
||||
},
|
||||
"theme": {
|
||||
"light": "라이트 테마",
|
||||
"dark": "다크 테마",
|
||||
"toggle": "테마 전환"
|
||||
},
|
||||
"language": {
|
||||
"english": "English",
|
||||
"korean": "한국어",
|
||||
"russian": "Русский",
|
||||
"kazakh": "Қазақша"
|
||||
},
|
||||
"common": {
|
||||
"loading": "로딩 중...",
|
||||
"error": "오류가 발생했습니다",
|
||||
"success": "성공",
|
||||
"view_more": "더 보기",
|
||||
"back": "뒤로",
|
||||
"next": "다음",
|
||||
"previous": "이전"
|
||||
}
|
||||
}
|
||||
230
.history/locales/ko_20251019181450.json
Normal file
230
.history/locales/ko_20251019181450.json
Normal file
@@ -0,0 +1,230 @@
|
||||
{
|
||||
"navigation": {
|
||||
"home": "홈",
|
||||
"about": "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": "계산하기"
|
||||
},
|
||||
"nav": {
|
||||
"home": "홈",
|
||||
"about": "회사소개",
|
||||
"services": "서비스",
|
||||
"portfolio": "포트폴리오",
|
||||
"contact": "연락처",
|
||||
"calculator": "견적계산기",
|
||||
"admin": "관리자"
|
||||
},
|
||||
"hero": {
|
||||
"title": "Smart Technology",
|
||||
"subtitle": "Solutions",
|
||||
"description": "혁신적인 웹 개발, 모바일 앱, UI/UX 디자인으로 비즈니스의 디지털 전환을 이끌어갑니다",
|
||||
"cta_primary": "프로젝트 시작하기",
|
||||
"cta_secondary": "포트폴리오 보기"
|
||||
},
|
||||
"services": {
|
||||
"title": "Our",
|
||||
"title_highlight": "Services",
|
||||
"description": "최신 기술과 창의적인 아이디어로 완성하는 디지털 솔루션",
|
||||
"web_development": {
|
||||
"title": "웹 개발",
|
||||
"description": "현대적이고 반응형 웹사이트 및 웹 애플리케이션 개발",
|
||||
"price": "₩500,000~"
|
||||
},
|
||||
"mobile_app": {
|
||||
"title": "모바일 앱",
|
||||
"description": "iOS와 Android를 위한 네이티브 및 크로스플랫폼 앱",
|
||||
"price": "₩800,000~"
|
||||
},
|
||||
"ui_ux_design": {
|
||||
"title": "UI/UX 디자인",
|
||||
"description": "사용자 중심의 직관적이고 아름다운 인터페이스 디자인",
|
||||
"price": "₩300,000~"
|
||||
},
|
||||
"digital_marketing": {
|
||||
"title": "디지털 마케팅",
|
||||
"description": "SEO, 소셜미디어, 온라인 광고를 통한 디지털 마케팅",
|
||||
"price": "₩200,000~"
|
||||
},
|
||||
"view_all": "모든 서비스 보기"
|
||||
},
|
||||
"portfolio": {
|
||||
"title": "Recent",
|
||||
"title_highlight": "Projects",
|
||||
"description": "고객의 성공을 위해 완성한 프로젝트들을 확인해보세요",
|
||||
"view_details": "자세히 보기",
|
||||
"view_all": "전체 포트폴리오 보기"
|
||||
},
|
||||
"calculator": {
|
||||
"title": "프로젝트 견적을 확인해보세요",
|
||||
"description": "원하는 서비스와 요구사항을 선택하면 실시간으로 견적을 계산해드립니다",
|
||||
"cta": "견적 계산기 사용하기"
|
||||
},
|
||||
"contact": {
|
||||
"ready_title": "프로젝트를 시작할 준비가 되셨나요?",
|
||||
"ready_description": "아이디어를 현실로 만들어보세요. 전문가들이 최고의 솔루션을 제공합니다.",
|
||||
"phone_consultation": "전화 상담",
|
||||
"email_inquiry": "이메일 문의",
|
||||
"telegram_chat": "텔레그램 채팅",
|
||||
"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": "상담 신청하기"
|
||||
}
|
||||
},
|
||||
"about": {
|
||||
"hero_title": "About",
|
||||
"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": "Core",
|
||||
"title_highlight": "Values",
|
||||
"description": "SmartSolTech가 추구하는 핵심 가치들",
|
||||
"innovation": {
|
||||
"title": "혁신 (Innovation)",
|
||||
"description": "끊임없는 연구개발과 최신 기술 도입으로 혁신적인 솔루션을 제공합니다."
|
||||
},
|
||||
"collaboration": {
|
||||
"title": "협력 (Collaboration)",
|
||||
"description": "고객과의 긴밀한 소통과 협력을 통해 최상의 결과를 만들어냅니다."
|
||||
},
|
||||
"quality": {
|
||||
"title": "품질 (Quality)",
|
||||
"description": "높은 품질 기준을 유지하며 고객이 만족할 수 있는 완성도 높은 제품을 제공합니다."
|
||||
},
|
||||
"growth": {
|
||||
"title": "성장 (Growth)",
|
||||
"description": "고객과 함께 성장하며, 지속적인 학습과 발전을 추구합니다."
|
||||
}
|
||||
},
|
||||
"team": {
|
||||
"title": "Our",
|
||||
"title_highlight": "Team",
|
||||
"description": "전문성과 열정을 갖춘 SmartSolTech 팀을 소개합니다"
|
||||
},
|
||||
"tech_stack": {
|
||||
"title": "Technology",
|
||||
"title_highlight": "Stack",
|
||||
"description": "최신 기술과 검증된 도구들로 최고의 솔루션을 제공합니다",
|
||||
"frontend": "Frontend",
|
||||
"backend": "Backend",
|
||||
"mobile": "Mobile"
|
||||
},
|
||||
"cta": {
|
||||
"title": "함께 성공하는 파트너가 되어보세요",
|
||||
"description": "SmartSolTech와 함께 여러분의 비즈니스를 다음 단계로 발전시켜보세요",
|
||||
"partnership": "파트너십 문의",
|
||||
"portfolio": "포트폴리오 보기"
|
||||
}
|
||||
},
|
||||
"footer": {
|
||||
"company": "SmartSolTech",
|
||||
"description": "혁신을 이끌어가는 디지털 솔루션 전문 기업",
|
||||
"quick_links": "빠른 링크",
|
||||
"services": "서비스",
|
||||
"contact_info": "연락처 정보",
|
||||
"follow_us": "팔로우하기",
|
||||
"rights": "모든 권리 보유."
|
||||
},
|
||||
"theme": {
|
||||
"light": "라이트 테마",
|
||||
"dark": "다크 테마",
|
||||
"toggle": "테마 전환"
|
||||
},
|
||||
"language": {
|
||||
"english": "English",
|
||||
"korean": "한국어",
|
||||
"russian": "Русский",
|
||||
"kazakh": "Қазақша"
|
||||
},
|
||||
"common": {
|
||||
"loading": "로딩 중...",
|
||||
"error": "오류가 발생했습니다",
|
||||
"success": "성공",
|
||||
"view_more": "더 보기",
|
||||
"back": "뒤로",
|
||||
"next": "다음",
|
||||
"previous": "이전"
|
||||
}
|
||||
}
|
||||
230
.history/locales/ko_20251019181629.json
Normal file
230
.history/locales/ko_20251019181629.json
Normal file
@@ -0,0 +1,230 @@
|
||||
{
|
||||
"navigation": {
|
||||
"home": "홈",
|
||||
"about": "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": "계산하기"
|
||||
},
|
||||
"nav": {
|
||||
"home": "홈",
|
||||
"about": "회사소개",
|
||||
"services": "서비스",
|
||||
"portfolio": "포트폴리오",
|
||||
"contact": "연락처",
|
||||
"calculator": "견적계산기",
|
||||
"admin": "관리자"
|
||||
},
|
||||
"hero": {
|
||||
"title": "Smart Technology",
|
||||
"subtitle": "Solutions",
|
||||
"description": "혁신적인 웹 개발, 모바일 앱, UI/UX 디자인으로 비즈니스의 디지털 전환을 이끌어갑니다",
|
||||
"cta_primary": "프로젝트 시작하기",
|
||||
"cta_secondary": "포트폴리오 보기"
|
||||
},
|
||||
"services": {
|
||||
"title": "Our",
|
||||
"title_highlight": "Services",
|
||||
"description": "최신 기술과 창의적인 아이디어로 완성하는 디지털 솔루션",
|
||||
"web_development": {
|
||||
"title": "웹 개발",
|
||||
"description": "현대적이고 반응형 웹사이트 및 웹 애플리케이션 개발",
|
||||
"price": "₩500,000~"
|
||||
},
|
||||
"mobile_app": {
|
||||
"title": "모바일 앱",
|
||||
"description": "iOS와 Android를 위한 네이티브 및 크로스플랫폼 앱",
|
||||
"price": "₩800,000~"
|
||||
},
|
||||
"ui_ux_design": {
|
||||
"title": "UI/UX 디자인",
|
||||
"description": "사용자 중심의 직관적이고 아름다운 인터페이스 디자인",
|
||||
"price": "₩300,000~"
|
||||
},
|
||||
"digital_marketing": {
|
||||
"title": "디지털 마케팅",
|
||||
"description": "SEO, 소셜미디어, 온라인 광고를 통한 디지털 마케팅",
|
||||
"price": "₩200,000~"
|
||||
},
|
||||
"view_all": "모든 서비스 보기"
|
||||
},
|
||||
"portfolio": {
|
||||
"title": "Recent",
|
||||
"title_highlight": "Projects",
|
||||
"description": "고객의 성공을 위해 완성한 프로젝트들을 확인해보세요",
|
||||
"view_details": "자세히 보기",
|
||||
"view_all": "전체 포트폴리오 보기"
|
||||
},
|
||||
"calculator": {
|
||||
"title": "프로젝트 견적을 확인해보세요",
|
||||
"description": "원하는 서비스와 요구사항을 선택하면 실시간으로 견적을 계산해드립니다",
|
||||
"cta": "견적 계산기 사용하기"
|
||||
},
|
||||
"contact": {
|
||||
"ready_title": "프로젝트를 시작할 준비가 되셨나요?",
|
||||
"ready_description": "아이디어를 현실로 만들어보세요. 전문가들이 최고의 솔루션을 제공합니다.",
|
||||
"phone_consultation": "전화 상담",
|
||||
"email_inquiry": "이메일 문의",
|
||||
"telegram_chat": "텔레그램 채팅",
|
||||
"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": "상담 신청하기"
|
||||
}
|
||||
},
|
||||
"about": {
|
||||
"hero_title": "About",
|
||||
"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": "Core",
|
||||
"title_highlight": "Values",
|
||||
"description": "SmartSolTech가 추구하는 핵심 가치들",
|
||||
"innovation": {
|
||||
"title": "혁신 (Innovation)",
|
||||
"description": "끊임없는 연구개발과 최신 기술 도입으로 혁신적인 솔루션을 제공합니다."
|
||||
},
|
||||
"collaboration": {
|
||||
"title": "협력 (Collaboration)",
|
||||
"description": "고객과의 긴밀한 소통과 협력을 통해 최상의 결과를 만들어냅니다."
|
||||
},
|
||||
"quality": {
|
||||
"title": "품질 (Quality)",
|
||||
"description": "높은 품질 기준을 유지하며 고객이 만족할 수 있는 완성도 높은 제품을 제공합니다."
|
||||
},
|
||||
"growth": {
|
||||
"title": "성장 (Growth)",
|
||||
"description": "고객과 함께 성장하며, 지속적인 학습과 발전을 추구합니다."
|
||||
}
|
||||
},
|
||||
"team": {
|
||||
"title": "Our",
|
||||
"title_highlight": "Team",
|
||||
"description": "전문성과 열정을 갖춘 SmartSolTech 팀을 소개합니다"
|
||||
},
|
||||
"tech_stack": {
|
||||
"title": "Technology",
|
||||
"title_highlight": "Stack",
|
||||
"description": "최신 기술과 검증된 도구들로 최고의 솔루션을 제공합니다",
|
||||
"frontend": "Frontend",
|
||||
"backend": "Backend",
|
||||
"mobile": "Mobile"
|
||||
},
|
||||
"cta": {
|
||||
"title": "함께 성공하는 파트너가 되어보세요",
|
||||
"description": "SmartSolTech와 함께 여러분의 비즈니스를 다음 단계로 발전시켜보세요",
|
||||
"partnership": "파트너십 문의",
|
||||
"portfolio": "포트폴리오 보기"
|
||||
}
|
||||
},
|
||||
"footer": {
|
||||
"company": "SmartSolTech",
|
||||
"description": "혁신을 이끌어가는 디지털 솔루션 전문 기업",
|
||||
"quick_links": "빠른 링크",
|
||||
"services": "서비스",
|
||||
"contact_info": "연락처 정보",
|
||||
"follow_us": "팔로우하기",
|
||||
"rights": "모든 권리 보유."
|
||||
},
|
||||
"theme": {
|
||||
"light": "라이트 테마",
|
||||
"dark": "다크 테마",
|
||||
"toggle": "테마 전환"
|
||||
},
|
||||
"language": {
|
||||
"english": "English",
|
||||
"korean": "한국어",
|
||||
"russian": "Русский",
|
||||
"kazakh": "Қазақша"
|
||||
},
|
||||
"common": {
|
||||
"loading": "로딩 중...",
|
||||
"error": "오류가 발생했습니다",
|
||||
"success": "성공",
|
||||
"view_more": "더 보기",
|
||||
"back": "뒤로",
|
||||
"next": "다음",
|
||||
"previous": "이전"
|
||||
}
|
||||
}
|
||||
196
.history/locales/ko_20251019182536.json
Normal file
196
.history/locales/ko_20251019182536.json
Normal file
@@ -0,0 +1,196 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
196
.history/locales/ko_20251019182628.json
Normal file
196
.history/locales/ko_20251019182628.json
Normal file
@@ -0,0 +1,196 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
173
.history/locales/ru_20251019171530.json
Normal file
173
.history/locales/ru_20251019171530.json
Normal file
@@ -0,0 +1,173 @@
|
||||
{
|
||||
"navigation": {
|
||||
"home": "Главная",
|
||||
"about": "О нас",
|
||||
"services": "Услуги",
|
||||
"portfolio": "Портфолио",
|
||||
"contact": "Контакты",
|
||||
"calculator": "Калькулятор",
|
||||
"admin": "Админ"
|
||||
},
|
||||
"hero": {
|
||||
"title": "Умные Технологические",
|
||||
"subtitle": "Решения",
|
||||
"description": "Инновационная веб-разработка, мобильные приложения, UI/UX дизайн для цифровой трансформации вашего бизнеса",
|
||||
"cta_primary": "Начать проект",
|
||||
"cta_secondary": "Посмотреть портфолио"
|
||||
},
|
||||
"services": {
|
||||
"title": "Наши",
|
||||
"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": "Посмотреть все услуги"
|
||||
},
|
||||
"portfolio": {
|
||||
"title": "Недавние",
|
||||
"title_highlight": "Проекты",
|
||||
"description": "Ознакомьтесь с проектами, выполненными для успеха клиентов",
|
||||
"view_details": "Подробнее",
|
||||
"view_all": "Посмотреть все портфолио"
|
||||
},
|
||||
"calculator": {
|
||||
"title": "Проверьте стоимость вашего проекта",
|
||||
"description": "Выберите нужные услуги и требования для расчета стоимости в реальном времени",
|
||||
"cta": "Использовать калькулятор стоимости"
|
||||
},
|
||||
"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": "Подать заявку на консультацию"
|
||||
}
|
||||
},
|
||||
"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": "SmartSolTech",
|
||||
"description": "Специалист по цифровым решениям, ведущий инновации",
|
||||
"quick_links": "Быстрые ссылки",
|
||||
"services": "Услуги",
|
||||
"contact_info": "Контактная информация",
|
||||
"follow_us": "Подписывайтесь",
|
||||
"rights": "Все права защищены."
|
||||
},
|
||||
"theme": {
|
||||
"light": "Светлая тема",
|
||||
"dark": "Темная тема",
|
||||
"toggle": "Переключить тему"
|
||||
},
|
||||
"language": {
|
||||
"english": "English",
|
||||
"korean": "한국어",
|
||||
"russian": "Русский",
|
||||
"kazakh": "Қазақша"
|
||||
},
|
||||
"common": {
|
||||
"loading": "Загрузка...",
|
||||
"error": "Произошла ошибка",
|
||||
"success": "Успешно",
|
||||
"view_more": "Посмотреть еще",
|
||||
"back": "Назад",
|
||||
"next": "Далее",
|
||||
"previous": "Предыдущий"
|
||||
}
|
||||
}
|
||||
173
.history/locales/ru_20251019171645.json
Normal file
173
.history/locales/ru_20251019171645.json
Normal file
@@ -0,0 +1,173 @@
|
||||
{
|
||||
"navigation": {
|
||||
"home": "Главная",
|
||||
"about": "О нас",
|
||||
"services": "Услуги",
|
||||
"portfolio": "Портфолио",
|
||||
"contact": "Контакты",
|
||||
"calculator": "Калькулятор",
|
||||
"admin": "Админ"
|
||||
},
|
||||
"hero": {
|
||||
"title": "Умные Технологические",
|
||||
"subtitle": "Решения",
|
||||
"description": "Инновационная веб-разработка, мобильные приложения, UI/UX дизайн для цифровой трансформации вашего бизнеса",
|
||||
"cta_primary": "Начать проект",
|
||||
"cta_secondary": "Посмотреть портфолио"
|
||||
},
|
||||
"services": {
|
||||
"title": "Наши",
|
||||
"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": "Посмотреть все услуги"
|
||||
},
|
||||
"portfolio": {
|
||||
"title": "Недавние",
|
||||
"title_highlight": "Проекты",
|
||||
"description": "Ознакомьтесь с проектами, выполненными для успеха клиентов",
|
||||
"view_details": "Подробнее",
|
||||
"view_all": "Посмотреть все портфолио"
|
||||
},
|
||||
"calculator": {
|
||||
"title": "Проверьте стоимость вашего проекта",
|
||||
"description": "Выберите нужные услуги и требования для расчета стоимости в реальном времени",
|
||||
"cta": "Использовать калькулятор стоимости"
|
||||
},
|
||||
"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": "Подать заявку на консультацию"
|
||||
}
|
||||
},
|
||||
"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": "SmartSolTech",
|
||||
"description": "Специалист по цифровым решениям, ведущий инновации",
|
||||
"quick_links": "Быстрые ссылки",
|
||||
"services": "Услуги",
|
||||
"contact_info": "Контактная информация",
|
||||
"follow_us": "Подписывайтесь",
|
||||
"rights": "Все права защищены."
|
||||
},
|
||||
"theme": {
|
||||
"light": "Светлая тема",
|
||||
"dark": "Темная тема",
|
||||
"toggle": "Переключить тему"
|
||||
},
|
||||
"language": {
|
||||
"english": "English",
|
||||
"korean": "한국어",
|
||||
"russian": "Русский",
|
||||
"kazakh": "Қазақша"
|
||||
},
|
||||
"common": {
|
||||
"loading": "Загрузка...",
|
||||
"error": "Произошла ошибка",
|
||||
"success": "Успешно",
|
||||
"view_more": "Посмотреть еще",
|
||||
"back": "Назад",
|
||||
"next": "Далее",
|
||||
"previous": "Предыдущий"
|
||||
}
|
||||
}
|
||||
223
.history/locales/ru_20251019181401.json
Normal file
223
.history/locales/ru_20251019181401.json
Normal file
@@ -0,0 +1,223 @@
|
||||
{
|
||||
"navigation": {
|
||||
"home": "Главная",
|
||||
"about": "О нас",
|
||||
"services": "Услуги",
|
||||
"portfolio": "Портфолио",
|
||||
"contact": "Контакты",
|
||||
"calculator": "Калькулятор",
|
||||
"admin": "Админ"
|
||||
},
|
||||
"hero": {
|
||||
"title": "Умные Технологические",
|
||||
"subtitle": "Решения",
|
||||
"description": "Инновационная веб-разработка, мобильные приложения, UI/UX дизайн для цифровой трансформации вашего бизнеса",
|
||||
"cta_primary": "Начать проект",
|
||||
"cta_secondary": "Посмотреть портфолио"
|
||||
},
|
||||
"services": {
|
||||
"title": "Наши",
|
||||
"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": "Посмотреть все услуги"
|
||||
},
|
||||
"portfolio": {
|
||||
"title": "Недавние",
|
||||
"title_highlight": "Проекты",
|
||||
"description": "Ознакомьтесь с проектами, выполненными для успеха клиентов",
|
||||
"view_details": "Подробнее",
|
||||
"view_all": "Посмотреть все портфолио"
|
||||
},
|
||||
"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": "Подать заявку на консультацию"
|
||||
}
|
||||
},
|
||||
"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": "SmartSolTech",
|
||||
"description": "Специалист по цифровым решениям, ведущий инновации",
|
||||
"quick_links": "Быстрые ссылки",
|
||||
"services": "Услуги",
|
||||
"contact_info": "Контактная информация",
|
||||
"follow_us": "Подписывайтесь",
|
||||
"rights": "Все права защищены."
|
||||
},
|
||||
"theme": {
|
||||
"light": "Светлая тема",
|
||||
"dark": "Темная тема",
|
||||
"toggle": "Переключить тему"
|
||||
},
|
||||
"language": {
|
||||
"english": "English",
|
||||
"korean": "한국어",
|
||||
"russian": "Русский",
|
||||
"kazakh": "Қазақша"
|
||||
},
|
||||
"common": {
|
||||
"loading": "Загрузка...",
|
||||
"error": "Произошла ошибка",
|
||||
"success": "Успешно",
|
||||
"view_more": "Посмотреть еще",
|
||||
"back": "Назад",
|
||||
"next": "Далее",
|
||||
"previous": "Предыдущий"
|
||||
}
|
||||
}
|
||||
223
.history/locales/ru_20251019181629.json
Normal file
223
.history/locales/ru_20251019181629.json
Normal file
@@ -0,0 +1,223 @@
|
||||
{
|
||||
"navigation": {
|
||||
"home": "Главная",
|
||||
"about": "О нас",
|
||||
"services": "Услуги",
|
||||
"portfolio": "Портфолио",
|
||||
"contact": "Контакты",
|
||||
"calculator": "Калькулятор",
|
||||
"admin": "Админ"
|
||||
},
|
||||
"hero": {
|
||||
"title": "Умные Технологические",
|
||||
"subtitle": "Решения",
|
||||
"description": "Инновационная веб-разработка, мобильные приложения, UI/UX дизайн для цифровой трансформации вашего бизнеса",
|
||||
"cta_primary": "Начать проект",
|
||||
"cta_secondary": "Посмотреть портфолио"
|
||||
},
|
||||
"services": {
|
||||
"title": "Наши",
|
||||
"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": "Посмотреть все услуги"
|
||||
},
|
||||
"portfolio": {
|
||||
"title": "Недавние",
|
||||
"title_highlight": "Проекты",
|
||||
"description": "Ознакомьтесь с проектами, выполненными для успеха клиентов",
|
||||
"view_details": "Подробнее",
|
||||
"view_all": "Посмотреть все портфолио"
|
||||
},
|
||||
"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": "Подать заявку на консультацию"
|
||||
}
|
||||
},
|
||||
"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": "SmartSolTech",
|
||||
"description": "Специалист по цифровым решениям, ведущий инновации",
|
||||
"quick_links": "Быстрые ссылки",
|
||||
"services": "Услуги",
|
||||
"contact_info": "Контактная информация",
|
||||
"follow_us": "Подписывайтесь",
|
||||
"rights": "Все права защищены."
|
||||
},
|
||||
"theme": {
|
||||
"light": "Светлая тема",
|
||||
"dark": "Темная тема",
|
||||
"toggle": "Переключить тему"
|
||||
},
|
||||
"language": {
|
||||
"english": "English",
|
||||
"korean": "한국어",
|
||||
"russian": "Русский",
|
||||
"kazakh": "Қазақша"
|
||||
},
|
||||
"common": {
|
||||
"loading": "Загрузка...",
|
||||
"error": "Произошла ошибка",
|
||||
"success": "Успешно",
|
||||
"view_more": "Посмотреть еще",
|
||||
"back": "Назад",
|
||||
"next": "Далее",
|
||||
"previous": "Предыдущий"
|
||||
}
|
||||
}
|
||||
154
.history/middleware/auth_20251019163222.js
Normal file
154
.history/middleware/auth_20251019163222.js
Normal file
@@ -0,0 +1,154 @@
|
||||
const jwt = require('jsonwebtoken');
|
||||
const User = require('../models/User');
|
||||
|
||||
/**
|
||||
* Authentication middleware
|
||||
* Verifies JWT token and attaches user to request
|
||||
*/
|
||||
const authenticateToken = async (req, res, next) => {
|
||||
try {
|
||||
const authHeader = req.headers['authorization'];
|
||||
const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN
|
||||
|
||||
if (!token) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: 'Access token required'
|
||||
});
|
||||
}
|
||||
|
||||
const decoded = jwt.verify(token, process.env.JWT_SECRET);
|
||||
const user = await User.findById(decoded.userId).select('-password');
|
||||
|
||||
if (!user || !user.isActive) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: 'Invalid or inactive user'
|
||||
});
|
||||
}
|
||||
|
||||
req.user = user;
|
||||
next();
|
||||
} catch (error) {
|
||||
console.error('Token verification error:', error);
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
message: 'Invalid token'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Session-based authentication middleware
|
||||
* For web pages using sessions
|
||||
*/
|
||||
const authenticateSession = async (req, res, next) => {
|
||||
try {
|
||||
if (!req.session.userId) {
|
||||
req.flash('error', '로그인이 필요합니다.');
|
||||
return res.redirect('/auth/login');
|
||||
}
|
||||
|
||||
const user = await User.findById(req.session.userId).select('-password');
|
||||
|
||||
if (!user || !user.isActive) {
|
||||
req.session.destroy();
|
||||
req.flash('error', '유효하지 않은 사용자입니다.');
|
||||
return res.redirect('/auth/login');
|
||||
}
|
||||
|
||||
req.user = user;
|
||||
res.locals.user = user;
|
||||
next();
|
||||
} catch (error) {
|
||||
console.error('Session authentication error:', error);
|
||||
req.session.destroy();
|
||||
req.flash('error', '인증 오류가 발생했습니다.');
|
||||
return res.redirect('/auth/login');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Admin role middleware
|
||||
* Requires user to be authenticated and have admin role
|
||||
*/
|
||||
const requireAdmin = (req, res, next) => {
|
||||
if (!req.user) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: 'Authentication required'
|
||||
});
|
||||
}
|
||||
|
||||
if (req.user.role !== 'admin') {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
message: 'Admin access required'
|
||||
});
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
|
||||
/**
|
||||
* Admin session middleware for web pages
|
||||
*/
|
||||
const requireAdminSession = (req, res, next) => {
|
||||
if (!req.user) {
|
||||
req.flash('error', '로그인이 필요합니다.');
|
||||
return res.redirect('/auth/login');
|
||||
}
|
||||
|
||||
if (req.user.role !== 'admin') {
|
||||
req.flash('error', '관리자 권한이 필요합니다.');
|
||||
return res.redirect('/');
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
|
||||
/**
|
||||
* Optional authentication middleware
|
||||
* Attaches user if token exists but doesn't require it
|
||||
*/
|
||||
const optionalAuth = async (req, res, next) => {
|
||||
try {
|
||||
// Check session first
|
||||
if (req.session.userId) {
|
||||
const user = await User.findById(req.session.userId).select('-password');
|
||||
if (user && user.isActive) {
|
||||
req.user = user;
|
||||
res.locals.user = user;
|
||||
}
|
||||
}
|
||||
|
||||
// Check JWT token if no session
|
||||
if (!req.user) {
|
||||
const authHeader = req.headers['authorization'];
|
||||
const token = authHeader && authHeader.split(' ')[1];
|
||||
|
||||
if (token) {
|
||||
const decoded = jwt.verify(token, process.env.JWT_SECRET);
|
||||
const user = await User.findById(decoded.userId).select('-password');
|
||||
|
||||
if (user && user.isActive) {
|
||||
req.user = user;
|
||||
res.locals.user = user;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
next();
|
||||
} catch (error) {
|
||||
// Continue without authentication if token is invalid
|
||||
next();
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
authenticateToken,
|
||||
authenticateSession,
|
||||
requireAdmin,
|
||||
requireAdminSession,
|
||||
optionalAuth
|
||||
};
|
||||
154
.history/middleware/auth_20251019163807.js
Normal file
154
.history/middleware/auth_20251019163807.js
Normal file
@@ -0,0 +1,154 @@
|
||||
const jwt = require('jsonwebtoken');
|
||||
const User = require('../models/User');
|
||||
|
||||
/**
|
||||
* Authentication middleware
|
||||
* Verifies JWT token and attaches user to request
|
||||
*/
|
||||
const authenticateToken = async (req, res, next) => {
|
||||
try {
|
||||
const authHeader = req.headers['authorization'];
|
||||
const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN
|
||||
|
||||
if (!token) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: 'Access token required'
|
||||
});
|
||||
}
|
||||
|
||||
const decoded = jwt.verify(token, process.env.JWT_SECRET);
|
||||
const user = await User.findById(decoded.userId).select('-password');
|
||||
|
||||
if (!user || !user.isActive) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: 'Invalid or inactive user'
|
||||
});
|
||||
}
|
||||
|
||||
req.user = user;
|
||||
next();
|
||||
} catch (error) {
|
||||
console.error('Token verification error:', error);
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
message: 'Invalid token'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Session-based authentication middleware
|
||||
* For web pages using sessions
|
||||
*/
|
||||
const authenticateSession = async (req, res, next) => {
|
||||
try {
|
||||
if (!req.session.userId) {
|
||||
req.flash('error', '로그인이 필요합니다.');
|
||||
return res.redirect('/auth/login');
|
||||
}
|
||||
|
||||
const user = await User.findById(req.session.userId).select('-password');
|
||||
|
||||
if (!user || !user.isActive) {
|
||||
req.session.destroy();
|
||||
req.flash('error', '유효하지 않은 사용자입니다.');
|
||||
return res.redirect('/auth/login');
|
||||
}
|
||||
|
||||
req.user = user;
|
||||
res.locals.user = user;
|
||||
next();
|
||||
} catch (error) {
|
||||
console.error('Session authentication error:', error);
|
||||
req.session.destroy();
|
||||
req.flash('error', '인증 오류가 발생했습니다.');
|
||||
return res.redirect('/auth/login');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Admin role middleware
|
||||
* Requires user to be authenticated and have admin role
|
||||
*/
|
||||
const requireAdmin = (req, res, next) => {
|
||||
if (!req.user) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: 'Authentication required'
|
||||
});
|
||||
}
|
||||
|
||||
if (req.user.role !== 'admin') {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
message: 'Admin access required'
|
||||
});
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
|
||||
/**
|
||||
* Admin session middleware for web pages
|
||||
*/
|
||||
const requireAdminSession = (req, res, next) => {
|
||||
if (!req.user) {
|
||||
req.flash('error', '로그인이 필요합니다.');
|
||||
return res.redirect('/auth/login');
|
||||
}
|
||||
|
||||
if (req.user.role !== 'admin') {
|
||||
req.flash('error', '관리자 권한이 필요합니다.');
|
||||
return res.redirect('/');
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
|
||||
/**
|
||||
* Optional authentication middleware
|
||||
* Attaches user if token exists but doesn't require it
|
||||
*/
|
||||
const optionalAuth = async (req, res, next) => {
|
||||
try {
|
||||
// Check session first
|
||||
if (req.session.userId) {
|
||||
const user = await User.findById(req.session.userId).select('-password');
|
||||
if (user && user.isActive) {
|
||||
req.user = user;
|
||||
res.locals.user = user;
|
||||
}
|
||||
}
|
||||
|
||||
// Check JWT token if no session
|
||||
if (!req.user) {
|
||||
const authHeader = req.headers['authorization'];
|
||||
const token = authHeader && authHeader.split(' ')[1];
|
||||
|
||||
if (token) {
|
||||
const decoded = jwt.verify(token, process.env.JWT_SECRET);
|
||||
const user = await User.findById(decoded.userId).select('-password');
|
||||
|
||||
if (user && user.isActive) {
|
||||
req.user = user;
|
||||
res.locals.user = user;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
next();
|
||||
} catch (error) {
|
||||
// Continue without authentication if token is invalid
|
||||
next();
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
authenticateToken,
|
||||
authenticateSession,
|
||||
requireAdmin,
|
||||
requireAdminSession,
|
||||
optionalAuth
|
||||
};
|
||||
310
.history/middleware/validation_20251019163300.js
Normal file
310
.history/middleware/validation_20251019163300.js
Normal file
@@ -0,0 +1,310 @@
|
||||
/**
|
||||
* Validation middleware for various data types
|
||||
*/
|
||||
|
||||
const { body, validationResult } = require('express-validator');
|
||||
|
||||
/**
|
||||
* Validation error handler
|
||||
*/
|
||||
const handleValidationErrors = (req, res, next) => {
|
||||
const errors = validationResult(req);
|
||||
|
||||
if (!errors.isEmpty()) {
|
||||
// For API requests
|
||||
if (req.xhr || req.headers.accept?.includes('application/json')) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Validation failed',
|
||||
errors: errors.array()
|
||||
});
|
||||
}
|
||||
|
||||
// For web requests
|
||||
const errorMessages = errors.array().map(error => error.msg);
|
||||
req.flash('error', errorMessages);
|
||||
return res.redirect('back');
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
|
||||
/**
|
||||
* Contact form validation
|
||||
*/
|
||||
const validateContactForm = [
|
||||
body('name')
|
||||
.trim()
|
||||
.isLength({ min: 2, max: 50 })
|
||||
.withMessage('이름은 2-50자 사이여야 합니다.')
|
||||
.matches(/^[a-zA-Z가-힣\s]+$/)
|
||||
.withMessage('이름에는 한글, 영문, 공백만 사용할 수 있습니다.'),
|
||||
|
||||
body('email')
|
||||
.isEmail()
|
||||
.withMessage('유효한 이메일 주소를 입력해주세요.')
|
||||
.normalizeEmail(),
|
||||
|
||||
body('phone')
|
||||
.optional()
|
||||
.matches(/^[0-9\-\+\(\)\s]+$/)
|
||||
.withMessage('유효한 전화번호를 입력해주세요.'),
|
||||
|
||||
body('company')
|
||||
.optional()
|
||||
.trim()
|
||||
.isLength({ max: 100 })
|
||||
.withMessage('회사명은 100자 이하여야 합니다.'),
|
||||
|
||||
body('service')
|
||||
.isIn(['web-development', 'mobile-app', 'ui-ux-design', 'branding', 'digital-marketing', 'consulting', 'other'])
|
||||
.withMessage('유효한 서비스를 선택해주세요.'),
|
||||
|
||||
body('budget')
|
||||
.optional()
|
||||
.isIn(['under-500', '500-1000', '1000-3000', '3000-5000', '5000-10000', 'over-10000', 'discuss'])
|
||||
.withMessage('유효한 예산 범위를 선택해주세요.'),
|
||||
|
||||
body('message')
|
||||
.trim()
|
||||
.isLength({ min: 10, max: 2000 })
|
||||
.withMessage('메시지는 10-2000자 사이여야 합니다.'),
|
||||
|
||||
handleValidationErrors
|
||||
];
|
||||
|
||||
/**
|
||||
* User registration validation
|
||||
*/
|
||||
const validateRegistration = [
|
||||
body('name')
|
||||
.trim()
|
||||
.isLength({ min: 2, max: 50 })
|
||||
.withMessage('이름은 2-50자 사이여야 합니다.')
|
||||
.matches(/^[a-zA-Z가-힣\s]+$/)
|
||||
.withMessage('이름에는 한글, 영문, 공백만 사용할 수 있습니다.'),
|
||||
|
||||
body('email')
|
||||
.isEmail()
|
||||
.withMessage('유효한 이메일 주소를 입력해주세요.')
|
||||
.normalizeEmail(),
|
||||
|
||||
body('password')
|
||||
.isLength({ min: 8 })
|
||||
.withMessage('비밀번호는 최소 8자 이상이어야 합니다.')
|
||||
.matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]/)
|
||||
.withMessage('비밀번호는 대소문자, 숫자, 특수문자를 포함해야 합니다.'),
|
||||
|
||||
body('confirmPassword')
|
||||
.custom((value, { req }) => {
|
||||
if (value !== req.body.password) {
|
||||
throw new Error('비밀번호 확인이 일치하지 않습니다.');
|
||||
}
|
||||
return true;
|
||||
}),
|
||||
|
||||
handleValidationErrors
|
||||
];
|
||||
|
||||
/**
|
||||
* User login validation
|
||||
*/
|
||||
const validateLogin = [
|
||||
body('email')
|
||||
.isEmail()
|
||||
.withMessage('유효한 이메일 주소를 입력해주세요.')
|
||||
.normalizeEmail(),
|
||||
|
||||
body('password')
|
||||
.isLength({ min: 1 })
|
||||
.withMessage('비밀번호를 입력해주세요.'),
|
||||
|
||||
handleValidationErrors
|
||||
];
|
||||
|
||||
/**
|
||||
* Portfolio validation
|
||||
*/
|
||||
const validatePortfolio = [
|
||||
body('title')
|
||||
.trim()
|
||||
.isLength({ min: 2, max: 100 })
|
||||
.withMessage('제목은 2-100자 사이여야 합니다.'),
|
||||
|
||||
body('description')
|
||||
.trim()
|
||||
.isLength({ min: 10, max: 5000 })
|
||||
.withMessage('설명은 10-5000자 사이여야 합니다.'),
|
||||
|
||||
body('shortDescription')
|
||||
.optional()
|
||||
.trim()
|
||||
.isLength({ max: 200 })
|
||||
.withMessage('간단한 설명은 200자 이하여야 합니다.'),
|
||||
|
||||
body('category')
|
||||
.isIn(['web-development', 'mobile-app', 'ui-ux-design', 'branding', 'marketing'])
|
||||
.withMessage('유효한 카테고리를 선택해주세요.'),
|
||||
|
||||
body('technologies')
|
||||
.optional()
|
||||
.isArray()
|
||||
.withMessage('기술 스택은 배열이어야 합니다.'),
|
||||
|
||||
body('clientName')
|
||||
.optional()
|
||||
.trim()
|
||||
.isLength({ max: 100 })
|
||||
.withMessage('클라이언트 이름은 100자 이하여야 합니다.'),
|
||||
|
||||
body('projectUrl')
|
||||
.optional()
|
||||
.isURL()
|
||||
.withMessage('유효한 URL을 입력해주세요.'),
|
||||
|
||||
body('status')
|
||||
.optional()
|
||||
.isIn(['planning', 'in-progress', 'completed', 'on-hold'])
|
||||
.withMessage('유효한 상태를 선택해주세요.'),
|
||||
|
||||
handleValidationErrors
|
||||
];
|
||||
|
||||
/**
|
||||
* Service validation
|
||||
*/
|
||||
const validateService = [
|
||||
body('name')
|
||||
.trim()
|
||||
.isLength({ min: 2, max: 100 })
|
||||
.withMessage('서비스명은 2-100자 사이여야 합니다.'),
|
||||
|
||||
body('description')
|
||||
.trim()
|
||||
.isLength({ min: 10, max: 5000 })
|
||||
.withMessage('설명은 10-5000자 사이여야 합니다.'),
|
||||
|
||||
body('shortDescription')
|
||||
.optional()
|
||||
.trim()
|
||||
.isLength({ max: 200 })
|
||||
.withMessage('간단한 설명은 200자 이하여야 합니다.'),
|
||||
|
||||
body('category')
|
||||
.isIn(['development', 'design', 'marketing', 'consulting'])
|
||||
.withMessage('유효한 카테고리를 선택해주세요.'),
|
||||
|
||||
body('pricing.basePrice')
|
||||
.optional()
|
||||
.isNumeric()
|
||||
.withMessage('기본 가격은 숫자여야 합니다.'),
|
||||
|
||||
body('pricing.priceType')
|
||||
.optional()
|
||||
.isIn(['project', 'hourly', 'monthly'])
|
||||
.withMessage('유효한 가격 유형을 선택해주세요.'),
|
||||
|
||||
handleValidationErrors
|
||||
];
|
||||
|
||||
/**
|
||||
* Calculator validation
|
||||
*/
|
||||
const validateCalculator = [
|
||||
body('service')
|
||||
.isMongoId()
|
||||
.withMessage('유효한 서비스를 선택해주세요.'),
|
||||
|
||||
body('projectType')
|
||||
.optional()
|
||||
.isIn(['simple', 'medium', 'complex', 'enterprise'])
|
||||
.withMessage('유효한 프로젝트 유형을 선택해주세요.'),
|
||||
|
||||
body('timeline')
|
||||
.optional()
|
||||
.isIn(['urgent', 'normal', 'flexible'])
|
||||
.withMessage('유효한 타임라인을 선택해주세요.'),
|
||||
|
||||
body('features')
|
||||
.optional()
|
||||
.isArray()
|
||||
.withMessage('기능은 배열이어야 합니다.'),
|
||||
|
||||
body('contactInfo.name')
|
||||
.optional()
|
||||
.trim()
|
||||
.isLength({ min: 2, max: 50 })
|
||||
.withMessage('이름은 2-50자 사이여야 합니다.'),
|
||||
|
||||
body('contactInfo.email')
|
||||
.optional()
|
||||
.isEmail()
|
||||
.withMessage('유효한 이메일 주소를 입력해주세요.'),
|
||||
|
||||
body('contactInfo.phone')
|
||||
.optional()
|
||||
.matches(/^[0-9\-\+\(\)\s]+$/)
|
||||
.withMessage('유효한 전화번호를 입력해주세요.'),
|
||||
|
||||
handleValidationErrors
|
||||
];
|
||||
|
||||
/**
|
||||
* Settings validation
|
||||
*/
|
||||
const validateSettings = [
|
||||
body('siteName')
|
||||
.optional()
|
||||
.trim()
|
||||
.isLength({ min: 1, max: 100 })
|
||||
.withMessage('사이트명은 1-100자 사이여야 합니다.'),
|
||||
|
||||
body('siteDescription')
|
||||
.optional()
|
||||
.trim()
|
||||
.isLength({ max: 500 })
|
||||
.withMessage('사이트 설명은 500자 이하여야 합니다.'),
|
||||
|
||||
body('contact.email')
|
||||
.optional()
|
||||
.isEmail()
|
||||
.withMessage('유효한 이메일 주소를 입력해주세요.'),
|
||||
|
||||
body('contact.phone')
|
||||
.optional()
|
||||
.matches(/^[0-9\-\+\(\)\s]+$/)
|
||||
.withMessage('유효한 전화번호를 입력해주세요.'),
|
||||
|
||||
body('social.facebook')
|
||||
.optional()
|
||||
.isURL()
|
||||
.withMessage('유효한 Facebook URL을 입력해주세요.'),
|
||||
|
||||
body('social.twitter')
|
||||
.optional()
|
||||
.isURL()
|
||||
.withMessage('유효한 Twitter URL을 입력해주세요.'),
|
||||
|
||||
body('social.linkedin')
|
||||
.optional()
|
||||
.isURL()
|
||||
.withMessage('유효한 LinkedIn URL을 입력해주세요.'),
|
||||
|
||||
body('social.instagram')
|
||||
.optional()
|
||||
.isURL()
|
||||
.withMessage('유효한 Instagram URL을 입력해주세요.'),
|
||||
|
||||
handleValidationErrors
|
||||
];
|
||||
|
||||
module.exports = {
|
||||
handleValidationErrors,
|
||||
validateContactForm,
|
||||
validateRegistration,
|
||||
validateLogin,
|
||||
validatePortfolio,
|
||||
validateService,
|
||||
validateCalculator,
|
||||
validateSettings
|
||||
};
|
||||
310
.history/middleware/validation_20251019163807.js
Normal file
310
.history/middleware/validation_20251019163807.js
Normal file
@@ -0,0 +1,310 @@
|
||||
/**
|
||||
* Validation middleware for various data types
|
||||
*/
|
||||
|
||||
const { body, validationResult } = require('express-validator');
|
||||
|
||||
/**
|
||||
* Validation error handler
|
||||
*/
|
||||
const handleValidationErrors = (req, res, next) => {
|
||||
const errors = validationResult(req);
|
||||
|
||||
if (!errors.isEmpty()) {
|
||||
// For API requests
|
||||
if (req.xhr || req.headers.accept?.includes('application/json')) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Validation failed',
|
||||
errors: errors.array()
|
||||
});
|
||||
}
|
||||
|
||||
// For web requests
|
||||
const errorMessages = errors.array().map(error => error.msg);
|
||||
req.flash('error', errorMessages);
|
||||
return res.redirect('back');
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
|
||||
/**
|
||||
* Contact form validation
|
||||
*/
|
||||
const validateContactForm = [
|
||||
body('name')
|
||||
.trim()
|
||||
.isLength({ min: 2, max: 50 })
|
||||
.withMessage('이름은 2-50자 사이여야 합니다.')
|
||||
.matches(/^[a-zA-Z가-힣\s]+$/)
|
||||
.withMessage('이름에는 한글, 영문, 공백만 사용할 수 있습니다.'),
|
||||
|
||||
body('email')
|
||||
.isEmail()
|
||||
.withMessage('유효한 이메일 주소를 입력해주세요.')
|
||||
.normalizeEmail(),
|
||||
|
||||
body('phone')
|
||||
.optional()
|
||||
.matches(/^[0-9\-\+\(\)\s]+$/)
|
||||
.withMessage('유효한 전화번호를 입력해주세요.'),
|
||||
|
||||
body('company')
|
||||
.optional()
|
||||
.trim()
|
||||
.isLength({ max: 100 })
|
||||
.withMessage('회사명은 100자 이하여야 합니다.'),
|
||||
|
||||
body('service')
|
||||
.isIn(['web-development', 'mobile-app', 'ui-ux-design', 'branding', 'digital-marketing', 'consulting', 'other'])
|
||||
.withMessage('유효한 서비스를 선택해주세요.'),
|
||||
|
||||
body('budget')
|
||||
.optional()
|
||||
.isIn(['under-500', '500-1000', '1000-3000', '3000-5000', '5000-10000', 'over-10000', 'discuss'])
|
||||
.withMessage('유효한 예산 범위를 선택해주세요.'),
|
||||
|
||||
body('message')
|
||||
.trim()
|
||||
.isLength({ min: 10, max: 2000 })
|
||||
.withMessage('메시지는 10-2000자 사이여야 합니다.'),
|
||||
|
||||
handleValidationErrors
|
||||
];
|
||||
|
||||
/**
|
||||
* User registration validation
|
||||
*/
|
||||
const validateRegistration = [
|
||||
body('name')
|
||||
.trim()
|
||||
.isLength({ min: 2, max: 50 })
|
||||
.withMessage('이름은 2-50자 사이여야 합니다.')
|
||||
.matches(/^[a-zA-Z가-힣\s]+$/)
|
||||
.withMessage('이름에는 한글, 영문, 공백만 사용할 수 있습니다.'),
|
||||
|
||||
body('email')
|
||||
.isEmail()
|
||||
.withMessage('유효한 이메일 주소를 입력해주세요.')
|
||||
.normalizeEmail(),
|
||||
|
||||
body('password')
|
||||
.isLength({ min: 8 })
|
||||
.withMessage('비밀번호는 최소 8자 이상이어야 합니다.')
|
||||
.matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]/)
|
||||
.withMessage('비밀번호는 대소문자, 숫자, 특수문자를 포함해야 합니다.'),
|
||||
|
||||
body('confirmPassword')
|
||||
.custom((value, { req }) => {
|
||||
if (value !== req.body.password) {
|
||||
throw new Error('비밀번호 확인이 일치하지 않습니다.');
|
||||
}
|
||||
return true;
|
||||
}),
|
||||
|
||||
handleValidationErrors
|
||||
];
|
||||
|
||||
/**
|
||||
* User login validation
|
||||
*/
|
||||
const validateLogin = [
|
||||
body('email')
|
||||
.isEmail()
|
||||
.withMessage('유효한 이메일 주소를 입력해주세요.')
|
||||
.normalizeEmail(),
|
||||
|
||||
body('password')
|
||||
.isLength({ min: 1 })
|
||||
.withMessage('비밀번호를 입력해주세요.'),
|
||||
|
||||
handleValidationErrors
|
||||
];
|
||||
|
||||
/**
|
||||
* Portfolio validation
|
||||
*/
|
||||
const validatePortfolio = [
|
||||
body('title')
|
||||
.trim()
|
||||
.isLength({ min: 2, max: 100 })
|
||||
.withMessage('제목은 2-100자 사이여야 합니다.'),
|
||||
|
||||
body('description')
|
||||
.trim()
|
||||
.isLength({ min: 10, max: 5000 })
|
||||
.withMessage('설명은 10-5000자 사이여야 합니다.'),
|
||||
|
||||
body('shortDescription')
|
||||
.optional()
|
||||
.trim()
|
||||
.isLength({ max: 200 })
|
||||
.withMessage('간단한 설명은 200자 이하여야 합니다.'),
|
||||
|
||||
body('category')
|
||||
.isIn(['web-development', 'mobile-app', 'ui-ux-design', 'branding', 'marketing'])
|
||||
.withMessage('유효한 카테고리를 선택해주세요.'),
|
||||
|
||||
body('technologies')
|
||||
.optional()
|
||||
.isArray()
|
||||
.withMessage('기술 스택은 배열이어야 합니다.'),
|
||||
|
||||
body('clientName')
|
||||
.optional()
|
||||
.trim()
|
||||
.isLength({ max: 100 })
|
||||
.withMessage('클라이언트 이름은 100자 이하여야 합니다.'),
|
||||
|
||||
body('projectUrl')
|
||||
.optional()
|
||||
.isURL()
|
||||
.withMessage('유효한 URL을 입력해주세요.'),
|
||||
|
||||
body('status')
|
||||
.optional()
|
||||
.isIn(['planning', 'in-progress', 'completed', 'on-hold'])
|
||||
.withMessage('유효한 상태를 선택해주세요.'),
|
||||
|
||||
handleValidationErrors
|
||||
];
|
||||
|
||||
/**
|
||||
* Service validation
|
||||
*/
|
||||
const validateService = [
|
||||
body('name')
|
||||
.trim()
|
||||
.isLength({ min: 2, max: 100 })
|
||||
.withMessage('서비스명은 2-100자 사이여야 합니다.'),
|
||||
|
||||
body('description')
|
||||
.trim()
|
||||
.isLength({ min: 10, max: 5000 })
|
||||
.withMessage('설명은 10-5000자 사이여야 합니다.'),
|
||||
|
||||
body('shortDescription')
|
||||
.optional()
|
||||
.trim()
|
||||
.isLength({ max: 200 })
|
||||
.withMessage('간단한 설명은 200자 이하여야 합니다.'),
|
||||
|
||||
body('category')
|
||||
.isIn(['development', 'design', 'marketing', 'consulting'])
|
||||
.withMessage('유효한 카테고리를 선택해주세요.'),
|
||||
|
||||
body('pricing.basePrice')
|
||||
.optional()
|
||||
.isNumeric()
|
||||
.withMessage('기본 가격은 숫자여야 합니다.'),
|
||||
|
||||
body('pricing.priceType')
|
||||
.optional()
|
||||
.isIn(['project', 'hourly', 'monthly'])
|
||||
.withMessage('유효한 가격 유형을 선택해주세요.'),
|
||||
|
||||
handleValidationErrors
|
||||
];
|
||||
|
||||
/**
|
||||
* Calculator validation
|
||||
*/
|
||||
const validateCalculator = [
|
||||
body('service')
|
||||
.isMongoId()
|
||||
.withMessage('유효한 서비스를 선택해주세요.'),
|
||||
|
||||
body('projectType')
|
||||
.optional()
|
||||
.isIn(['simple', 'medium', 'complex', 'enterprise'])
|
||||
.withMessage('유효한 프로젝트 유형을 선택해주세요.'),
|
||||
|
||||
body('timeline')
|
||||
.optional()
|
||||
.isIn(['urgent', 'normal', 'flexible'])
|
||||
.withMessage('유효한 타임라인을 선택해주세요.'),
|
||||
|
||||
body('features')
|
||||
.optional()
|
||||
.isArray()
|
||||
.withMessage('기능은 배열이어야 합니다.'),
|
||||
|
||||
body('contactInfo.name')
|
||||
.optional()
|
||||
.trim()
|
||||
.isLength({ min: 2, max: 50 })
|
||||
.withMessage('이름은 2-50자 사이여야 합니다.'),
|
||||
|
||||
body('contactInfo.email')
|
||||
.optional()
|
||||
.isEmail()
|
||||
.withMessage('유효한 이메일 주소를 입력해주세요.'),
|
||||
|
||||
body('contactInfo.phone')
|
||||
.optional()
|
||||
.matches(/^[0-9\-\+\(\)\s]+$/)
|
||||
.withMessage('유효한 전화번호를 입력해주세요.'),
|
||||
|
||||
handleValidationErrors
|
||||
];
|
||||
|
||||
/**
|
||||
* Settings validation
|
||||
*/
|
||||
const validateSettings = [
|
||||
body('siteName')
|
||||
.optional()
|
||||
.trim()
|
||||
.isLength({ min: 1, max: 100 })
|
||||
.withMessage('사이트명은 1-100자 사이여야 합니다.'),
|
||||
|
||||
body('siteDescription')
|
||||
.optional()
|
||||
.trim()
|
||||
.isLength({ max: 500 })
|
||||
.withMessage('사이트 설명은 500자 이하여야 합니다.'),
|
||||
|
||||
body('contact.email')
|
||||
.optional()
|
||||
.isEmail()
|
||||
.withMessage('유효한 이메일 주소를 입력해주세요.'),
|
||||
|
||||
body('contact.phone')
|
||||
.optional()
|
||||
.matches(/^[0-9\-\+\(\)\s]+$/)
|
||||
.withMessage('유효한 전화번호를 입력해주세요.'),
|
||||
|
||||
body('social.facebook')
|
||||
.optional()
|
||||
.isURL()
|
||||
.withMessage('유효한 Facebook URL을 입력해주세요.'),
|
||||
|
||||
body('social.twitter')
|
||||
.optional()
|
||||
.isURL()
|
||||
.withMessage('유효한 Twitter URL을 입력해주세요.'),
|
||||
|
||||
body('social.linkedin')
|
||||
.optional()
|
||||
.isURL()
|
||||
.withMessage('유효한 LinkedIn URL을 입력해주세요.'),
|
||||
|
||||
body('social.instagram')
|
||||
.optional()
|
||||
.isURL()
|
||||
.withMessage('유효한 Instagram URL을 입력해주세요.'),
|
||||
|
||||
handleValidationErrors
|
||||
];
|
||||
|
||||
module.exports = {
|
||||
handleValidationErrors,
|
||||
validateContactForm,
|
||||
validateRegistration,
|
||||
validateLogin,
|
||||
validatePortfolio,
|
||||
validateService,
|
||||
validateCalculator,
|
||||
validateSettings
|
||||
};
|
||||
80
.history/models/Contact_20251019160612.js
Normal file
80
.history/models/Contact_20251019160612.js
Normal file
@@ -0,0 +1,80 @@
|
||||
const mongoose = require('mongoose');
|
||||
|
||||
const contactSchema = new mongoose.Schema({
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true
|
||||
},
|
||||
email: {
|
||||
type: String,
|
||||
required: true,
|
||||
lowercase: true,
|
||||
trim: true
|
||||
},
|
||||
phone: {
|
||||
type: String,
|
||||
trim: true
|
||||
},
|
||||
company: {
|
||||
type: String,
|
||||
trim: true
|
||||
},
|
||||
subject: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true
|
||||
},
|
||||
message: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
serviceInterest: {
|
||||
type: String,
|
||||
enum: ['web-development', 'mobile-app', 'ui-ux-design', 'branding', 'consulting', 'other']
|
||||
},
|
||||
budget: {
|
||||
type: String,
|
||||
enum: ['under-1m', '1m-5m', '5m-10m', '10m-20m', '20m-50m', 'over-50m']
|
||||
},
|
||||
timeline: {
|
||||
type: String,
|
||||
enum: ['asap', '1-month', '1-3-months', '3-6-months', 'flexible']
|
||||
},
|
||||
status: {
|
||||
type: String,
|
||||
enum: ['new', 'in-progress', 'replied', 'closed'],
|
||||
default: 'new'
|
||||
},
|
||||
priority: {
|
||||
type: String,
|
||||
enum: ['low', 'medium', 'high', 'urgent'],
|
||||
default: 'medium'
|
||||
},
|
||||
source: {
|
||||
type: String,
|
||||
enum: ['website', 'telegram', 'email', 'phone', 'referral'],
|
||||
default: 'website'
|
||||
},
|
||||
isRead: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
adminNotes: {
|
||||
type: String
|
||||
},
|
||||
ipAddress: {
|
||||
type: String
|
||||
},
|
||||
userAgent: {
|
||||
type: String
|
||||
}
|
||||
}, {
|
||||
timestamps: true
|
||||
});
|
||||
|
||||
contactSchema.index({ status: 1, createdAt: -1 });
|
||||
contactSchema.index({ isRead: 1, createdAt: -1 });
|
||||
contactSchema.index({ email: 1 });
|
||||
|
||||
module.exports = mongoose.model('Contact', contactSchema);
|
||||
80
.history/models/Contact_20251019162544.js
Normal file
80
.history/models/Contact_20251019162544.js
Normal file
@@ -0,0 +1,80 @@
|
||||
const mongoose = require('mongoose');
|
||||
|
||||
const contactSchema = new mongoose.Schema({
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true
|
||||
},
|
||||
email: {
|
||||
type: String,
|
||||
required: true,
|
||||
lowercase: true,
|
||||
trim: true
|
||||
},
|
||||
phone: {
|
||||
type: String,
|
||||
trim: true
|
||||
},
|
||||
company: {
|
||||
type: String,
|
||||
trim: true
|
||||
},
|
||||
subject: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true
|
||||
},
|
||||
message: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
serviceInterest: {
|
||||
type: String,
|
||||
enum: ['web-development', 'mobile-app', 'ui-ux-design', 'branding', 'consulting', 'other']
|
||||
},
|
||||
budget: {
|
||||
type: String,
|
||||
enum: ['under-1m', '1m-5m', '5m-10m', '10m-20m', '20m-50m', 'over-50m']
|
||||
},
|
||||
timeline: {
|
||||
type: String,
|
||||
enum: ['asap', '1-month', '1-3-months', '3-6-months', 'flexible']
|
||||
},
|
||||
status: {
|
||||
type: String,
|
||||
enum: ['new', 'in-progress', 'replied', 'closed'],
|
||||
default: 'new'
|
||||
},
|
||||
priority: {
|
||||
type: String,
|
||||
enum: ['low', 'medium', 'high', 'urgent'],
|
||||
default: 'medium'
|
||||
},
|
||||
source: {
|
||||
type: String,
|
||||
enum: ['website', 'telegram', 'email', 'phone', 'referral'],
|
||||
default: 'website'
|
||||
},
|
||||
isRead: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
adminNotes: {
|
||||
type: String
|
||||
},
|
||||
ipAddress: {
|
||||
type: String
|
||||
},
|
||||
userAgent: {
|
||||
type: String
|
||||
}
|
||||
}, {
|
||||
timestamps: true
|
||||
});
|
||||
|
||||
contactSchema.index({ status: 1, createdAt: -1 });
|
||||
contactSchema.index({ isRead: 1, createdAt: -1 });
|
||||
contactSchema.index({ email: 1 });
|
||||
|
||||
module.exports = mongoose.model('Contact', contactSchema);
|
||||
107
.history/models/Portfolio_20251019160550.js
Normal file
107
.history/models/Portfolio_20251019160550.js
Normal file
@@ -0,0 +1,107 @@
|
||||
const mongoose = require('mongoose');
|
||||
|
||||
const portfolioSchema = new mongoose.Schema({
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
shortDescription: {
|
||||
type: String,
|
||||
required: true,
|
||||
maxlength: 200
|
||||
},
|
||||
category: {
|
||||
type: String,
|
||||
required: true,
|
||||
enum: ['web-development', 'mobile-app', 'ui-ux-design', 'branding', 'e-commerce', 'other']
|
||||
},
|
||||
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
|
||||
},
|
||||
projectUrl: {
|
||||
type: String,
|
||||
trim: true
|
||||
},
|
||||
githubUrl: {
|
||||
type: String,
|
||||
trim: true
|
||||
},
|
||||
status: {
|
||||
type: String,
|
||||
enum: ['completed', 'in-progress', 'planning'],
|
||||
default: 'completed'
|
||||
},
|
||||
featured: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
publishedAt: {
|
||||
type: Date,
|
||||
default: Date.now
|
||||
},
|
||||
completedAt: {
|
||||
type: Date
|
||||
},
|
||||
isPublished: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
viewCount: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
likes: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
order: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
seo: {
|
||||
metaTitle: String,
|
||||
metaDescription: String,
|
||||
keywords: [String]
|
||||
}
|
||||
}, {
|
||||
timestamps: true
|
||||
});
|
||||
|
||||
// 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() {
|
||||
const primary = this.images.find(img => img.isPrimary);
|
||||
return primary || (this.images.length > 0 ? this.images[0] : null);
|
||||
});
|
||||
|
||||
portfolioSchema.set('toJSON', { virtuals: true });
|
||||
|
||||
module.exports = mongoose.model('Portfolio', portfolioSchema);
|
||||
107
.history/models/Portfolio_20251019162544.js
Normal file
107
.history/models/Portfolio_20251019162544.js
Normal file
@@ -0,0 +1,107 @@
|
||||
const mongoose = require('mongoose');
|
||||
|
||||
const portfolioSchema = new mongoose.Schema({
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
shortDescription: {
|
||||
type: String,
|
||||
required: true,
|
||||
maxlength: 200
|
||||
},
|
||||
category: {
|
||||
type: String,
|
||||
required: true,
|
||||
enum: ['web-development', 'mobile-app', 'ui-ux-design', 'branding', 'e-commerce', 'other']
|
||||
},
|
||||
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
|
||||
},
|
||||
projectUrl: {
|
||||
type: String,
|
||||
trim: true
|
||||
},
|
||||
githubUrl: {
|
||||
type: String,
|
||||
trim: true
|
||||
},
|
||||
status: {
|
||||
type: String,
|
||||
enum: ['completed', 'in-progress', 'planning'],
|
||||
default: 'completed'
|
||||
},
|
||||
featured: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
publishedAt: {
|
||||
type: Date,
|
||||
default: Date.now
|
||||
},
|
||||
completedAt: {
|
||||
type: Date
|
||||
},
|
||||
isPublished: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
viewCount: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
likes: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
order: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
seo: {
|
||||
metaTitle: String,
|
||||
metaDescription: String,
|
||||
keywords: [String]
|
||||
}
|
||||
}, {
|
||||
timestamps: true
|
||||
});
|
||||
|
||||
// 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() {
|
||||
const primary = this.images.find(img => img.isPrimary);
|
||||
return primary || (this.images.length > 0 ? this.images[0] : null);
|
||||
});
|
||||
|
||||
portfolioSchema.set('toJSON', { virtuals: true });
|
||||
|
||||
module.exports = mongoose.model('Portfolio', portfolioSchema);
|
||||
102
.history/models/Service_20251019160601.js
Normal file
102
.history/models/Service_20251019160601.js
Normal file
@@ -0,0 +1,102 @@
|
||||
const mongoose = require('mongoose');
|
||||
|
||||
const serviceSchema = new mongoose.Schema({
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
shortDescription: {
|
||||
type: String,
|
||||
required: true,
|
||||
maxlength: 150
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
category: {
|
||||
type: String,
|
||||
required: true,
|
||||
enum: ['development', 'design', 'consulting', 'marketing', 'maintenance']
|
||||
},
|
||||
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
|
||||
}
|
||||
},
|
||||
estimatedTime: {
|
||||
min: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
max: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
unit: {
|
||||
type: String,
|
||||
enum: ['hours', 'days', 'weeks', 'months'],
|
||||
default: 'days'
|
||||
}
|
||||
},
|
||||
isActive: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
featured: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
order: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
portfolio: [{
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'Portfolio'
|
||||
}],
|
||||
tags: [{
|
||||
type: String,
|
||||
trim: true
|
||||
}],
|
||||
seo: {
|
||||
metaTitle: String,
|
||||
metaDescription: String,
|
||||
keywords: [String]
|
||||
}
|
||||
}, {
|
||||
timestamps: true
|
||||
});
|
||||
|
||||
serviceSchema.index({ name: 'text', description: 'text', tags: 'text' });
|
||||
serviceSchema.index({ category: 1, featured: -1, order: 1 });
|
||||
|
||||
module.exports = mongoose.model('Service', serviceSchema);
|
||||
102
.history/models/Service_20251019162544.js
Normal file
102
.history/models/Service_20251019162544.js
Normal file
@@ -0,0 +1,102 @@
|
||||
const mongoose = require('mongoose');
|
||||
|
||||
const serviceSchema = new mongoose.Schema({
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
shortDescription: {
|
||||
type: String,
|
||||
required: true,
|
||||
maxlength: 150
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
category: {
|
||||
type: String,
|
||||
required: true,
|
||||
enum: ['development', 'design', 'consulting', 'marketing', 'maintenance']
|
||||
},
|
||||
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
|
||||
}
|
||||
},
|
||||
estimatedTime: {
|
||||
min: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
max: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
unit: {
|
||||
type: String,
|
||||
enum: ['hours', 'days', 'weeks', 'months'],
|
||||
default: 'days'
|
||||
}
|
||||
},
|
||||
isActive: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
featured: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
order: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
portfolio: [{
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'Portfolio'
|
||||
}],
|
||||
tags: [{
|
||||
type: String,
|
||||
trim: true
|
||||
}],
|
||||
seo: {
|
||||
metaTitle: String,
|
||||
metaDescription: String,
|
||||
keywords: [String]
|
||||
}
|
||||
}, {
|
||||
timestamps: true
|
||||
});
|
||||
|
||||
serviceSchema.index({ name: 'text', description: 'text', tags: 'text' });
|
||||
serviceSchema.index({ category: 1, featured: -1, order: 1 });
|
||||
|
||||
module.exports = mongoose.model('Service', serviceSchema);
|
||||
116
.history/models/SiteSettings_20251019160626.js
Normal file
116
.history/models/SiteSettings_20251019160626.js
Normal file
@@ -0,0 +1,116 @@
|
||||
const mongoose = require('mongoose');
|
||||
|
||||
const siteSettingsSchema = new mongoose.Schema({
|
||||
siteName: {
|
||||
type: String,
|
||||
default: 'SmartSolTech'
|
||||
},
|
||||
siteDescription: {
|
||||
type: String,
|
||||
default: 'Innovative technology solutions for modern businesses'
|
||||
},
|
||||
logo: {
|
||||
type: String,
|
||||
default: '/images/logo.png'
|
||||
},
|
||||
favicon: {
|
||||
type: String,
|
||||
default: '/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'
|
||||
}
|
||||
},
|
||||
social: {
|
||||
facebook: String,
|
||||
twitter: String,
|
||||
linkedin: String,
|
||||
instagram: String,
|
||||
github: String,
|
||||
telegram: String
|
||||
},
|
||||
telegram: {
|
||||
botToken: String,
|
||||
chatId: String,
|
||||
isEnabled: {
|
||||
type: Boolean,
|
||||
default: 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
|
||||
},
|
||||
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'
|
||||
}
|
||||
},
|
||||
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'
|
||||
}
|
||||
},
|
||||
maintenance: {
|
||||
isEnabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
message: {
|
||||
type: String,
|
||||
default: 'We are currently performing maintenance. Please check back soon.'
|
||||
}
|
||||
}
|
||||
}, {
|
||||
timestamps: true
|
||||
});
|
||||
|
||||
module.exports = mongoose.model('SiteSettings', siteSettingsSchema);
|
||||
116
.history/models/SiteSettings_20251019162544.js
Normal file
116
.history/models/SiteSettings_20251019162544.js
Normal file
@@ -0,0 +1,116 @@
|
||||
const mongoose = require('mongoose');
|
||||
|
||||
const siteSettingsSchema = new mongoose.Schema({
|
||||
siteName: {
|
||||
type: String,
|
||||
default: 'SmartSolTech'
|
||||
},
|
||||
siteDescription: {
|
||||
type: String,
|
||||
default: 'Innovative technology solutions for modern businesses'
|
||||
},
|
||||
logo: {
|
||||
type: String,
|
||||
default: '/images/logo.png'
|
||||
},
|
||||
favicon: {
|
||||
type: String,
|
||||
default: '/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'
|
||||
}
|
||||
},
|
||||
social: {
|
||||
facebook: String,
|
||||
twitter: String,
|
||||
linkedin: String,
|
||||
instagram: String,
|
||||
github: String,
|
||||
telegram: String
|
||||
},
|
||||
telegram: {
|
||||
botToken: String,
|
||||
chatId: String,
|
||||
isEnabled: {
|
||||
type: Boolean,
|
||||
default: 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
|
||||
},
|
||||
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'
|
||||
}
|
||||
},
|
||||
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'
|
||||
}
|
||||
},
|
||||
maintenance: {
|
||||
isEnabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
message: {
|
||||
type: String,
|
||||
default: 'We are currently performing maintenance. Please check back soon.'
|
||||
}
|
||||
}
|
||||
}, {
|
||||
timestamps: true
|
||||
});
|
||||
|
||||
module.exports = mongoose.model('SiteSettings', siteSettingsSchema);
|
||||
75
.history/models/User_20251019160538.js
Normal file
75
.history/models/User_20251019160538.js
Normal file
@@ -0,0 +1,75 @@
|
||||
const mongoose = require('mongoose');
|
||||
const bcrypt = require('bcryptjs');
|
||||
|
||||
const userSchema = new mongoose.Schema({
|
||||
email: {
|
||||
type: String,
|
||||
required: true,
|
||||
unique: true,
|
||||
lowercase: true,
|
||||
trim: true
|
||||
},
|
||||
password: {
|
||||
type: String,
|
||||
required: true,
|
||||
minlength: 6
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true
|
||||
},
|
||||
role: {
|
||||
type: String,
|
||||
enum: ['admin', 'moderator'],
|
||||
default: 'admin'
|
||||
},
|
||||
avatar: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
isActive: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
lastLogin: {
|
||||
type: Date,
|
||||
default: null
|
||||
},
|
||||
createdAt: {
|
||||
type: Date,
|
||||
default: Date.now
|
||||
},
|
||||
updatedAt: {
|
||||
type: Date,
|
||||
default: Date.now
|
||||
}
|
||||
}, {
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
||||
// Compare password method
|
||||
userSchema.methods.comparePassword = async function(candidatePassword) {
|
||||
return bcrypt.compare(candidatePassword, this.password);
|
||||
};
|
||||
|
||||
// Update last login
|
||||
userSchema.methods.updateLastLogin = function() {
|
||||
this.lastLogin = new Date();
|
||||
return this.save();
|
||||
};
|
||||
|
||||
module.exports = mongoose.model('User', userSchema);
|
||||
75
.history/models/User_20251019162544.js
Normal file
75
.history/models/User_20251019162544.js
Normal file
@@ -0,0 +1,75 @@
|
||||
const mongoose = require('mongoose');
|
||||
const bcrypt = require('bcryptjs');
|
||||
|
||||
const userSchema = new mongoose.Schema({
|
||||
email: {
|
||||
type: String,
|
||||
required: true,
|
||||
unique: true,
|
||||
lowercase: true,
|
||||
trim: true
|
||||
},
|
||||
password: {
|
||||
type: String,
|
||||
required: true,
|
||||
minlength: 6
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true
|
||||
},
|
||||
role: {
|
||||
type: String,
|
||||
enum: ['admin', 'moderator'],
|
||||
default: 'admin'
|
||||
},
|
||||
avatar: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
isActive: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
lastLogin: {
|
||||
type: Date,
|
||||
default: null
|
||||
},
|
||||
createdAt: {
|
||||
type: Date,
|
||||
default: Date.now
|
||||
},
|
||||
updatedAt: {
|
||||
type: Date,
|
||||
default: Date.now
|
||||
}
|
||||
}, {
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
||||
// Compare password method
|
||||
userSchema.methods.comparePassword = async function(candidatePassword) {
|
||||
return bcrypt.compare(candidatePassword, this.password);
|
||||
};
|
||||
|
||||
// Update last login
|
||||
userSchema.methods.updateLastLogin = function() {
|
||||
this.lastLogin = new Date();
|
||||
return this.save();
|
||||
};
|
||||
|
||||
module.exports = mongoose.model('User', userSchema);
|
||||
48
.history/package_20251019160453.json
Normal file
48
.history/package_20251019160453.json
Normal file
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"name": "smartsoltech-website",
|
||||
"version": "1.0.0",
|
||||
"description": "Modern PWA website for SmartSolTech with admin panel and Telegram integration",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
"start": "node server.js",
|
||||
"dev": "nodemon server.js",
|
||||
"build": "webpack --mode production",
|
||||
"build:dev": "webpack --mode development",
|
||||
"watch": "webpack --mode development --watch"
|
||||
},
|
||||
"keywords": ["pwa", "nodejs", "express", "telegram", "portfolio", "calculator"],
|
||||
"author": "SmartSolTech",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"express": "^4.18.2",
|
||||
"mongoose": "^8.0.3",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"express-session": "^1.17.3",
|
||||
"connect-mongo": "^5.1.0",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"sharp": "^0.33.0",
|
||||
"node-telegram-bot-api": "^0.64.0",
|
||||
"express-rate-limit": "^7.1.5",
|
||||
"helmet": "^7.1.0",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.3.1",
|
||||
"compression": "^1.7.4",
|
||||
"morgan": "^1.10.0",
|
||||
"nodemailer": "^6.9.7",
|
||||
"express-validator": "^7.0.1",
|
||||
"socket.io": "^4.7.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^3.0.2",
|
||||
"webpack": "^5.89.0",
|
||||
"webpack-cli": "^5.1.4",
|
||||
"css-loader": "^6.8.1",
|
||||
"style-loader": "^3.3.3",
|
||||
"file-loader": "^6.2.0",
|
||||
"html-webpack-plugin": "^5.5.3",
|
||||
"mini-css-extract-plugin": "^2.7.6",
|
||||
"terser-webpack-plugin": "^5.3.9",
|
||||
"workbox-webpack-plugin": "^7.0.0"
|
||||
}
|
||||
}
|
||||
48
.history/package_20251019162544.json
Normal file
48
.history/package_20251019162544.json
Normal file
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"name": "smartsoltech-website",
|
||||
"version": "1.0.0",
|
||||
"description": "Modern PWA website for SmartSolTech with admin panel and Telegram integration",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
"start": "node server.js",
|
||||
"dev": "nodemon server.js",
|
||||
"build": "webpack --mode production",
|
||||
"build:dev": "webpack --mode development",
|
||||
"watch": "webpack --mode development --watch"
|
||||
},
|
||||
"keywords": ["pwa", "nodejs", "express", "telegram", "portfolio", "calculator"],
|
||||
"author": "SmartSolTech",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"express": "^4.18.2",
|
||||
"mongoose": "^8.0.3",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"express-session": "^1.17.3",
|
||||
"connect-mongo": "^5.1.0",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"sharp": "^0.33.0",
|
||||
"node-telegram-bot-api": "^0.64.0",
|
||||
"express-rate-limit": "^7.1.5",
|
||||
"helmet": "^7.1.0",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.3.1",
|
||||
"compression": "^1.7.4",
|
||||
"morgan": "^1.10.0",
|
||||
"nodemailer": "^6.9.7",
|
||||
"express-validator": "^7.0.1",
|
||||
"socket.io": "^4.7.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^3.0.2",
|
||||
"webpack": "^5.89.0",
|
||||
"webpack-cli": "^5.1.4",
|
||||
"css-loader": "^6.8.1",
|
||||
"style-loader": "^3.3.3",
|
||||
"file-loader": "^6.2.0",
|
||||
"html-webpack-plugin": "^5.5.3",
|
||||
"mini-css-extract-plugin": "^2.7.6",
|
||||
"terser-webpack-plugin": "^5.3.9",
|
||||
"workbox-webpack-plugin": "^7.0.0"
|
||||
}
|
||||
}
|
||||
48
.history/package_20251019162621.json
Normal file
48
.history/package_20251019162621.json
Normal file
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"name": "smartsoltech-website",
|
||||
"version": "1.0.0",
|
||||
"description": "Modern PWA website for SmartSolTech with admin panel and Telegram integration",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
"start": "node server.js",
|
||||
"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"
|
||||
},
|
||||
"keywords": ["pwa", "nodejs", "express", "telegram", "portfolio", "calculator"],
|
||||
"author": "SmartSolTech",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"express": "^4.18.2",
|
||||
"mongoose": "^8.0.3",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"express-session": "^1.17.3",
|
||||
"connect-mongo": "^5.1.0",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"sharp": "^0.33.0",
|
||||
"node-telegram-bot-api": "^0.64.0",
|
||||
"express-rate-limit": "^7.1.5",
|
||||
"helmet": "^7.1.0",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.3.1",
|
||||
"compression": "^1.7.4",
|
||||
"morgan": "^1.10.0",
|
||||
"nodemailer": "^6.9.7",
|
||||
"express-validator": "^7.0.1",
|
||||
"socket.io": "^4.7.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^3.0.2",
|
||||
"webpack": "^5.89.0",
|
||||
"webpack-cli": "^5.1.4",
|
||||
"css-loader": "^6.8.1",
|
||||
"style-loader": "^3.3.3",
|
||||
"file-loader": "^6.2.0",
|
||||
"html-webpack-plugin": "^5.5.3",
|
||||
"mini-css-extract-plugin": "^2.7.6",
|
||||
"terser-webpack-plugin": "^5.3.9",
|
||||
"workbox-webpack-plugin": "^7.0.0"
|
||||
}
|
||||
}
|
||||
48
.history/package_20251019163806.json
Normal file
48
.history/package_20251019163806.json
Normal file
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"name": "smartsoltech-website",
|
||||
"version": "1.0.0",
|
||||
"description": "Modern PWA website for SmartSolTech with admin panel and Telegram integration",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
"start": "node server.js",
|
||||
"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"
|
||||
},
|
||||
"keywords": ["pwa", "nodejs", "express", "telegram", "portfolio", "calculator"],
|
||||
"author": "SmartSolTech",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"express": "^4.18.2",
|
||||
"mongoose": "^8.0.3",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"express-session": "^1.17.3",
|
||||
"connect-mongo": "^5.1.0",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"sharp": "^0.33.0",
|
||||
"node-telegram-bot-api": "^0.64.0",
|
||||
"express-rate-limit": "^7.1.5",
|
||||
"helmet": "^7.1.0",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.3.1",
|
||||
"compression": "^1.7.4",
|
||||
"morgan": "^1.10.0",
|
||||
"nodemailer": "^6.9.7",
|
||||
"express-validator": "^7.0.1",
|
||||
"socket.io": "^4.7.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^3.0.2",
|
||||
"webpack": "^5.89.0",
|
||||
"webpack-cli": "^5.1.4",
|
||||
"css-loader": "^6.8.1",
|
||||
"style-loader": "^3.3.3",
|
||||
"file-loader": "^6.2.0",
|
||||
"html-webpack-plugin": "^5.5.3",
|
||||
"mini-css-extract-plugin": "^2.7.6",
|
||||
"terser-webpack-plugin": "^5.3.9",
|
||||
"workbox-webpack-plugin": "^7.0.0"
|
||||
}
|
||||
}
|
||||
49
.history/package_20251019163809.json
Normal file
49
.history/package_20251019163809.json
Normal file
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"name": "smartsoltech-website",
|
||||
"version": "1.0.0",
|
||||
"description": "Modern PWA website for SmartSolTech with admin panel and Telegram integration",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
"start": "node server.js",
|
||||
"demo": "node server-demo.js",
|
||||
"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"
|
||||
},
|
||||
"keywords": ["pwa", "nodejs", "express", "telegram", "portfolio", "calculator"],
|
||||
"author": "SmartSolTech",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"express": "^4.18.2",
|
||||
"mongoose": "^8.0.3",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"express-session": "^1.17.3",
|
||||
"connect-mongo": "^5.1.0",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"sharp": "^0.33.0",
|
||||
"node-telegram-bot-api": "^0.64.0",
|
||||
"express-rate-limit": "^7.1.5",
|
||||
"helmet": "^7.1.0",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.3.1",
|
||||
"compression": "^1.7.4",
|
||||
"morgan": "^1.10.0",
|
||||
"nodemailer": "^6.9.7",
|
||||
"express-validator": "^7.0.1",
|
||||
"socket.io": "^4.7.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^3.0.2",
|
||||
"webpack": "^5.89.0",
|
||||
"webpack-cli": "^5.1.4",
|
||||
"css-loader": "^6.8.1",
|
||||
"style-loader": "^3.3.3",
|
||||
"file-loader": "^6.2.0",
|
||||
"html-webpack-plugin": "^5.5.3",
|
||||
"mini-css-extract-plugin": "^2.7.6",
|
||||
"terser-webpack-plugin": "^5.3.9",
|
||||
"workbox-webpack-plugin": "^7.0.0"
|
||||
}
|
||||
}
|
||||
49
.history/package_20251019163858.json
Normal file
49
.history/package_20251019163858.json
Normal file
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"name": "smartsoltech-website",
|
||||
"version": "1.0.0",
|
||||
"description": "Modern PWA website for SmartSolTech with admin panel and Telegram integration",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
"start": "node server.js",
|
||||
"demo": "node server-demo.js",
|
||||
"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"
|
||||
},
|
||||
"keywords": ["pwa", "nodejs", "express", "telegram", "portfolio", "calculator"],
|
||||
"author": "SmartSolTech",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"express": "^4.18.2",
|
||||
"mongoose": "^8.0.3",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"express-session": "^1.17.3",
|
||||
"connect-mongo": "^5.1.0",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"sharp": "^0.33.0",
|
||||
"node-telegram-bot-api": "^0.64.0",
|
||||
"express-rate-limit": "^7.1.5",
|
||||
"helmet": "^7.1.0",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.3.1",
|
||||
"compression": "^1.7.4",
|
||||
"morgan": "^1.10.0",
|
||||
"nodemailer": "^6.9.7",
|
||||
"express-validator": "^7.0.1",
|
||||
"socket.io": "^4.7.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^3.0.2",
|
||||
"webpack": "^5.89.0",
|
||||
"webpack-cli": "^5.1.4",
|
||||
"css-loader": "^6.8.1",
|
||||
"style-loader": "^3.3.3",
|
||||
"file-loader": "^6.2.0",
|
||||
"html-webpack-plugin": "^5.5.3",
|
||||
"mini-css-extract-plugin": "^2.7.6",
|
||||
"terser-webpack-plugin": "^5.3.9",
|
||||
"workbox-webpack-plugin": "^7.0.0"
|
||||
}
|
||||
}
|
||||
88
.history/public/css/custom_20251019164526.css
Normal file
88
.history/public/css/custom_20251019164526.css
Normal file
@@ -0,0 +1,88 @@
|
||||
/* SmartSolTech - Main Styles */
|
||||
|
||||
/* Basic reset */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
/* Additional styles for demo */
|
||||
.hero-section {
|
||||
padding: 6rem 0 4rem;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.section-padding {
|
||||
padding: 4rem 0;
|
||||
}
|
||||
|
||||
.card-hover {
|
||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||
}
|
||||
|
||||
.card-hover:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.btn-gradient {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.btn-gradient:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.3);
|
||||
}
|
||||
|
||||
/* Loading spinner */
|
||||
.loading {
|
||||
display: inline-block;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 3px solid rgba(255, 255, 255, 0.3);
|
||||
border-radius: 50%;
|
||||
border-top-color: #fff;
|
||||
animation: spin 1s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* Animations */
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-fade-in-up {
|
||||
animation: fadeInUp 0.6s ease-out;
|
||||
}
|
||||
|
||||
/* Responsive utilities */
|
||||
@media (max-width: 768px) {
|
||||
.hero-section {
|
||||
padding: 4rem 0 3rem;
|
||||
}
|
||||
|
||||
.section-padding {
|
||||
padding: 2rem 0;
|
||||
}
|
||||
}
|
||||
88
.history/public/css/custom_20251019165556.css
Normal file
88
.history/public/css/custom_20251019165556.css
Normal file
@@ -0,0 +1,88 @@
|
||||
/* SmartSolTech - Main Styles */
|
||||
|
||||
/* Basic reset */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
/* Additional styles for demo */
|
||||
.hero-section {
|
||||
padding: 6rem 0 4rem;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.section-padding {
|
||||
padding: 4rem 0;
|
||||
}
|
||||
|
||||
.card-hover {
|
||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||
}
|
||||
|
||||
.card-hover:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.btn-gradient {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.btn-gradient:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.3);
|
||||
}
|
||||
|
||||
/* Loading spinner */
|
||||
.loading {
|
||||
display: inline-block;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 3px solid rgba(255, 255, 255, 0.3);
|
||||
border-radius: 50%;
|
||||
border-top-color: #fff;
|
||||
animation: spin 1s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* Animations */
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-fade-in-up {
|
||||
animation: fadeInUp 0.6s ease-out;
|
||||
}
|
||||
|
||||
/* Responsive utilities */
|
||||
@media (max-width: 768px) {
|
||||
.hero-section {
|
||||
padding: 4rem 0 3rem;
|
||||
}
|
||||
|
||||
.section-padding {
|
||||
padding: 2rem 0;
|
||||
}
|
||||
}
|
||||
315
.history/public/css/dark-theme_20251019173803.css
Normal file
315
.history/public/css/dark-theme_20251019173803.css
Normal file
@@ -0,0 +1,315 @@
|
||||
/* Dark Theme Support for SmartSolTech */
|
||||
|
||||
/* Base Dark Theme */
|
||||
html.dark {
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
.dark body {
|
||||
background-color: #111827;
|
||||
color: #f9fafb;
|
||||
}
|
||||
|
||||
/* Navigation Dark Theme */
|
||||
.dark nav {
|
||||
background-color: #1f2937;
|
||||
border-color: #374151;
|
||||
}
|
||||
|
||||
.dark .nav-link {
|
||||
color: #d1d5db;
|
||||
}
|
||||
|
||||
.dark .nav-link:hover {
|
||||
color: #60a5fa;
|
||||
}
|
||||
|
||||
.dark .nav-link.active {
|
||||
color: #60a5fa;
|
||||
}
|
||||
|
||||
/* Sections Dark Theme */
|
||||
.dark section {
|
||||
background-color: #111827;
|
||||
color: #f9fafb;
|
||||
}
|
||||
|
||||
.dark .bg-white {
|
||||
background-color: #1f2937 !important;
|
||||
}
|
||||
|
||||
.dark .bg-gray-50 {
|
||||
background-color: #111827 !important;
|
||||
}
|
||||
|
||||
.dark .bg-gray-100 {
|
||||
background-color: #374151 !important;
|
||||
}
|
||||
|
||||
.dark .bg-gray-900 {
|
||||
background-color: #030712 !important;
|
||||
}
|
||||
|
||||
/* Text Colors Dark Theme */
|
||||
.dark .text-gray-900 {
|
||||
color: #f9fafb !important;
|
||||
}
|
||||
|
||||
.dark .text-gray-800 {
|
||||
color: #e5e7eb !important;
|
||||
}
|
||||
|
||||
.dark .text-gray-700 {
|
||||
color: #d1d5db !important;
|
||||
}
|
||||
|
||||
.dark .text-gray-600 {
|
||||
color: #9ca3af !important;
|
||||
}
|
||||
|
||||
.dark .text-gray-500 {
|
||||
color: #6b7280 !important;
|
||||
}
|
||||
|
||||
.dark .text-gray-400 {
|
||||
color: #9ca3af !important;
|
||||
}
|
||||
|
||||
.dark .text-gray-300 {
|
||||
color: #d1d5db !important;
|
||||
}
|
||||
|
||||
/* Cards Dark Theme */
|
||||
.dark .card-hover {
|
||||
background-color: #1f2937;
|
||||
border-color: #374151;
|
||||
}
|
||||
|
||||
.dark .card-hover:hover {
|
||||
background-color: #374151;
|
||||
box-shadow: 0 25px 50px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
/* Forms Dark Theme */
|
||||
.dark .form-input {
|
||||
background-color: #374151;
|
||||
border-color: #4b5563;
|
||||
color: #f9fafb;
|
||||
}
|
||||
|
||||
.dark .form-input:focus {
|
||||
border-color: #60a5fa;
|
||||
background-color: #1f2937;
|
||||
}
|
||||
|
||||
.dark .form-input::placeholder {
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.dark input,
|
||||
.dark textarea,
|
||||
.dark select {
|
||||
background-color: #374151;
|
||||
border-color: #4b5563;
|
||||
color: #f9fafb;
|
||||
}
|
||||
|
||||
.dark input:focus,
|
||||
.dark textarea:focus,
|
||||
.dark select:focus {
|
||||
border-color: #60a5fa;
|
||||
background-color: #1f2937;
|
||||
}
|
||||
|
||||
/* Borders Dark Theme */
|
||||
.dark .border-gray-300 {
|
||||
border-color: #4b5563 !important;
|
||||
}
|
||||
|
||||
.dark .border-gray-200 {
|
||||
border-color: #374151 !important;
|
||||
}
|
||||
|
||||
.dark .border-t {
|
||||
border-color: #374151;
|
||||
}
|
||||
|
||||
/* Contact Form Dark Theme */
|
||||
.dark .contact-form {
|
||||
background: linear-gradient(145deg, rgba(31,41,55,0.9), rgba(31,41,55,0.95));
|
||||
border-color: #374151;
|
||||
}
|
||||
|
||||
/* Service Cards Dark Theme */
|
||||
.dark .service-card {
|
||||
background: linear-gradient(145deg, rgba(31,41,55,0.8), rgba(31,41,55,0.6));
|
||||
border-color: #374151;
|
||||
}
|
||||
|
||||
/* Team Cards Dark Theme */
|
||||
.dark .team-card {
|
||||
background-color: #1f2937;
|
||||
border-color: #374151;
|
||||
}
|
||||
|
||||
/* Portfolio Items Dark Theme */
|
||||
.dark .portfolio-item {
|
||||
background-color: #1f2937;
|
||||
}
|
||||
|
||||
/* Hero Section Dark Theme */
|
||||
.dark .hero-section {
|
||||
background: linear-gradient(135deg, #0f172a 0%, #1e293b 50%, #0f172a 100%);
|
||||
}
|
||||
|
||||
/* Shadows Dark Theme */
|
||||
.dark .shadow-lg {
|
||||
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.3), 0 4px 6px -2px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.dark .shadow-xl {
|
||||
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.3), 0 10px 10px -5px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.dark .shadow-2xl {
|
||||
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
/* Dropdown Dark Theme */
|
||||
.dark .dropdown-menu {
|
||||
background-color: #1f2937;
|
||||
border-color: #374151;
|
||||
}
|
||||
|
||||
.dark .dropdown-menu a {
|
||||
color: #d1d5db;
|
||||
}
|
||||
|
||||
.dark .dropdown-menu a:hover {
|
||||
background-color: #374151;
|
||||
color: #f9fafb;
|
||||
}
|
||||
|
||||
/* Icons Dark Theme */
|
||||
.dark .text-blue-600 {
|
||||
color: #60a5fa !important;
|
||||
}
|
||||
|
||||
.dark .text-purple-600 {
|
||||
color: #a78bfa !important;
|
||||
}
|
||||
|
||||
.dark .text-green-600 {
|
||||
color: #34d399 !important;
|
||||
}
|
||||
|
||||
.dark .text-yellow-600 {
|
||||
color: #fbbf24 !important;
|
||||
}
|
||||
|
||||
/* Buttons Dark Theme */
|
||||
.dark .btn-primary {
|
||||
background: linear-gradient(135deg, #3b82f6, #1d4ed8);
|
||||
}
|
||||
|
||||
.dark .btn-primary:hover {
|
||||
background: linear-gradient(135deg, #1d4ed8, #1e40af);
|
||||
}
|
||||
|
||||
/* Footer Dark Theme */
|
||||
.dark footer {
|
||||
background-color: #030712;
|
||||
border-color: #1f2937;
|
||||
}
|
||||
|
||||
.dark .footer-gradient {
|
||||
background: linear-gradient(135deg, #030712 0%, #111827 100%);
|
||||
}
|
||||
|
||||
/* Scrollbar Dark Theme */
|
||||
.dark ::-webkit-scrollbar-track {
|
||||
background: #1f2937;
|
||||
}
|
||||
|
||||
.dark ::-webkit-scrollbar-thumb {
|
||||
background: linear-gradient(135deg, #3b82f6, #1d4ed8);
|
||||
}
|
||||
|
||||
/* Technology Stack Dark Theme */
|
||||
.dark .tech-stack {
|
||||
background-color: #030712;
|
||||
}
|
||||
|
||||
.dark .tech-card {
|
||||
background-color: #1f2937;
|
||||
border-color: #374151;
|
||||
}
|
||||
|
||||
/* CTA Section Dark Theme */
|
||||
.dark .cta-section {
|
||||
background: linear-gradient(135deg, #1e40af 0%, #7c3aed 100%);
|
||||
}
|
||||
|
||||
/* Testimonials Dark Theme */
|
||||
.dark .testimonial-card {
|
||||
background: rgba(31, 41, 55, 0.8);
|
||||
border-color: #374151;
|
||||
}
|
||||
|
||||
/* Alert Messages Dark Theme */
|
||||
.dark .alert-success {
|
||||
background: rgba(16, 185, 129, 0.1);
|
||||
border-color: rgba(16, 185, 129, 0.3);
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
.dark .alert-error {
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
border-color: rgba(239, 68, 68, 0.3);
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
/* Mobile Menu Dark Theme */
|
||||
.dark #mobile-menu {
|
||||
background-color: #1f2937;
|
||||
border-color: #374151;
|
||||
}
|
||||
|
||||
/* Language Dropdown Dark Theme */
|
||||
.dark #mobile-language-menu {
|
||||
background-color: #1f2937;
|
||||
border-color: #374151;
|
||||
}
|
||||
|
||||
/* Smooth Theme Transition */
|
||||
* {
|
||||
transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease;
|
||||
}
|
||||
|
||||
/* Print styles for dark theme */
|
||||
@media print {
|
||||
.dark * {
|
||||
background: white !important;
|
||||
color: black !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* High contrast mode */
|
||||
@media (prefers-contrast: high) {
|
||||
.dark {
|
||||
--tw-bg-opacity: 1;
|
||||
--tw-text-opacity: 1;
|
||||
}
|
||||
|
||||
.dark .border {
|
||||
border-width: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Reduced motion for accessibility */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.dark * {
|
||||
transition: none !important;
|
||||
animation: none !important;
|
||||
}
|
||||
}
|
||||
315
.history/public/css/dark-theme_20251019174052.css
Normal file
315
.history/public/css/dark-theme_20251019174052.css
Normal file
@@ -0,0 +1,315 @@
|
||||
/* Dark Theme Support for SmartSolTech */
|
||||
|
||||
/* Base Dark Theme */
|
||||
html.dark {
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
.dark body {
|
||||
background-color: #111827;
|
||||
color: #f9fafb;
|
||||
}
|
||||
|
||||
/* Navigation Dark Theme */
|
||||
.dark nav {
|
||||
background-color: #1f2937;
|
||||
border-color: #374151;
|
||||
}
|
||||
|
||||
.dark .nav-link {
|
||||
color: #d1d5db;
|
||||
}
|
||||
|
||||
.dark .nav-link:hover {
|
||||
color: #60a5fa;
|
||||
}
|
||||
|
||||
.dark .nav-link.active {
|
||||
color: #60a5fa;
|
||||
}
|
||||
|
||||
/* Sections Dark Theme */
|
||||
.dark section {
|
||||
background-color: #111827;
|
||||
color: #f9fafb;
|
||||
}
|
||||
|
||||
.dark .bg-white {
|
||||
background-color: #1f2937 !important;
|
||||
}
|
||||
|
||||
.dark .bg-gray-50 {
|
||||
background-color: #111827 !important;
|
||||
}
|
||||
|
||||
.dark .bg-gray-100 {
|
||||
background-color: #374151 !important;
|
||||
}
|
||||
|
||||
.dark .bg-gray-900 {
|
||||
background-color: #030712 !important;
|
||||
}
|
||||
|
||||
/* Text Colors Dark Theme */
|
||||
.dark .text-gray-900 {
|
||||
color: #f9fafb !important;
|
||||
}
|
||||
|
||||
.dark .text-gray-800 {
|
||||
color: #e5e7eb !important;
|
||||
}
|
||||
|
||||
.dark .text-gray-700 {
|
||||
color: #d1d5db !important;
|
||||
}
|
||||
|
||||
.dark .text-gray-600 {
|
||||
color: #9ca3af !important;
|
||||
}
|
||||
|
||||
.dark .text-gray-500 {
|
||||
color: #6b7280 !important;
|
||||
}
|
||||
|
||||
.dark .text-gray-400 {
|
||||
color: #9ca3af !important;
|
||||
}
|
||||
|
||||
.dark .text-gray-300 {
|
||||
color: #d1d5db !important;
|
||||
}
|
||||
|
||||
/* Cards Dark Theme */
|
||||
.dark .card-hover {
|
||||
background-color: #1f2937;
|
||||
border-color: #374151;
|
||||
}
|
||||
|
||||
.dark .card-hover:hover {
|
||||
background-color: #374151;
|
||||
box-shadow: 0 25px 50px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
/* Forms Dark Theme */
|
||||
.dark .form-input {
|
||||
background-color: #374151;
|
||||
border-color: #4b5563;
|
||||
color: #f9fafb;
|
||||
}
|
||||
|
||||
.dark .form-input:focus {
|
||||
border-color: #60a5fa;
|
||||
background-color: #1f2937;
|
||||
}
|
||||
|
||||
.dark .form-input::placeholder {
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.dark input,
|
||||
.dark textarea,
|
||||
.dark select {
|
||||
background-color: #374151;
|
||||
border-color: #4b5563;
|
||||
color: #f9fafb;
|
||||
}
|
||||
|
||||
.dark input:focus,
|
||||
.dark textarea:focus,
|
||||
.dark select:focus {
|
||||
border-color: #60a5fa;
|
||||
background-color: #1f2937;
|
||||
}
|
||||
|
||||
/* Borders Dark Theme */
|
||||
.dark .border-gray-300 {
|
||||
border-color: #4b5563 !important;
|
||||
}
|
||||
|
||||
.dark .border-gray-200 {
|
||||
border-color: #374151 !important;
|
||||
}
|
||||
|
||||
.dark .border-t {
|
||||
border-color: #374151;
|
||||
}
|
||||
|
||||
/* Contact Form Dark Theme */
|
||||
.dark .contact-form {
|
||||
background: linear-gradient(145deg, rgba(31,41,55,0.9), rgba(31,41,55,0.95));
|
||||
border-color: #374151;
|
||||
}
|
||||
|
||||
/* Service Cards Dark Theme */
|
||||
.dark .service-card {
|
||||
background: linear-gradient(145deg, rgba(31,41,55,0.8), rgba(31,41,55,0.6));
|
||||
border-color: #374151;
|
||||
}
|
||||
|
||||
/* Team Cards Dark Theme */
|
||||
.dark .team-card {
|
||||
background-color: #1f2937;
|
||||
border-color: #374151;
|
||||
}
|
||||
|
||||
/* Portfolio Items Dark Theme */
|
||||
.dark .portfolio-item {
|
||||
background-color: #1f2937;
|
||||
}
|
||||
|
||||
/* Hero Section Dark Theme */
|
||||
.dark .hero-section {
|
||||
background: linear-gradient(135deg, #0f172a 0%, #1e293b 50%, #0f172a 100%);
|
||||
}
|
||||
|
||||
/* Shadows Dark Theme */
|
||||
.dark .shadow-lg {
|
||||
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.3), 0 4px 6px -2px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.dark .shadow-xl {
|
||||
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.3), 0 10px 10px -5px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.dark .shadow-2xl {
|
||||
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
/* Dropdown Dark Theme */
|
||||
.dark .dropdown-menu {
|
||||
background-color: #1f2937;
|
||||
border-color: #374151;
|
||||
}
|
||||
|
||||
.dark .dropdown-menu a {
|
||||
color: #d1d5db;
|
||||
}
|
||||
|
||||
.dark .dropdown-menu a:hover {
|
||||
background-color: #374151;
|
||||
color: #f9fafb;
|
||||
}
|
||||
|
||||
/* Icons Dark Theme */
|
||||
.dark .text-blue-600 {
|
||||
color: #60a5fa !important;
|
||||
}
|
||||
|
||||
.dark .text-purple-600 {
|
||||
color: #a78bfa !important;
|
||||
}
|
||||
|
||||
.dark .text-green-600 {
|
||||
color: #34d399 !important;
|
||||
}
|
||||
|
||||
.dark .text-yellow-600 {
|
||||
color: #fbbf24 !important;
|
||||
}
|
||||
|
||||
/* Buttons Dark Theme */
|
||||
.dark .btn-primary {
|
||||
background: linear-gradient(135deg, #3b82f6, #1d4ed8);
|
||||
}
|
||||
|
||||
.dark .btn-primary:hover {
|
||||
background: linear-gradient(135deg, #1d4ed8, #1e40af);
|
||||
}
|
||||
|
||||
/* Footer Dark Theme */
|
||||
.dark footer {
|
||||
background-color: #030712;
|
||||
border-color: #1f2937;
|
||||
}
|
||||
|
||||
.dark .footer-gradient {
|
||||
background: linear-gradient(135deg, #030712 0%, #111827 100%);
|
||||
}
|
||||
|
||||
/* Scrollbar Dark Theme */
|
||||
.dark ::-webkit-scrollbar-track {
|
||||
background: #1f2937;
|
||||
}
|
||||
|
||||
.dark ::-webkit-scrollbar-thumb {
|
||||
background: linear-gradient(135deg, #3b82f6, #1d4ed8);
|
||||
}
|
||||
|
||||
/* Technology Stack Dark Theme */
|
||||
.dark .tech-stack {
|
||||
background-color: #030712;
|
||||
}
|
||||
|
||||
.dark .tech-card {
|
||||
background-color: #1f2937;
|
||||
border-color: #374151;
|
||||
}
|
||||
|
||||
/* CTA Section Dark Theme */
|
||||
.dark .cta-section {
|
||||
background: linear-gradient(135deg, #1e40af 0%, #7c3aed 100%);
|
||||
}
|
||||
|
||||
/* Testimonials Dark Theme */
|
||||
.dark .testimonial-card {
|
||||
background: rgba(31, 41, 55, 0.8);
|
||||
border-color: #374151;
|
||||
}
|
||||
|
||||
/* Alert Messages Dark Theme */
|
||||
.dark .alert-success {
|
||||
background: rgba(16, 185, 129, 0.1);
|
||||
border-color: rgba(16, 185, 129, 0.3);
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
.dark .alert-error {
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
border-color: rgba(239, 68, 68, 0.3);
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
/* Mobile Menu Dark Theme */
|
||||
.dark #mobile-menu {
|
||||
background-color: #1f2937;
|
||||
border-color: #374151;
|
||||
}
|
||||
|
||||
/* Language Dropdown Dark Theme */
|
||||
.dark #mobile-language-menu {
|
||||
background-color: #1f2937;
|
||||
border-color: #374151;
|
||||
}
|
||||
|
||||
/* Smooth Theme Transition */
|
||||
* {
|
||||
transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease;
|
||||
}
|
||||
|
||||
/* Print styles for dark theme */
|
||||
@media print {
|
||||
.dark * {
|
||||
background: white !important;
|
||||
color: black !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* High contrast mode */
|
||||
@media (prefers-contrast: high) {
|
||||
.dark {
|
||||
--tw-bg-opacity: 1;
|
||||
--tw-text-opacity: 1;
|
||||
}
|
||||
|
||||
.dark .border {
|
||||
border-width: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Reduced motion for accessibility */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.dark * {
|
||||
transition: none !important;
|
||||
animation: none !important;
|
||||
}
|
||||
}
|
||||
281
.history/public/css/fixes_20251019170837.css
Normal file
281
.history/public/css/fixes_20251019170837.css
Normal file
@@ -0,0 +1,281 @@
|
||||
/* SmartSolTech - Design Fixes & Enhancements */
|
||||
|
||||
/* Glass effect improvements */
|
||||
.glass-effect {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||||
/* Fallback for browsers that don't support backdrop-filter */
|
||||
}
|
||||
|
||||
/* Support backdrop-filter for modern browsers */
|
||||
@supports (backdrop-filter: blur(10px)) {
|
||||
.glass-effect {
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
}
|
||||
}
|
||||
|
||||
/* Hero section improvements */
|
||||
.hero-section {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* Background blob animations */
|
||||
@keyframes blob {
|
||||
0% { transform: translate(0px, 0px) scale(1); }
|
||||
33% { transform: translate(30px, -50px) scale(1.1); }
|
||||
66% { transform: translate(-20px, 20px) scale(0.9); }
|
||||
100% { transform: translate(0px, 0px) scale(1); }
|
||||
}
|
||||
|
||||
.animate-blob {
|
||||
animation: blob 7s infinite;
|
||||
}
|
||||
|
||||
.animation-delay-2000 {
|
||||
animation-delay: 2s;
|
||||
}
|
||||
|
||||
.animation-delay-4000 {
|
||||
animation-delay: 4s;
|
||||
}
|
||||
|
||||
/* Enhanced card hover effects */
|
||||
.card-hover {
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
backface-visibility: hidden;
|
||||
}
|
||||
|
||||
.card-hover:hover {
|
||||
transform: translateY(-8px) scale(1.02);
|
||||
box-shadow: 0 25px 50px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
/* Portfolio item enhancements */
|
||||
.portfolio-item {
|
||||
overflow: hidden;
|
||||
border-radius: 16px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.portfolio-image {
|
||||
transition: transform 0.5s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.portfolio-item:hover .portfolio-image {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
/* Button improvements */
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, #3B82F6, #1D4ED8);
|
||||
border: none;
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.btn-primary::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.3), transparent);
|
||||
transition: left 0.5s;
|
||||
}
|
||||
|
||||
.btn-primary:hover::before {
|
||||
left: 100%;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: linear-gradient(135deg, #1D4ED8, #1E40AF);
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 15px 35px rgba(59, 130, 246, 0.4);
|
||||
}
|
||||
|
||||
/* Navigation improvements */
|
||||
.nav-link {
|
||||
position: relative;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.nav-link::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -2px;
|
||||
left: 50%;
|
||||
width: 0;
|
||||
height: 2px;
|
||||
background: linear-gradient(90deg, #3B82F6, #8B5CF6);
|
||||
transition: all 0.3s ease;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.nav-link:hover::after,
|
||||
.nav-link.active::after {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Form improvements */
|
||||
.form-input {
|
||||
transition: all 0.3s ease;
|
||||
border: 2px solid #E5E7EB;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
}
|
||||
|
||||
.form-input:focus {
|
||||
border-color: #3B82F6;
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* Contact form styling */
|
||||
.contact-form {
|
||||
background: linear-gradient(145deg, rgba(255,255,255,0.9), rgba(255,255,255,0.95));
|
||||
border: 1px solid rgba(255,255,255,0.3);
|
||||
box-shadow: 0 20px 40px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
/* CTA section improvements */
|
||||
.cta-section {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.cta-section::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><defs><pattern id="grain" width="100" height="100" patternUnits="userSpaceOnUse"><circle cx="25" cy="25" r="1" fill="rgba(255,255,255,0.1)"/><circle cx="75" cy="75" r="1" fill="rgba(255,255,255,0.1)"/></pattern></defs><rect width="100" height="100" fill="url(%23grain)"/></svg>');
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
/* Service cards */
|
||||
.service-card {
|
||||
background: linear-gradient(145deg, rgba(255,255,255,0.1), rgba(255,255,255,0.05));
|
||||
border: 1px solid rgba(255,255,255,0.2);
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
/* Team member cards */
|
||||
.team-card {
|
||||
transition: all 0.3s ease;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
}
|
||||
|
||||
.team-card:hover {
|
||||
transform: translateY(-10px);
|
||||
box-shadow: 0 30px 60px rgba(0,0,0,0.12);
|
||||
}
|
||||
|
||||
/* Technology icons */
|
||||
.tech-icon {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.tech-icon:hover {
|
||||
transform: scale(1.1) rotate(5deg);
|
||||
filter: brightness(1.2);
|
||||
}
|
||||
|
||||
/* Loading states */
|
||||
.loading {
|
||||
opacity: 0.7;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.loading::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin: -10px 0 0 -10px;
|
||||
border: 2px solid #f3f3f3;
|
||||
border-top: 2px solid #3B82F6;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* Mobile optimizations */
|
||||
@media (max-width: 768px) {
|
||||
.card-hover:hover {
|
||||
transform: translateY(-4px) scale(1.01);
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.hero-section {
|
||||
min-height: 80vh;
|
||||
}
|
||||
|
||||
.portfolio-item:hover .portfolio-image {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
}
|
||||
|
||||
/* Accessibility improvements */
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
/* Focus styles for better accessibility */
|
||||
button:focus,
|
||||
input:focus,
|
||||
textarea:focus,
|
||||
select:focus,
|
||||
a:focus {
|
||||
outline: 2px solid #3B82F6;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* Smooth scrolling */
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
/* Print styles */
|
||||
@media print {
|
||||
.no-print {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
body {
|
||||
color: black !important;
|
||||
background: white !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Dark mode support (if needed) */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.auto-dark {
|
||||
color: #f9fafb;
|
||||
background-color: #111827;
|
||||
}
|
||||
}
|
||||
281
.history/public/css/fixes_20251019170927.css
Normal file
281
.history/public/css/fixes_20251019170927.css
Normal file
@@ -0,0 +1,281 @@
|
||||
/* SmartSolTech - Design Fixes & Enhancements */
|
||||
|
||||
/* Glass effect improvements */
|
||||
.glass-effect {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||||
/* Fallback for browsers that don't support backdrop-filter */
|
||||
}
|
||||
|
||||
/* Support backdrop-filter for modern browsers */
|
||||
@supports (backdrop-filter: blur(10px)) {
|
||||
.glass-effect {
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
}
|
||||
}
|
||||
|
||||
/* Hero section improvements */
|
||||
.hero-section {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* Background blob animations */
|
||||
@keyframes blob {
|
||||
0% { transform: translate(0px, 0px) scale(1); }
|
||||
33% { transform: translate(30px, -50px) scale(1.1); }
|
||||
66% { transform: translate(-20px, 20px) scale(0.9); }
|
||||
100% { transform: translate(0px, 0px) scale(1); }
|
||||
}
|
||||
|
||||
.animate-blob {
|
||||
animation: blob 7s infinite;
|
||||
}
|
||||
|
||||
.animation-delay-2000 {
|
||||
animation-delay: 2s;
|
||||
}
|
||||
|
||||
.animation-delay-4000 {
|
||||
animation-delay: 4s;
|
||||
}
|
||||
|
||||
/* Enhanced card hover effects */
|
||||
.card-hover {
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
backface-visibility: hidden;
|
||||
}
|
||||
|
||||
.card-hover:hover {
|
||||
transform: translateY(-8px) scale(1.02);
|
||||
box-shadow: 0 25px 50px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
/* Portfolio item enhancements */
|
||||
.portfolio-item {
|
||||
overflow: hidden;
|
||||
border-radius: 16px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.portfolio-image {
|
||||
transition: transform 0.5s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.portfolio-item:hover .portfolio-image {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
/* Button improvements */
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, #3B82F6, #1D4ED8);
|
||||
border: none;
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.btn-primary::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.3), transparent);
|
||||
transition: left 0.5s;
|
||||
}
|
||||
|
||||
.btn-primary:hover::before {
|
||||
left: 100%;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: linear-gradient(135deg, #1D4ED8, #1E40AF);
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 15px 35px rgba(59, 130, 246, 0.4);
|
||||
}
|
||||
|
||||
/* Navigation improvements */
|
||||
.nav-link {
|
||||
position: relative;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.nav-link::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -2px;
|
||||
left: 50%;
|
||||
width: 0;
|
||||
height: 2px;
|
||||
background: linear-gradient(90deg, #3B82F6, #8B5CF6);
|
||||
transition: all 0.3s ease;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.nav-link:hover::after,
|
||||
.nav-link.active::after {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Form improvements */
|
||||
.form-input {
|
||||
transition: all 0.3s ease;
|
||||
border: 2px solid #E5E7EB;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
}
|
||||
|
||||
.form-input:focus {
|
||||
border-color: #3B82F6;
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* Contact form styling */
|
||||
.contact-form {
|
||||
background: linear-gradient(145deg, rgba(255,255,255,0.9), rgba(255,255,255,0.95));
|
||||
border: 1px solid rgba(255,255,255,0.3);
|
||||
box-shadow: 0 20px 40px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
/* CTA section improvements */
|
||||
.cta-section {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.cta-section::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><defs><pattern id="grain" width="100" height="100" patternUnits="userSpaceOnUse"><circle cx="25" cy="25" r="1" fill="rgba(255,255,255,0.1)"/><circle cx="75" cy="75" r="1" fill="rgba(255,255,255,0.1)"/></pattern></defs><rect width="100" height="100" fill="url(%23grain)"/></svg>');
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
/* Service cards */
|
||||
.service-card {
|
||||
background: linear-gradient(145deg, rgba(255,255,255,0.1), rgba(255,255,255,0.05));
|
||||
border: 1px solid rgba(255,255,255,0.2);
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
/* Team member cards */
|
||||
.team-card {
|
||||
transition: all 0.3s ease;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
}
|
||||
|
||||
.team-card:hover {
|
||||
transform: translateY(-10px);
|
||||
box-shadow: 0 30px 60px rgba(0,0,0,0.12);
|
||||
}
|
||||
|
||||
/* Technology icons */
|
||||
.tech-icon {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.tech-icon:hover {
|
||||
transform: scale(1.1) rotate(5deg);
|
||||
filter: brightness(1.2);
|
||||
}
|
||||
|
||||
/* Loading states */
|
||||
.loading {
|
||||
opacity: 0.7;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.loading::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin: -10px 0 0 -10px;
|
||||
border: 2px solid #f3f3f3;
|
||||
border-top: 2px solid #3B82F6;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* Mobile optimizations */
|
||||
@media (max-width: 768px) {
|
||||
.card-hover:hover {
|
||||
transform: translateY(-4px) scale(1.01);
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.hero-section {
|
||||
min-height: 80vh;
|
||||
}
|
||||
|
||||
.portfolio-item:hover .portfolio-image {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
}
|
||||
|
||||
/* Accessibility improvements */
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
/* Focus styles for better accessibility */
|
||||
button:focus,
|
||||
input:focus,
|
||||
textarea:focus,
|
||||
select:focus,
|
||||
a:focus {
|
||||
outline: 2px solid #3B82F6;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* Smooth scrolling */
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
/* Print styles */
|
||||
@media print {
|
||||
.no-print {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
body {
|
||||
color: black !important;
|
||||
background: white !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Dark mode support (if needed) */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.auto-dark {
|
||||
color: #f9fafb;
|
||||
background-color: #111827;
|
||||
}
|
||||
}
|
||||
425
.history/public/css/main_20251019161505.css
Normal file
425
.history/public/css/main_20251019161505.css
Normal file
@@ -0,0 +1,425 @@
|
||||
/* Custom CSS for SmartSolTech */
|
||||
:root {
|
||||
--primary-color: #3b82f6;
|
||||
--secondary-color: #8b5cf6;
|
||||
--accent-color: #10b981;
|
||||
--text-dark: #1f2937;
|
||||
--text-light: #6b7280;
|
||||
--bg-light: #f9fafb;
|
||||
--border-color: #e5e7eb;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: var(--text-dark);
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
/* Loading Animation */
|
||||
.loading {
|
||||
display: inline-block;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 3px solid rgba(255, 255, 255, 0.3);
|
||||
border-radius: 50%;
|
||||
border-top-color: #fff;
|
||||
animation: spin 1s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* Navigation Enhancements */
|
||||
.navbar-scrolled {
|
||||
background-color: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(10px);
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* Mobile Menu Animation */
|
||||
.mobile-menu {
|
||||
transition: all 0.3s ease-in-out;
|
||||
max-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.mobile-menu.show {
|
||||
max-height: 500px;
|
||||
}
|
||||
|
||||
/* Button Hover Effects */
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
|
||||
transition: all 0.3s ease;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 10px 25px rgba(59, 130, 246, 0.3);
|
||||
}
|
||||
|
||||
/* Card Hover Effects */
|
||||
.card-hover {
|
||||
transition: all 0.3s ease;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.card-hover:hover {
|
||||
transform: translateY(-8px);
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* Portfolio Grid */
|
||||
.portfolio-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.portfolio-item {
|
||||
border-radius: 1rem;
|
||||
overflow: hidden;
|
||||
background: white;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.portfolio-item:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.portfolio-image {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
aspect-ratio: 16/10;
|
||||
}
|
||||
|
||||
.portfolio-image img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.portfolio-item:hover .portfolio-image img {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.portfolio-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: linear-gradient(135deg, rgba(59, 130, 246, 0.8), rgba(139, 92, 246, 0.8));
|
||||
opacity: 0;
|
||||
transition: all 0.3s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.portfolio-item:hover .portfolio-overlay {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Service Cards */
|
||||
.service-card {
|
||||
background: white;
|
||||
border-radius: 1rem;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
transition: all 0.3s ease;
|
||||
border: 2px solid transparent;
|
||||
}
|
||||
|
||||
.service-card:hover {
|
||||
border-color: var(--primary-color);
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 20px 40px rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
|
||||
.service-icon {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0 auto 1rem;
|
||||
font-size: 2rem;
|
||||
color: white;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.service-card:hover .service-icon {
|
||||
transform: scale(1.1) rotate(5deg);
|
||||
}
|
||||
|
||||
/* Contact Form */
|
||||
.contact-form {
|
||||
background: white;
|
||||
border-radius: 1rem;
|
||||
padding: 2rem;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
width: 100%;
|
||||
padding: 1rem;
|
||||
border: 2px solid var(--border-color);
|
||||
border-radius: 0.5rem;
|
||||
font-size: 1rem;
|
||||
transition: all 0.3s ease;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary-color);
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
|
||||
/* Calculator Styles */
|
||||
.calculator-step {
|
||||
animation: fadeIn 0.5s ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
.option-card {
|
||||
border: 2px solid var(--border-color);
|
||||
border-radius: 1rem;
|
||||
padding: 1.5rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.option-card:hover {
|
||||
border-color: var(--primary-color);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.option-card.selected {
|
||||
border-color: var(--primary-color);
|
||||
background: rgba(59, 130, 246, 0.05);
|
||||
}
|
||||
|
||||
/* Progress Bar */
|
||||
.progress-bar {
|
||||
width: 100%;
|
||||
height: 8px;
|
||||
background: var(--border-color);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, var(--primary-color), var(--secondary-color));
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
/* Hero Section Animations */
|
||||
.hero-content {
|
||||
animation: heroFadeIn 1s ease-out;
|
||||
}
|
||||
|
||||
@keyframes heroFadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Parallax Effect */
|
||||
.parallax {
|
||||
background-attachment: fixed;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
/* Scroll Animations */
|
||||
.fade-in-up {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
transition: all 0.8s ease;
|
||||
}
|
||||
|
||||
.fade-in-up.animate {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
/* Typography */
|
||||
.gradient-text {
|
||||
background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
/* Status Badges */
|
||||
.status-badge {
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 9999px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.status-new {
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.status-in-progress {
|
||||
background: rgba(245, 158, 11, 0.1);
|
||||
color: #d97706;
|
||||
}
|
||||
|
||||
.status-completed {
|
||||
background: rgba(16, 185, 129, 0.1);
|
||||
color: #059669;
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 768px) {
|
||||
.portfolio-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
|
||||
.service-card {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.calculator-step {
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Dark Mode Support */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--text-dark: #f9fafb;
|
||||
--text-light: #d1d5db;
|
||||
--bg-light: #1f2937;
|
||||
--border-color: #374151;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #111827;
|
||||
color: var(--text-dark);
|
||||
}
|
||||
|
||||
.card-hover, .service-card, .contact-form, .option-card {
|
||||
background: #1f2937;
|
||||
border-color: var(--border-color);
|
||||
}
|
||||
|
||||
.form-control {
|
||||
background: #374151;
|
||||
border-color: #4b5563;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
}
|
||||
|
||||
/* Print Styles */
|
||||
@media print {
|
||||
.navbar, .footer, .contact-form, .mobile-menu {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
body {
|
||||
font-size: 12pt;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.portfolio-item, .service-card {
|
||||
break-inside: avoid;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Accessibility */
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
/* Focus styles for better accessibility */
|
||||
.form-control:focus,
|
||||
.btn:focus,
|
||||
.option-card:focus {
|
||||
outline: 2px solid var(--primary-color);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* Loading States */
|
||||
.btn-loading {
|
||||
position: relative;
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
.btn-loading::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
margin-left: -8px;
|
||||
margin-top: -8px;
|
||||
border: 2px solid transparent;
|
||||
border-top-color: currentColor;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
425
.history/public/css/main_20251019162545.css
Normal file
425
.history/public/css/main_20251019162545.css
Normal file
@@ -0,0 +1,425 @@
|
||||
/* Custom CSS for SmartSolTech */
|
||||
:root {
|
||||
--primary-color: #3b82f6;
|
||||
--secondary-color: #8b5cf6;
|
||||
--accent-color: #10b981;
|
||||
--text-dark: #1f2937;
|
||||
--text-light: #6b7280;
|
||||
--bg-light: #f9fafb;
|
||||
--border-color: #e5e7eb;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: var(--text-dark);
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
/* Loading Animation */
|
||||
.loading {
|
||||
display: inline-block;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 3px solid rgba(255, 255, 255, 0.3);
|
||||
border-radius: 50%;
|
||||
border-top-color: #fff;
|
||||
animation: spin 1s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* Navigation Enhancements */
|
||||
.navbar-scrolled {
|
||||
background-color: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(10px);
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* Mobile Menu Animation */
|
||||
.mobile-menu {
|
||||
transition: all 0.3s ease-in-out;
|
||||
max-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.mobile-menu.show {
|
||||
max-height: 500px;
|
||||
}
|
||||
|
||||
/* Button Hover Effects */
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
|
||||
transition: all 0.3s ease;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 10px 25px rgba(59, 130, 246, 0.3);
|
||||
}
|
||||
|
||||
/* Card Hover Effects */
|
||||
.card-hover {
|
||||
transition: all 0.3s ease;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.card-hover:hover {
|
||||
transform: translateY(-8px);
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* Portfolio Grid */
|
||||
.portfolio-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.portfolio-item {
|
||||
border-radius: 1rem;
|
||||
overflow: hidden;
|
||||
background: white;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.portfolio-item:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.portfolio-image {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
aspect-ratio: 16/10;
|
||||
}
|
||||
|
||||
.portfolio-image img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.portfolio-item:hover .portfolio-image img {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.portfolio-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: linear-gradient(135deg, rgba(59, 130, 246, 0.8), rgba(139, 92, 246, 0.8));
|
||||
opacity: 0;
|
||||
transition: all 0.3s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.portfolio-item:hover .portfolio-overlay {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Service Cards */
|
||||
.service-card {
|
||||
background: white;
|
||||
border-radius: 1rem;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
transition: all 0.3s ease;
|
||||
border: 2px solid transparent;
|
||||
}
|
||||
|
||||
.service-card:hover {
|
||||
border-color: var(--primary-color);
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 20px 40px rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
|
||||
.service-icon {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0 auto 1rem;
|
||||
font-size: 2rem;
|
||||
color: white;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.service-card:hover .service-icon {
|
||||
transform: scale(1.1) rotate(5deg);
|
||||
}
|
||||
|
||||
/* Contact Form */
|
||||
.contact-form {
|
||||
background: white;
|
||||
border-radius: 1rem;
|
||||
padding: 2rem;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
width: 100%;
|
||||
padding: 1rem;
|
||||
border: 2px solid var(--border-color);
|
||||
border-radius: 0.5rem;
|
||||
font-size: 1rem;
|
||||
transition: all 0.3s ease;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary-color);
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
|
||||
/* Calculator Styles */
|
||||
.calculator-step {
|
||||
animation: fadeIn 0.5s ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
.option-card {
|
||||
border: 2px solid var(--border-color);
|
||||
border-radius: 1rem;
|
||||
padding: 1.5rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.option-card:hover {
|
||||
border-color: var(--primary-color);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.option-card.selected {
|
||||
border-color: var(--primary-color);
|
||||
background: rgba(59, 130, 246, 0.05);
|
||||
}
|
||||
|
||||
/* Progress Bar */
|
||||
.progress-bar {
|
||||
width: 100%;
|
||||
height: 8px;
|
||||
background: var(--border-color);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, var(--primary-color), var(--secondary-color));
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
/* Hero Section Animations */
|
||||
.hero-content {
|
||||
animation: heroFadeIn 1s ease-out;
|
||||
}
|
||||
|
||||
@keyframes heroFadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Parallax Effect */
|
||||
.parallax {
|
||||
background-attachment: fixed;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
/* Scroll Animations */
|
||||
.fade-in-up {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
transition: all 0.8s ease;
|
||||
}
|
||||
|
||||
.fade-in-up.animate {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
/* Typography */
|
||||
.gradient-text {
|
||||
background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
/* Status Badges */
|
||||
.status-badge {
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 9999px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.status-new {
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.status-in-progress {
|
||||
background: rgba(245, 158, 11, 0.1);
|
||||
color: #d97706;
|
||||
}
|
||||
|
||||
.status-completed {
|
||||
background: rgba(16, 185, 129, 0.1);
|
||||
color: #059669;
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 768px) {
|
||||
.portfolio-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
|
||||
.service-card {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.calculator-step {
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Dark Mode Support */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--text-dark: #f9fafb;
|
||||
--text-light: #d1d5db;
|
||||
--bg-light: #1f2937;
|
||||
--border-color: #374151;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #111827;
|
||||
color: var(--text-dark);
|
||||
}
|
||||
|
||||
.card-hover, .service-card, .contact-form, .option-card {
|
||||
background: #1f2937;
|
||||
border-color: var(--border-color);
|
||||
}
|
||||
|
||||
.form-control {
|
||||
background: #374151;
|
||||
border-color: #4b5563;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
}
|
||||
|
||||
/* Print Styles */
|
||||
@media print {
|
||||
.navbar, .footer, .contact-form, .mobile-menu {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
body {
|
||||
font-size: 12pt;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.portfolio-item, .service-card {
|
||||
break-inside: avoid;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Accessibility */
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
/* Focus styles for better accessibility */
|
||||
.form-control:focus,
|
||||
.btn:focus,
|
||||
.option-card:focus {
|
||||
outline: 2px solid var(--primary-color);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* Loading States */
|
||||
.btn-loading {
|
||||
position: relative;
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
.btn-loading::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
margin-left: -8px;
|
||||
margin-top: -8px;
|
||||
border: 2px solid transparent;
|
||||
border-top-color: currentColor;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
427
.history/public/css/main_20251019164257.css
Normal file
427
.history/public/css/main_20251019164257.css
Normal file
@@ -0,0 +1,427 @@
|
||||
/* SmartSolTech - Main Styles */
|
||||
|
||||
/* CSS Reset and Base */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
/* Utility Classes */
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
.section-padding {
|
||||
padding: 4rem 0;
|
||||
}
|
||||
|
||||
/* Loading Animation */
|
||||
.loading {
|
||||
display: inline-block;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 3px solid rgba(255, 255, 255, 0.3);
|
||||
border-radius: 50%;
|
||||
border-top-color: #fff;
|
||||
animation: spin 1s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* Navigation Enhancements */
|
||||
.navbar-scrolled {
|
||||
background-color: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(10px);
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* Mobile Menu Animation */
|
||||
.mobile-menu {
|
||||
transition: all 0.3s ease-in-out;
|
||||
max-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.mobile-menu.show {
|
||||
max-height: 500px;
|
||||
}
|
||||
|
||||
/* Button Hover Effects */
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
|
||||
transition: all 0.3s ease;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 10px 25px rgba(59, 130, 246, 0.3);
|
||||
}
|
||||
|
||||
/* Card Hover Effects */
|
||||
.card-hover {
|
||||
transition: all 0.3s ease;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.card-hover:hover {
|
||||
transform: translateY(-8px);
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* Portfolio Grid */
|
||||
.portfolio-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.portfolio-item {
|
||||
border-radius: 1rem;
|
||||
overflow: hidden;
|
||||
background: white;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.portfolio-item:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.portfolio-image {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
aspect-ratio: 16/10;
|
||||
}
|
||||
|
||||
.portfolio-image img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.portfolio-item:hover .portfolio-image img {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.portfolio-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: linear-gradient(135deg, rgba(59, 130, 246, 0.8), rgba(139, 92, 246, 0.8));
|
||||
opacity: 0;
|
||||
transition: all 0.3s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.portfolio-item:hover .portfolio-overlay {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Service Cards */
|
||||
.service-card {
|
||||
background: white;
|
||||
border-radius: 1rem;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
transition: all 0.3s ease;
|
||||
border: 2px solid transparent;
|
||||
}
|
||||
|
||||
.service-card:hover {
|
||||
border-color: var(--primary-color);
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 20px 40px rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
|
||||
.service-icon {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0 auto 1rem;
|
||||
font-size: 2rem;
|
||||
color: white;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.service-card:hover .service-icon {
|
||||
transform: scale(1.1) rotate(5deg);
|
||||
}
|
||||
|
||||
/* Contact Form */
|
||||
.contact-form {
|
||||
background: white;
|
||||
border-radius: 1rem;
|
||||
padding: 2rem;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
width: 100%;
|
||||
padding: 1rem;
|
||||
border: 2px solid var(--border-color);
|
||||
border-radius: 0.5rem;
|
||||
font-size: 1rem;
|
||||
transition: all 0.3s ease;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary-color);
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
|
||||
/* Calculator Styles */
|
||||
.calculator-step {
|
||||
animation: fadeIn 0.5s ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
.option-card {
|
||||
border: 2px solid var(--border-color);
|
||||
border-radius: 1rem;
|
||||
padding: 1.5rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.option-card:hover {
|
||||
border-color: var(--primary-color);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.option-card.selected {
|
||||
border-color: var(--primary-color);
|
||||
background: rgba(59, 130, 246, 0.05);
|
||||
}
|
||||
|
||||
/* Progress Bar */
|
||||
.progress-bar {
|
||||
width: 100%;
|
||||
height: 8px;
|
||||
background: var(--border-color);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, var(--primary-color), var(--secondary-color));
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
/* Hero Section Animations */
|
||||
.hero-content {
|
||||
animation: heroFadeIn 1s ease-out;
|
||||
}
|
||||
|
||||
@keyframes heroFadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Parallax Effect */
|
||||
.parallax {
|
||||
background-attachment: fixed;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
/* Scroll Animations */
|
||||
.fade-in-up {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
transition: all 0.8s ease;
|
||||
}
|
||||
|
||||
.fade-in-up.animate {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
/* Typography */
|
||||
.gradient-text {
|
||||
background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
/* Status Badges */
|
||||
.status-badge {
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 9999px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.status-new {
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.status-in-progress {
|
||||
background: rgba(245, 158, 11, 0.1);
|
||||
color: #d97706;
|
||||
}
|
||||
|
||||
.status-completed {
|
||||
background: rgba(16, 185, 129, 0.1);
|
||||
color: #059669;
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 768px) {
|
||||
.portfolio-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
|
||||
.service-card {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.calculator-step {
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Dark Mode Support */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--text-dark: #f9fafb;
|
||||
--text-light: #d1d5db;
|
||||
--bg-light: #1f2937;
|
||||
--border-color: #374151;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #111827;
|
||||
color: var(--text-dark);
|
||||
}
|
||||
|
||||
.card-hover, .service-card, .contact-form, .option-card {
|
||||
background: #1f2937;
|
||||
border-color: var(--border-color);
|
||||
}
|
||||
|
||||
.form-control {
|
||||
background: #374151;
|
||||
border-color: #4b5563;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
}
|
||||
|
||||
/* Print Styles */
|
||||
@media print {
|
||||
.navbar, .footer, .contact-form, .mobile-menu {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
body {
|
||||
font-size: 12pt;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.portfolio-item, .service-card {
|
||||
break-inside: avoid;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Accessibility */
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
/* Focus styles for better accessibility */
|
||||
.form-control:focus,
|
||||
.btn:focus,
|
||||
.option-card:focus {
|
||||
outline: 2px solid var(--primary-color);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* Loading States */
|
||||
.btn-loading {
|
||||
position: relative;
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
.btn-loading::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
margin-left: -8px;
|
||||
margin-top: -8px;
|
||||
border: 2px solid transparent;
|
||||
border-top-color: currentColor;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
442
.history/public/css/main_20251019164506.css
Normal file
442
.history/public/css/main_20251019164506.css
Normal file
@@ -0,0 +1,442 @@
|
||||
/* SmartSolTech - Main Styles */
|
||||
|
||||
/* CSS Reset and Base */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
/* Utility Classes */
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
.section-padding {
|
||||
padding: 4rem 0;
|
||||
}
|
||||
|
||||
/* Navigation */
|
||||
.navbar {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
backdrop-filter: blur(10px);
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1000;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
font-size: 1.5rem;
|
||||
font-weight: bold;
|
||||
color: #3b82f6;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.navbar-nav {
|
||||
display: flex;
|
||||
gap: 2rem;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
color: #6b7280;
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 0.5rem;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.nav-link:hover,
|
||||
.nav-link.active {
|
||||
color: #3b82f6;
|
||||
background-color: #eff6ff;
|
||||
}
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.mobile-menu.show {
|
||||
max-height: 500px;
|
||||
}
|
||||
|
||||
/* Button Hover Effects */
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
|
||||
transition: all 0.3s ease;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 10px 25px rgba(59, 130, 246, 0.3);
|
||||
}
|
||||
|
||||
/* Card Hover Effects */
|
||||
.card-hover {
|
||||
transition: all 0.3s ease;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.card-hover:hover {
|
||||
transform: translateY(-8px);
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* Portfolio Grid */
|
||||
.portfolio-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.portfolio-item {
|
||||
border-radius: 1rem;
|
||||
overflow: hidden;
|
||||
background: white;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.portfolio-item:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.portfolio-image {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
aspect-ratio: 16/10;
|
||||
}
|
||||
|
||||
.portfolio-image img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.portfolio-item:hover .portfolio-image img {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.portfolio-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: linear-gradient(135deg, rgba(59, 130, 246, 0.8), rgba(139, 92, 246, 0.8));
|
||||
opacity: 0;
|
||||
transition: all 0.3s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.portfolio-item:hover .portfolio-overlay {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Service Cards */
|
||||
.service-card {
|
||||
background: white;
|
||||
border-radius: 1rem;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
transition: all 0.3s ease;
|
||||
border: 2px solid transparent;
|
||||
}
|
||||
|
||||
.service-card:hover {
|
||||
border-color: var(--primary-color);
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 20px 40px rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
|
||||
.service-icon {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0 auto 1rem;
|
||||
font-size: 2rem;
|
||||
color: white;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.service-card:hover .service-icon {
|
||||
transform: scale(1.1) rotate(5deg);
|
||||
}
|
||||
|
||||
/* Contact Form */
|
||||
.contact-form {
|
||||
background: white;
|
||||
border-radius: 1rem;
|
||||
padding: 2rem;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
width: 100%;
|
||||
padding: 1rem;
|
||||
border: 2px solid var(--border-color);
|
||||
border-radius: 0.5rem;
|
||||
font-size: 1rem;
|
||||
transition: all 0.3s ease;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary-color);
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
|
||||
/* Calculator Styles */
|
||||
.calculator-step {
|
||||
animation: fadeIn 0.5s ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
.option-card {
|
||||
border: 2px solid var(--border-color);
|
||||
border-radius: 1rem;
|
||||
padding: 1.5rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.option-card:hover {
|
||||
border-color: var(--primary-color);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.option-card.selected {
|
||||
border-color: var(--primary-color);
|
||||
background: rgba(59, 130, 246, 0.05);
|
||||
}
|
||||
|
||||
/* Progress Bar */
|
||||
.progress-bar {
|
||||
width: 100%;
|
||||
height: 8px;
|
||||
background: var(--border-color);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, var(--primary-color), var(--secondary-color));
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
/* Hero Section Animations */
|
||||
.hero-content {
|
||||
animation: heroFadeIn 1s ease-out;
|
||||
}
|
||||
|
||||
@keyframes heroFadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Parallax Effect */
|
||||
.parallax {
|
||||
background-attachment: fixed;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
/* Scroll Animations */
|
||||
.fade-in-up {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
transition: all 0.8s ease;
|
||||
}
|
||||
|
||||
.fade-in-up.animate {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
/* Typography */
|
||||
.gradient-text {
|
||||
background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
/* Status Badges */
|
||||
.status-badge {
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 9999px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.status-new {
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.status-in-progress {
|
||||
background: rgba(245, 158, 11, 0.1);
|
||||
color: #d97706;
|
||||
}
|
||||
|
||||
.status-completed {
|
||||
background: rgba(16, 185, 129, 0.1);
|
||||
color: #059669;
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 768px) {
|
||||
.portfolio-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
|
||||
.service-card {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.calculator-step {
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Dark Mode Support */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--text-dark: #f9fafb;
|
||||
--text-light: #d1d5db;
|
||||
--bg-light: #1f2937;
|
||||
--border-color: #374151;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #111827;
|
||||
color: var(--text-dark);
|
||||
}
|
||||
|
||||
.card-hover, .service-card, .contact-form, .option-card {
|
||||
background: #1f2937;
|
||||
border-color: var(--border-color);
|
||||
}
|
||||
|
||||
.form-control {
|
||||
background: #374151;
|
||||
border-color: #4b5563;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
}
|
||||
|
||||
/* Print Styles */
|
||||
@media print {
|
||||
.navbar, .footer, .contact-form, .mobile-menu {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
body {
|
||||
font-size: 12pt;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.portfolio-item, .service-card {
|
||||
break-inside: avoid;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Accessibility */
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
/* Focus styles for better accessibility */
|
||||
.form-control:focus,
|
||||
.btn:focus,
|
||||
.option-card:focus {
|
||||
outline: 2px solid var(--primary-color);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* Loading States */
|
||||
.btn-loading {
|
||||
position: relative;
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
.btn-loading::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
margin-left: -8px;
|
||||
margin-top: -8px;
|
||||
border: 2px solid transparent;
|
||||
border-top-color: currentColor;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
442
.history/public/css/main_20251019165556.css
Normal file
442
.history/public/css/main_20251019165556.css
Normal file
@@ -0,0 +1,442 @@
|
||||
/* SmartSolTech - Main Styles */
|
||||
|
||||
/* CSS Reset and Base */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
/* Utility Classes */
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
.section-padding {
|
||||
padding: 4rem 0;
|
||||
}
|
||||
|
||||
/* Navigation */
|
||||
.navbar {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
backdrop-filter: blur(10px);
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1000;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
font-size: 1.5rem;
|
||||
font-weight: bold;
|
||||
color: #3b82f6;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.navbar-nav {
|
||||
display: flex;
|
||||
gap: 2rem;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
color: #6b7280;
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 0.5rem;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.nav-link:hover,
|
||||
.nav-link.active {
|
||||
color: #3b82f6;
|
||||
background-color: #eff6ff;
|
||||
}
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.mobile-menu.show {
|
||||
max-height: 500px;
|
||||
}
|
||||
|
||||
/* Button Hover Effects */
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
|
||||
transition: all 0.3s ease;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 10px 25px rgba(59, 130, 246, 0.3);
|
||||
}
|
||||
|
||||
/* Card Hover Effects */
|
||||
.card-hover {
|
||||
transition: all 0.3s ease;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.card-hover:hover {
|
||||
transform: translateY(-8px);
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* Portfolio Grid */
|
||||
.portfolio-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.portfolio-item {
|
||||
border-radius: 1rem;
|
||||
overflow: hidden;
|
||||
background: white;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.portfolio-item:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.portfolio-image {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
aspect-ratio: 16/10;
|
||||
}
|
||||
|
||||
.portfolio-image img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.portfolio-item:hover .portfolio-image img {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.portfolio-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: linear-gradient(135deg, rgba(59, 130, 246, 0.8), rgba(139, 92, 246, 0.8));
|
||||
opacity: 0;
|
||||
transition: all 0.3s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.portfolio-item:hover .portfolio-overlay {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Service Cards */
|
||||
.service-card {
|
||||
background: white;
|
||||
border-radius: 1rem;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
transition: all 0.3s ease;
|
||||
border: 2px solid transparent;
|
||||
}
|
||||
|
||||
.service-card:hover {
|
||||
border-color: var(--primary-color);
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 20px 40px rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
|
||||
.service-icon {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0 auto 1rem;
|
||||
font-size: 2rem;
|
||||
color: white;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.service-card:hover .service-icon {
|
||||
transform: scale(1.1) rotate(5deg);
|
||||
}
|
||||
|
||||
/* Contact Form */
|
||||
.contact-form {
|
||||
background: white;
|
||||
border-radius: 1rem;
|
||||
padding: 2rem;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
width: 100%;
|
||||
padding: 1rem;
|
||||
border: 2px solid var(--border-color);
|
||||
border-radius: 0.5rem;
|
||||
font-size: 1rem;
|
||||
transition: all 0.3s ease;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary-color);
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
|
||||
/* Calculator Styles */
|
||||
.calculator-step {
|
||||
animation: fadeIn 0.5s ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
.option-card {
|
||||
border: 2px solid var(--border-color);
|
||||
border-radius: 1rem;
|
||||
padding: 1.5rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.option-card:hover {
|
||||
border-color: var(--primary-color);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.option-card.selected {
|
||||
border-color: var(--primary-color);
|
||||
background: rgba(59, 130, 246, 0.05);
|
||||
}
|
||||
|
||||
/* Progress Bar */
|
||||
.progress-bar {
|
||||
width: 100%;
|
||||
height: 8px;
|
||||
background: var(--border-color);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, var(--primary-color), var(--secondary-color));
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
/* Hero Section Animations */
|
||||
.hero-content {
|
||||
animation: heroFadeIn 1s ease-out;
|
||||
}
|
||||
|
||||
@keyframes heroFadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Parallax Effect */
|
||||
.parallax {
|
||||
background-attachment: fixed;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
/* Scroll Animations */
|
||||
.fade-in-up {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
transition: all 0.8s ease;
|
||||
}
|
||||
|
||||
.fade-in-up.animate {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
/* Typography */
|
||||
.gradient-text {
|
||||
background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
/* Status Badges */
|
||||
.status-badge {
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 9999px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.status-new {
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.status-in-progress {
|
||||
background: rgba(245, 158, 11, 0.1);
|
||||
color: #d97706;
|
||||
}
|
||||
|
||||
.status-completed {
|
||||
background: rgba(16, 185, 129, 0.1);
|
||||
color: #059669;
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 768px) {
|
||||
.portfolio-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
|
||||
.service-card {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.calculator-step {
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Dark Mode Support */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--text-dark: #f9fafb;
|
||||
--text-light: #d1d5db;
|
||||
--bg-light: #1f2937;
|
||||
--border-color: #374151;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #111827;
|
||||
color: var(--text-dark);
|
||||
}
|
||||
|
||||
.card-hover, .service-card, .contact-form, .option-card {
|
||||
background: #1f2937;
|
||||
border-color: var(--border-color);
|
||||
}
|
||||
|
||||
.form-control {
|
||||
background: #374151;
|
||||
border-color: #4b5563;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
}
|
||||
|
||||
/* Print Styles */
|
||||
@media print {
|
||||
.navbar, .footer, .contact-form, .mobile-menu {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
body {
|
||||
font-size: 12pt;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.portfolio-item, .service-card {
|
||||
break-inside: avoid;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Accessibility */
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
/* Focus styles for better accessibility */
|
||||
.form-control:focus,
|
||||
.btn:focus,
|
||||
.option-card:focus {
|
||||
outline: 2px solid var(--primary-color);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* Loading States */
|
||||
.btn-loading {
|
||||
position: relative;
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
.btn-loading::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
margin-left: -8px;
|
||||
margin-top: -8px;
|
||||
border: 2px solid transparent;
|
||||
border-top-color: currentColor;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
552
.history/public/css/main_20251019182154.css
Normal file
552
.history/public/css/main_20251019182154.css
Normal file
@@ -0,0 +1,552 @@
|
||||
/* SmartSolTech - Main Styles */
|
||||
|
||||
/* CSS Reset and Base */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
/* Utility Classes */
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
.section-padding {
|
||||
padding: 4rem 0;
|
||||
}
|
||||
|
||||
/* Navigation */
|
||||
.navbar {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
backdrop-filter: blur(10px);
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1000;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
font-size: 1.5rem;
|
||||
font-weight: bold;
|
||||
color: #3b82f6;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.navbar-nav {
|
||||
display: flex;
|
||||
gap: 2rem;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
color: #6b7280;
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 0.5rem;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.nav-link:hover,
|
||||
.nav-link.active {
|
||||
color: #3b82f6;
|
||||
background-color: #eff6ff;
|
||||
}
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.mobile-menu.show {
|
||||
max-height: 500px;
|
||||
}
|
||||
|
||||
/* Button Hover Effects */
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
|
||||
transition: all 0.3s ease;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 10px 25px rgba(59, 130, 246, 0.3);
|
||||
}
|
||||
|
||||
/* Card Hover Effects */
|
||||
.card-hover {
|
||||
transition: all 0.3s ease;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.card-hover:hover {
|
||||
transform: translateY(-8px);
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* Portfolio Grid */
|
||||
.portfolio-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.portfolio-item {
|
||||
border-radius: 1rem;
|
||||
overflow: hidden;
|
||||
background: white;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.portfolio-item:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.portfolio-image {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
aspect-ratio: 16/10;
|
||||
}
|
||||
|
||||
.portfolio-image img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.portfolio-item:hover .portfolio-image img {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.portfolio-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: linear-gradient(135deg, rgba(59, 130, 246, 0.8), rgba(139, 92, 246, 0.8));
|
||||
opacity: 0;
|
||||
transition: all 0.3s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.portfolio-item:hover .portfolio-overlay {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Service Cards */
|
||||
.service-card {
|
||||
background: white;
|
||||
border-radius: 1rem;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
transition: all 0.3s ease;
|
||||
border: 2px solid transparent;
|
||||
}
|
||||
|
||||
.service-card:hover {
|
||||
border-color: var(--primary-color);
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 20px 40px rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
|
||||
.service-icon {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0 auto 1rem;
|
||||
font-size: 2rem;
|
||||
color: white;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.service-card:hover .service-icon {
|
||||
transform: scale(1.1) rotate(5deg);
|
||||
}
|
||||
|
||||
/* Contact Form */
|
||||
.contact-form {
|
||||
background: white;
|
||||
border-radius: 1rem;
|
||||
padding: 2rem;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
width: 100%;
|
||||
padding: 1rem;
|
||||
border: 2px solid var(--border-color);
|
||||
border-radius: 0.5rem;
|
||||
font-size: 1rem;
|
||||
transition: all 0.3s ease;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary-color);
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
|
||||
/* Calculator Styles */
|
||||
.calculator-step {
|
||||
animation: fadeIn 0.5s ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
.option-card {
|
||||
border: 2px solid var(--border-color);
|
||||
border-radius: 1rem;
|
||||
padding: 1.5rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.option-card:hover {
|
||||
border-color: var(--primary-color);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.option-card.selected {
|
||||
border-color: var(--primary-color);
|
||||
background: rgba(59, 130, 246, 0.05);
|
||||
}
|
||||
|
||||
/* Progress Bar */
|
||||
.progress-bar {
|
||||
width: 100%;
|
||||
height: 8px;
|
||||
background: var(--border-color);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, var(--primary-color), var(--secondary-color));
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
/* Hero Section Animations */
|
||||
.hero-content {
|
||||
animation: heroFadeIn 1s ease-out;
|
||||
}
|
||||
|
||||
@keyframes heroFadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Parallax Effect */
|
||||
.parallax {
|
||||
background-attachment: fixed;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
/* Scroll Animations */
|
||||
.fade-in-up {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
transition: all 0.8s ease;
|
||||
}
|
||||
|
||||
.fade-in-up.animate {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
/* Typography */
|
||||
.gradient-text {
|
||||
background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
/* Status Badges */
|
||||
.status-badge {
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 9999px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.status-new {
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.status-in-progress {
|
||||
background: rgba(245, 158, 11, 0.1);
|
||||
color: #d97706;
|
||||
}
|
||||
|
||||
.status-completed {
|
||||
background: rgba(16, 185, 129, 0.1);
|
||||
color: #059669;
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 768px) {
|
||||
.portfolio-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
|
||||
.service-card {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.calculator-step {
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Dark Mode Support */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--text-dark: #f9fafb;
|
||||
--text-light: #d1d5db;
|
||||
--bg-light: #1f2937;
|
||||
--border-color: #374151;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #111827;
|
||||
color: var(--text-dark);
|
||||
}
|
||||
|
||||
.card-hover, .service-card, .contact-form, .option-card {
|
||||
background: #1f2937;
|
||||
border-color: var(--border-color);
|
||||
}
|
||||
|
||||
.form-control {
|
||||
background: #374151;
|
||||
border-color: #4b5563;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
}
|
||||
|
||||
/* Print Styles */
|
||||
@media print {
|
||||
.navbar, .footer, .contact-form, .mobile-menu {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
body {
|
||||
font-size: 12pt;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.portfolio-item, .service-card {
|
||||
break-inside: avoid;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Accessibility */
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
/* Focus styles for better accessibility */
|
||||
.form-control:focus,
|
||||
.btn:focus,
|
||||
.option-card:focus {
|
||||
outline: 2px solid var(--primary-color);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* Loading States */
|
||||
.btn-loading {
|
||||
position: relative;
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
.btn-loading::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
margin-left: -8px;
|
||||
margin-top: -8px;
|
||||
border: 2px solid transparent;
|
||||
border-top-color: currentColor;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
/* Calculator Styles */
|
||||
.calculator-step {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.calculator-step.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.service-option,
|
||||
.complexity-option,
|
||||
.timeline-option {
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.service-option:hover,
|
||||
.complexity-option:hover,
|
||||
.timeline-option:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 25px rgba(59, 130, 246, 0.15);
|
||||
}
|
||||
|
||||
.service-option.selected,
|
||||
.complexity-option.selected,
|
||||
.timeline-option.selected {
|
||||
border-color: #3B82F6 !important;
|
||||
background-color: #EBF8FF !important;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 25px rgba(59, 130, 246, 0.15);
|
||||
}
|
||||
|
||||
.service-option.selected::after,
|
||||
.complexity-option.selected::after,
|
||||
.timeline-option.selected::after {
|
||||
content: '✓';
|
||||
position: absolute;
|
||||
top: 0.5rem;
|
||||
right: 0.5rem;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background: #3B82F6;
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Price Display Animation */
|
||||
#final-price {
|
||||
animation: priceReveal 0.8s ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes priceReveal {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: scale(0.8);
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
/* Calculator Progress Bar */
|
||||
.calculator-progress {
|
||||
width: 100%;
|
||||
height: 4px;
|
||||
background: #e5e7eb;
|
||||
border-radius: 2px;
|
||||
margin-bottom: 2rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.calculator-progress-bar {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #3B82F6, #8B5CF6);
|
||||
border-radius: 2px;
|
||||
transition: width 0.3s ease;
|
||||
width: 33.33%;
|
||||
}
|
||||
|
||||
.calculator-progress-bar.step-2 {
|
||||
width: 66.66%;
|
||||
}
|
||||
|
||||
.calculator-progress-bar.step-3 {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Calculator Mobile Improvements */
|
||||
@media (max-width: 768px) {
|
||||
.service-option,
|
||||
.complexity-option,
|
||||
.timeline-option {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
#final-price {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
}
|
||||
552
.history/public/css/main_20251019182628.css
Normal file
552
.history/public/css/main_20251019182628.css
Normal file
@@ -0,0 +1,552 @@
|
||||
/* SmartSolTech - Main Styles */
|
||||
|
||||
/* CSS Reset and Base */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
/* Utility Classes */
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
.section-padding {
|
||||
padding: 4rem 0;
|
||||
}
|
||||
|
||||
/* Navigation */
|
||||
.navbar {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
backdrop-filter: blur(10px);
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1000;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
font-size: 1.5rem;
|
||||
font-weight: bold;
|
||||
color: #3b82f6;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.navbar-nav {
|
||||
display: flex;
|
||||
gap: 2rem;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
color: #6b7280;
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 0.5rem;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.nav-link:hover,
|
||||
.nav-link.active {
|
||||
color: #3b82f6;
|
||||
background-color: #eff6ff;
|
||||
}
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.mobile-menu.show {
|
||||
max-height: 500px;
|
||||
}
|
||||
|
||||
/* Button Hover Effects */
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
|
||||
transition: all 0.3s ease;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 10px 25px rgba(59, 130, 246, 0.3);
|
||||
}
|
||||
|
||||
/* Card Hover Effects */
|
||||
.card-hover {
|
||||
transition: all 0.3s ease;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.card-hover:hover {
|
||||
transform: translateY(-8px);
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* Portfolio Grid */
|
||||
.portfolio-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.portfolio-item {
|
||||
border-radius: 1rem;
|
||||
overflow: hidden;
|
||||
background: white;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.portfolio-item:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.portfolio-image {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
aspect-ratio: 16/10;
|
||||
}
|
||||
|
||||
.portfolio-image img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.portfolio-item:hover .portfolio-image img {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.portfolio-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: linear-gradient(135deg, rgba(59, 130, 246, 0.8), rgba(139, 92, 246, 0.8));
|
||||
opacity: 0;
|
||||
transition: all 0.3s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.portfolio-item:hover .portfolio-overlay {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Service Cards */
|
||||
.service-card {
|
||||
background: white;
|
||||
border-radius: 1rem;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
transition: all 0.3s ease;
|
||||
border: 2px solid transparent;
|
||||
}
|
||||
|
||||
.service-card:hover {
|
||||
border-color: var(--primary-color);
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 20px 40px rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
|
||||
.service-icon {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0 auto 1rem;
|
||||
font-size: 2rem;
|
||||
color: white;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.service-card:hover .service-icon {
|
||||
transform: scale(1.1) rotate(5deg);
|
||||
}
|
||||
|
||||
/* Contact Form */
|
||||
.contact-form {
|
||||
background: white;
|
||||
border-radius: 1rem;
|
||||
padding: 2rem;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
width: 100%;
|
||||
padding: 1rem;
|
||||
border: 2px solid var(--border-color);
|
||||
border-radius: 0.5rem;
|
||||
font-size: 1rem;
|
||||
transition: all 0.3s ease;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary-color);
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
|
||||
/* Calculator Styles */
|
||||
.calculator-step {
|
||||
animation: fadeIn 0.5s ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
.option-card {
|
||||
border: 2px solid var(--border-color);
|
||||
border-radius: 1rem;
|
||||
padding: 1.5rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.option-card:hover {
|
||||
border-color: var(--primary-color);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.option-card.selected {
|
||||
border-color: var(--primary-color);
|
||||
background: rgba(59, 130, 246, 0.05);
|
||||
}
|
||||
|
||||
/* Progress Bar */
|
||||
.progress-bar {
|
||||
width: 100%;
|
||||
height: 8px;
|
||||
background: var(--border-color);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, var(--primary-color), var(--secondary-color));
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
/* Hero Section Animations */
|
||||
.hero-content {
|
||||
animation: heroFadeIn 1s ease-out;
|
||||
}
|
||||
|
||||
@keyframes heroFadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Parallax Effect */
|
||||
.parallax {
|
||||
background-attachment: fixed;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
/* Scroll Animations */
|
||||
.fade-in-up {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
transition: all 0.8s ease;
|
||||
}
|
||||
|
||||
.fade-in-up.animate {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
/* Typography */
|
||||
.gradient-text {
|
||||
background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
/* Status Badges */
|
||||
.status-badge {
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 9999px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.status-new {
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.status-in-progress {
|
||||
background: rgba(245, 158, 11, 0.1);
|
||||
color: #d97706;
|
||||
}
|
||||
|
||||
.status-completed {
|
||||
background: rgba(16, 185, 129, 0.1);
|
||||
color: #059669;
|
||||
}
|
||||
|
||||
/* Responsive Design */
|
||||
@media (max-width: 768px) {
|
||||
.portfolio-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
|
||||
.service-card {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.calculator-step {
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Dark Mode Support */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--text-dark: #f9fafb;
|
||||
--text-light: #d1d5db;
|
||||
--bg-light: #1f2937;
|
||||
--border-color: #374151;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #111827;
|
||||
color: var(--text-dark);
|
||||
}
|
||||
|
||||
.card-hover, .service-card, .contact-form, .option-card {
|
||||
background: #1f2937;
|
||||
border-color: var(--border-color);
|
||||
}
|
||||
|
||||
.form-control {
|
||||
background: #374151;
|
||||
border-color: #4b5563;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
}
|
||||
|
||||
/* Print Styles */
|
||||
@media print {
|
||||
.navbar, .footer, .contact-form, .mobile-menu {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
body {
|
||||
font-size: 12pt;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.portfolio-item, .service-card {
|
||||
break-inside: avoid;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Accessibility */
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
/* Focus styles for better accessibility */
|
||||
.form-control:focus,
|
||||
.btn:focus,
|
||||
.option-card:focus {
|
||||
outline: 2px solid var(--primary-color);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* Loading States */
|
||||
.btn-loading {
|
||||
position: relative;
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
.btn-loading::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
margin-left: -8px;
|
||||
margin-top: -8px;
|
||||
border: 2px solid transparent;
|
||||
border-top-color: currentColor;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
/* Calculator Styles */
|
||||
.calculator-step {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.calculator-step.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.service-option,
|
||||
.complexity-option,
|
||||
.timeline-option {
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.service-option:hover,
|
||||
.complexity-option:hover,
|
||||
.timeline-option:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 25px rgba(59, 130, 246, 0.15);
|
||||
}
|
||||
|
||||
.service-option.selected,
|
||||
.complexity-option.selected,
|
||||
.timeline-option.selected {
|
||||
border-color: #3B82F6 !important;
|
||||
background-color: #EBF8FF !important;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 25px rgba(59, 130, 246, 0.15);
|
||||
}
|
||||
|
||||
.service-option.selected::after,
|
||||
.complexity-option.selected::after,
|
||||
.timeline-option.selected::after {
|
||||
content: '✓';
|
||||
position: absolute;
|
||||
top: 0.5rem;
|
||||
right: 0.5rem;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background: #3B82F6;
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Price Display Animation */
|
||||
#final-price {
|
||||
animation: priceReveal 0.8s ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes priceReveal {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: scale(0.8);
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
/* Calculator Progress Bar */
|
||||
.calculator-progress {
|
||||
width: 100%;
|
||||
height: 4px;
|
||||
background: #e5e7eb;
|
||||
border-radius: 2px;
|
||||
margin-bottom: 2rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.calculator-progress-bar {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #3B82F6, #8B5CF6);
|
||||
border-radius: 2px;
|
||||
transition: width 0.3s ease;
|
||||
width: 33.33%;
|
||||
}
|
||||
|
||||
.calculator-progress-bar.step-2 {
|
||||
width: 66.66%;
|
||||
}
|
||||
|
||||
.calculator-progress-bar.step-3 {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Calculator Mobile Improvements */
|
||||
@media (max-width: 768px) {
|
||||
.service-option,
|
||||
.complexity-option,
|
||||
.timeline-option {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
#final-price {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
}
|
||||
368
.history/public/js/calculator_20251019182236.js
Normal file
368
.history/public/js/calculator_20251019182236.js
Normal file
@@ -0,0 +1,368 @@
|
||||
// Calculator Logic
|
||||
class ProjectCalculator {
|
||||
constructor() {
|
||||
this.selectedServices = [];
|
||||
this.selectedComplexity = null;
|
||||
this.selectedTimeline = null;
|
||||
this.currentStep = 1;
|
||||
this.totalSteps = 3;
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.setupEventListeners();
|
||||
this.updateProgressBar();
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
// Service selection
|
||||
document.querySelectorAll('.service-option').forEach(option => {
|
||||
option.addEventListener('click', (e) => this.handleServiceSelection(e));
|
||||
});
|
||||
|
||||
// Complexity selection
|
||||
document.querySelectorAll('.complexity-option').forEach(option => {
|
||||
option.addEventListener('click', (e) => this.handleComplexitySelection(e));
|
||||
});
|
||||
|
||||
// Timeline selection
|
||||
document.querySelectorAll('.timeline-option').forEach(option => {
|
||||
option.addEventListener('click', (e) => this.handleTimelineSelection(e));
|
||||
});
|
||||
|
||||
// Navigation buttons
|
||||
document.querySelectorAll('.next-step').forEach(btn => {
|
||||
btn.addEventListener('click', () => this.nextStep());
|
||||
});
|
||||
|
||||
document.querySelectorAll('.prev-step').forEach(btn => {
|
||||
btn.addEventListener('click', () => this.prevStep());
|
||||
});
|
||||
|
||||
// Restart button
|
||||
const restartBtn = document.querySelector('.restart-calculator');
|
||||
if (restartBtn) {
|
||||
restartBtn.addEventListener('click', () => this.restart());
|
||||
}
|
||||
}
|
||||
|
||||
handleServiceSelection(e) {
|
||||
const option = e.currentTarget;
|
||||
const service = option.dataset.service;
|
||||
const price = parseInt(option.dataset.basePrice);
|
||||
|
||||
option.classList.toggle('selected');
|
||||
|
||||
if (option.classList.contains('selected')) {
|
||||
this.selectedServices.push({ service, price });
|
||||
this.animateSelection(option, true);
|
||||
} else {
|
||||
this.selectedServices = this.selectedServices.filter(s => s.service !== service);
|
||||
this.animateSelection(option, false);
|
||||
}
|
||||
|
||||
this.updateStepButton();
|
||||
}
|
||||
|
||||
handleComplexitySelection(e) {
|
||||
const option = e.currentTarget;
|
||||
|
||||
// Clear previous selections
|
||||
document.querySelectorAll('.complexity-option').forEach(opt => {
|
||||
opt.classList.remove('selected');
|
||||
this.animateSelection(opt, false);
|
||||
});
|
||||
|
||||
// Select current option
|
||||
option.classList.add('selected');
|
||||
this.animateSelection(option, true);
|
||||
|
||||
this.selectedComplexity = {
|
||||
value: option.dataset.value,
|
||||
multiplier: parseFloat(option.dataset.multiplier)
|
||||
};
|
||||
|
||||
this.updateStepButton();
|
||||
}
|
||||
|
||||
handleTimelineSelection(e) {
|
||||
const option = e.currentTarget;
|
||||
|
||||
// Clear previous selections
|
||||
document.querySelectorAll('.timeline-option').forEach(opt => {
|
||||
opt.classList.remove('selected');
|
||||
this.animateSelection(opt, false);
|
||||
});
|
||||
|
||||
// Select current option
|
||||
option.classList.add('selected');
|
||||
this.animateSelection(option, true);
|
||||
|
||||
this.selectedTimeline = {
|
||||
value: option.dataset.value,
|
||||
multiplier: parseFloat(option.dataset.multiplier)
|
||||
};
|
||||
|
||||
this.updateStepButton();
|
||||
}
|
||||
|
||||
animateSelection(element, selected) {
|
||||
if (selected) {
|
||||
element.style.borderColor = '#3B82F6';
|
||||
element.style.backgroundColor = '#EBF8FF';
|
||||
element.style.transform = 'translateY(-2px)';
|
||||
element.style.boxShadow = '0 8px 25px rgba(59, 130, 246, 0.15)';
|
||||
} else {
|
||||
element.style.borderColor = '';
|
||||
element.style.backgroundColor = '';
|
||||
element.style.transform = '';
|
||||
element.style.boxShadow = '';
|
||||
}
|
||||
}
|
||||
|
||||
updateStepButton() {
|
||||
const step1Button = document.querySelector('#step-1 .next-step');
|
||||
const step2Button = document.querySelector('#step-2 .next-step');
|
||||
|
||||
if (step1Button) {
|
||||
step1Button.disabled = this.selectedServices.length === 0;
|
||||
step1Button.style.opacity = this.selectedServices.length > 0 ? '1' : '0.6';
|
||||
}
|
||||
|
||||
if (step2Button) {
|
||||
const isValid = this.selectedComplexity && this.selectedTimeline;
|
||||
step2Button.disabled = !isValid;
|
||||
step2Button.style.opacity = isValid ? '1' : '0.6';
|
||||
}
|
||||
}
|
||||
|
||||
updateProgressBar() {
|
||||
const progressBar = document.querySelector('.calculator-progress-bar');
|
||||
if (progressBar) {
|
||||
const progress = (this.currentStep / this.totalSteps) * 100;
|
||||
progressBar.style.width = `${progress}%`;
|
||||
progressBar.className = `calculator-progress-bar step-${this.currentStep}`;
|
||||
}
|
||||
}
|
||||
|
||||
nextStep() {
|
||||
if (this.currentStep >= this.totalSteps) return;
|
||||
|
||||
const currentStepElement = document.querySelector('.calculator-step.active');
|
||||
const nextStepElement = currentStepElement.nextElementSibling;
|
||||
|
||||
if (nextStepElement && nextStepElement.classList.contains('calculator-step')) {
|
||||
// Animate out current step
|
||||
currentStepElement.style.opacity = '0';
|
||||
currentStepElement.style.transform = 'translateX(-20px)';
|
||||
|
||||
setTimeout(() => {
|
||||
currentStepElement.classList.remove('active');
|
||||
currentStepElement.style.display = 'none';
|
||||
|
||||
// Animate in next step
|
||||
nextStepElement.classList.add('active');
|
||||
nextStepElement.style.display = 'block';
|
||||
nextStepElement.style.opacity = '0';
|
||||
nextStepElement.style.transform = 'translateX(20px)';
|
||||
|
||||
setTimeout(() => {
|
||||
nextStepElement.style.opacity = '1';
|
||||
nextStepElement.style.transform = 'translateX(0)';
|
||||
}, 50);
|
||||
|
||||
this.currentStep++;
|
||||
this.updateProgressBar();
|
||||
|
||||
if (this.currentStep === 3) {
|
||||
setTimeout(() => this.calculateFinalPrice(), 300);
|
||||
}
|
||||
}, 200);
|
||||
}
|
||||
}
|
||||
|
||||
prevStep() {
|
||||
if (this.currentStep <= 1) return;
|
||||
|
||||
const currentStepElement = document.querySelector('.calculator-step.active');
|
||||
const prevStepElement = currentStepElement.previousElementSibling;
|
||||
|
||||
if (prevStepElement && prevStepElement.classList.contains('calculator-step')) {
|
||||
// Animate out current step
|
||||
currentStepElement.style.opacity = '0';
|
||||
currentStepElement.style.transform = 'translateX(20px)';
|
||||
|
||||
setTimeout(() => {
|
||||
currentStepElement.classList.remove('active');
|
||||
currentStepElement.style.display = 'none';
|
||||
|
||||
// Animate in previous step
|
||||
prevStepElement.classList.add('active');
|
||||
prevStepElement.style.display = 'block';
|
||||
prevStepElement.style.opacity = '0';
|
||||
prevStepElement.style.transform = 'translateX(-20px)';
|
||||
|
||||
setTimeout(() => {
|
||||
prevStepElement.style.opacity = '1';
|
||||
prevStepElement.style.transform = 'translateX(0)';
|
||||
}, 50);
|
||||
|
||||
this.currentStep--;
|
||||
this.updateProgressBar();
|
||||
}, 200);
|
||||
}
|
||||
}
|
||||
|
||||
calculateFinalPrice() {
|
||||
let total = 0;
|
||||
|
||||
// Calculate base price from services
|
||||
this.selectedServices.forEach(service => {
|
||||
total += service.price;
|
||||
});
|
||||
|
||||
// Apply complexity multiplier
|
||||
if (this.selectedComplexity) {
|
||||
total *= this.selectedComplexity.multiplier;
|
||||
}
|
||||
|
||||
// Apply timeline multiplier
|
||||
if (this.selectedTimeline) {
|
||||
total *= this.selectedTimeline.multiplier;
|
||||
}
|
||||
|
||||
// Animate price reveal
|
||||
const priceElement = document.getElementById('final-price');
|
||||
if (priceElement) {
|
||||
priceElement.style.opacity = '0';
|
||||
priceElement.style.transform = 'scale(0.8)';
|
||||
|
||||
setTimeout(() => {
|
||||
priceElement.textContent = '₩' + total.toLocaleString();
|
||||
priceElement.style.opacity = '1';
|
||||
priceElement.style.transform = 'scale(1)';
|
||||
}, 300);
|
||||
}
|
||||
|
||||
// Generate summary
|
||||
setTimeout(() => this.generateSummary(total), 600);
|
||||
}
|
||||
|
||||
generateSummary(total) {
|
||||
const summary = document.getElementById('project-summary');
|
||||
if (!summary) return;
|
||||
|
||||
let summaryHTML = '';
|
||||
|
||||
// Services
|
||||
summaryHTML += '<div class="mb-4"><strong>Selected Services:</strong></div>';
|
||||
this.selectedServices.forEach(service => {
|
||||
summaryHTML += `<div class="ml-4 mb-2 flex items-center">
|
||||
<i class="fas fa-check-circle text-green-500 mr-2"></i>
|
||||
${this.getServiceName(service.service)}
|
||||
<span class="ml-auto font-semibold">₩${service.price.toLocaleString()}</span>
|
||||
</div>`;
|
||||
});
|
||||
|
||||
// Complexity
|
||||
if (this.selectedComplexity) {
|
||||
summaryHTML += `<div class="mt-4 mb-2 flex items-center">
|
||||
<i class="fas fa-layer-group text-blue-500 mr-2"></i>
|
||||
<strong>Complexity:</strong>
|
||||
<span class="ml-2">${this.getComplexityName(this.selectedComplexity.value)}</span>
|
||||
<span class="ml-auto text-sm text-gray-500">×${this.selectedComplexity.multiplier}</span>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// Timeline
|
||||
if (this.selectedTimeline) {
|
||||
summaryHTML += `<div class="mb-2 flex items-center">
|
||||
<i class="fas fa-clock text-purple-500 mr-2"></i>
|
||||
<strong>Timeline:</strong>
|
||||
<span class="ml-2">${this.getTimelineName(this.selectedTimeline.value)}</span>
|
||||
<span class="ml-auto text-sm text-gray-500">×${this.selectedTimeline.multiplier}</span>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// Animate summary appearance
|
||||
summary.style.opacity = '0';
|
||||
summary.innerHTML = summaryHTML;
|
||||
|
||||
setTimeout(() => {
|
||||
summary.style.opacity = '1';
|
||||
}, 200);
|
||||
}
|
||||
|
||||
getServiceName(service) {
|
||||
const names = {
|
||||
web: 'Web Development',
|
||||
mobile: 'Mobile App',
|
||||
design: 'UI/UX Design',
|
||||
marketing: 'Digital Marketing'
|
||||
};
|
||||
return names[service] || service;
|
||||
}
|
||||
|
||||
getComplexityName(complexity) {
|
||||
const names = {
|
||||
simple: 'Simple',
|
||||
medium: 'Medium',
|
||||
complex: 'Complex'
|
||||
};
|
||||
return names[complexity] || complexity;
|
||||
}
|
||||
|
||||
getTimelineName(timeline) {
|
||||
const names = {
|
||||
standard: 'Standard',
|
||||
rush: 'Rush',
|
||||
extended: 'Extended'
|
||||
};
|
||||
return names[timeline] || timeline;
|
||||
}
|
||||
|
||||
restart() {
|
||||
// Reset all selections
|
||||
this.selectedServices = [];
|
||||
this.selectedComplexity = null;
|
||||
this.selectedTimeline = null;
|
||||
this.currentStep = 1;
|
||||
|
||||
// Reset UI
|
||||
document.querySelectorAll('.service-option, .complexity-option, .timeline-option').forEach(opt => {
|
||||
opt.classList.remove('selected');
|
||||
this.animateSelection(opt, false);
|
||||
});
|
||||
|
||||
// Reset steps
|
||||
document.querySelectorAll('.calculator-step').forEach(step => {
|
||||
step.classList.remove('active');
|
||||
step.style.display = 'none';
|
||||
step.style.opacity = '1';
|
||||
step.style.transform = 'translateX(0)';
|
||||
});
|
||||
|
||||
// Show first step
|
||||
const firstStep = document.getElementById('step-1');
|
||||
if (firstStep) {
|
||||
firstStep.classList.add('active');
|
||||
firstStep.style.display = 'block';
|
||||
}
|
||||
|
||||
this.updateProgressBar();
|
||||
this.updateStepButton();
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize calculator when DOM is loaded
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Theme initialization
|
||||
const theme = localStorage.getItem('theme') || 'light';
|
||||
document.documentElement.className = theme === 'dark' ? 'dark' : '';
|
||||
|
||||
// Initialize calculator
|
||||
if (document.querySelector('.calculator-step')) {
|
||||
new ProjectCalculator();
|
||||
}
|
||||
});
|
||||
380
.history/public/js/calculator_20251019182338.js
Normal file
380
.history/public/js/calculator_20251019182338.js
Normal file
@@ -0,0 +1,380 @@
|
||||
// Calculator Logic
|
||||
class ProjectCalculator {
|
||||
constructor() {
|
||||
this.selectedServices = [];
|
||||
this.selectedComplexity = null;
|
||||
this.selectedTimeline = null;
|
||||
this.currentStep = 1;
|
||||
this.totalSteps = 3;
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.setupEventListeners();
|
||||
this.updateProgressBar();
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
// Service selection
|
||||
document.querySelectorAll('.service-option').forEach(option => {
|
||||
option.addEventListener('click', (e) => this.handleServiceSelection(e));
|
||||
});
|
||||
|
||||
// Complexity selection
|
||||
document.querySelectorAll('.complexity-option').forEach(option => {
|
||||
option.addEventListener('click', (e) => this.handleComplexitySelection(e));
|
||||
});
|
||||
|
||||
// Timeline selection
|
||||
document.querySelectorAll('.timeline-option').forEach(option => {
|
||||
option.addEventListener('click', (e) => this.handleTimelineSelection(e));
|
||||
});
|
||||
|
||||
// Navigation buttons
|
||||
document.querySelectorAll('.next-step').forEach(btn => {
|
||||
btn.addEventListener('click', () => this.nextStep());
|
||||
});
|
||||
|
||||
document.querySelectorAll('.prev-step').forEach(btn => {
|
||||
btn.addEventListener('click', () => this.prevStep());
|
||||
});
|
||||
|
||||
// Restart button
|
||||
const restartBtn = document.querySelector('.restart-calculator');
|
||||
if (restartBtn) {
|
||||
restartBtn.addEventListener('click', () => this.restart());
|
||||
}
|
||||
}
|
||||
|
||||
handleServiceSelection(e) {
|
||||
const option = e.currentTarget;
|
||||
const service = option.dataset.service;
|
||||
const price = parseInt(option.dataset.basePrice);
|
||||
|
||||
option.classList.toggle('selected');
|
||||
|
||||
if (option.classList.contains('selected')) {
|
||||
this.selectedServices.push({ service, price });
|
||||
this.animateSelection(option, true);
|
||||
} else {
|
||||
this.selectedServices = this.selectedServices.filter(s => s.service !== service);
|
||||
this.animateSelection(option, false);
|
||||
}
|
||||
|
||||
this.updateStepButton();
|
||||
}
|
||||
|
||||
handleComplexitySelection(e) {
|
||||
const option = e.currentTarget;
|
||||
|
||||
// Clear previous selections
|
||||
document.querySelectorAll('.complexity-option').forEach(opt => {
|
||||
opt.classList.remove('selected');
|
||||
this.animateSelection(opt, false);
|
||||
});
|
||||
|
||||
// Select current option
|
||||
option.classList.add('selected');
|
||||
this.animateSelection(option, true);
|
||||
|
||||
this.selectedComplexity = {
|
||||
value: option.dataset.value,
|
||||
multiplier: parseFloat(option.dataset.multiplier)
|
||||
};
|
||||
|
||||
this.updateStepButton();
|
||||
}
|
||||
|
||||
handleTimelineSelection(e) {
|
||||
const option = e.currentTarget;
|
||||
|
||||
// Clear previous selections
|
||||
document.querySelectorAll('.timeline-option').forEach(opt => {
|
||||
opt.classList.remove('selected');
|
||||
this.animateSelection(opt, false);
|
||||
});
|
||||
|
||||
// Select current option
|
||||
option.classList.add('selected');
|
||||
this.animateSelection(option, true);
|
||||
|
||||
this.selectedTimeline = {
|
||||
value: option.dataset.value,
|
||||
multiplier: parseFloat(option.dataset.multiplier)
|
||||
};
|
||||
|
||||
this.updateStepButton();
|
||||
}
|
||||
|
||||
animateSelection(element, selected) {
|
||||
if (selected) {
|
||||
element.style.borderColor = '#3B82F6';
|
||||
element.style.backgroundColor = '#EBF8FF';
|
||||
element.style.transform = 'translateY(-2px)';
|
||||
element.style.boxShadow = '0 8px 25px rgba(59, 130, 246, 0.15)';
|
||||
} else {
|
||||
element.style.borderColor = '';
|
||||
element.style.backgroundColor = '';
|
||||
element.style.transform = '';
|
||||
element.style.boxShadow = '';
|
||||
}
|
||||
}
|
||||
|
||||
updateStepButton() {
|
||||
const step1Button = document.querySelector('#step-1 .next-step');
|
||||
const step2Button = document.querySelector('#step-2 .next-step');
|
||||
|
||||
if (step1Button) {
|
||||
step1Button.disabled = this.selectedServices.length === 0;
|
||||
step1Button.style.opacity = this.selectedServices.length > 0 ? '1' : '0.6';
|
||||
}
|
||||
|
||||
if (step2Button) {
|
||||
const isValid = this.selectedComplexity && this.selectedTimeline;
|
||||
step2Button.disabled = !isValid;
|
||||
step2Button.style.opacity = isValid ? '1' : '0.6';
|
||||
}
|
||||
}
|
||||
|
||||
updateProgressBar() {
|
||||
const progressBar = document.querySelector('.calculator-progress-bar');
|
||||
if (progressBar) {
|
||||
const progress = (this.currentStep / this.totalSteps) * 100;
|
||||
progressBar.style.width = `${progress}%`;
|
||||
progressBar.className = `calculator-progress-bar step-${this.currentStep}`;
|
||||
}
|
||||
}
|
||||
|
||||
nextStep() {
|
||||
if (this.currentStep >= this.totalSteps) return;
|
||||
|
||||
const currentStepElement = document.querySelector('.calculator-step.active');
|
||||
const nextStepElement = currentStepElement.nextElementSibling;
|
||||
|
||||
if (nextStepElement && nextStepElement.classList.contains('calculator-step')) {
|
||||
// Animate out current step
|
||||
currentStepElement.style.opacity = '0';
|
||||
currentStepElement.style.transform = 'translateX(-20px)';
|
||||
|
||||
setTimeout(() => {
|
||||
currentStepElement.classList.remove('active');
|
||||
currentStepElement.style.display = 'none';
|
||||
|
||||
// Animate in next step
|
||||
nextStepElement.classList.add('active');
|
||||
nextStepElement.style.display = 'block';
|
||||
nextStepElement.style.opacity = '0';
|
||||
nextStepElement.style.transform = 'translateX(20px)';
|
||||
|
||||
setTimeout(() => {
|
||||
nextStepElement.style.opacity = '1';
|
||||
nextStepElement.style.transform = 'translateX(0)';
|
||||
}, 50);
|
||||
|
||||
this.currentStep++;
|
||||
this.updateProgressBar();
|
||||
|
||||
if (this.currentStep === 3) {
|
||||
setTimeout(() => this.calculateFinalPrice(), 300);
|
||||
}
|
||||
}, 200);
|
||||
}
|
||||
}
|
||||
|
||||
prevStep() {
|
||||
if (this.currentStep <= 1) return;
|
||||
|
||||
const currentStepElement = document.querySelector('.calculator-step.active');
|
||||
const prevStepElement = currentStepElement.previousElementSibling;
|
||||
|
||||
if (prevStepElement && prevStepElement.classList.contains('calculator-step')) {
|
||||
// Animate out current step
|
||||
currentStepElement.style.opacity = '0';
|
||||
currentStepElement.style.transform = 'translateX(20px)';
|
||||
|
||||
setTimeout(() => {
|
||||
currentStepElement.classList.remove('active');
|
||||
currentStepElement.style.display = 'none';
|
||||
|
||||
// Animate in previous step
|
||||
prevStepElement.classList.add('active');
|
||||
prevStepElement.style.display = 'block';
|
||||
prevStepElement.style.opacity = '0';
|
||||
prevStepElement.style.transform = 'translateX(-20px)';
|
||||
|
||||
setTimeout(() => {
|
||||
prevStepElement.style.opacity = '1';
|
||||
prevStepElement.style.transform = 'translateX(0)';
|
||||
}, 50);
|
||||
|
||||
this.currentStep--;
|
||||
this.updateProgressBar();
|
||||
}, 200);
|
||||
}
|
||||
}
|
||||
|
||||
calculateFinalPrice() {
|
||||
let total = 0;
|
||||
|
||||
// Calculate base price from services
|
||||
this.selectedServices.forEach(service => {
|
||||
total += service.price;
|
||||
});
|
||||
|
||||
// Apply complexity multiplier
|
||||
if (this.selectedComplexity) {
|
||||
total *= this.selectedComplexity.multiplier;
|
||||
}
|
||||
|
||||
// Apply timeline multiplier
|
||||
if (this.selectedTimeline) {
|
||||
total *= this.selectedTimeline.multiplier;
|
||||
}
|
||||
|
||||
// Animate price reveal
|
||||
const priceElement = document.getElementById('final-price');
|
||||
if (priceElement) {
|
||||
priceElement.style.opacity = '0';
|
||||
priceElement.style.transform = 'scale(0.8)';
|
||||
|
||||
setTimeout(() => {
|
||||
priceElement.textContent = '₩' + total.toLocaleString();
|
||||
priceElement.style.opacity = '1';
|
||||
priceElement.style.transform = 'scale(1)';
|
||||
}, 300);
|
||||
}
|
||||
|
||||
// Generate summary
|
||||
setTimeout(() => this.generateSummary(total), 600);
|
||||
}
|
||||
|
||||
generateSummary(total) {
|
||||
const summary = document.getElementById('project-summary');
|
||||
if (!summary) return;
|
||||
|
||||
let summaryHTML = '';
|
||||
|
||||
// Services
|
||||
summaryHTML += '<div class="mb-4"><strong>Selected Services:</strong></div>';
|
||||
this.selectedServices.forEach(service => {
|
||||
summaryHTML += `<div class="ml-4 mb-2 flex items-center">
|
||||
<i class="fas fa-check-circle text-green-500 mr-2"></i>
|
||||
${this.getServiceName(service.service)}
|
||||
<span class="ml-auto font-semibold">₩${service.price.toLocaleString()}</span>
|
||||
</div>`;
|
||||
});
|
||||
|
||||
// Complexity
|
||||
if (this.selectedComplexity) {
|
||||
summaryHTML += `<div class="mt-4 mb-2 flex items-center">
|
||||
<i class="fas fa-layer-group text-blue-500 mr-2"></i>
|
||||
<strong>Complexity:</strong>
|
||||
<span class="ml-2">${this.getComplexityName(this.selectedComplexity.value)}</span>
|
||||
<span class="ml-auto text-sm text-gray-500">×${this.selectedComplexity.multiplier}</span>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// Timeline
|
||||
if (this.selectedTimeline) {
|
||||
summaryHTML += `<div class="mb-2 flex items-center">
|
||||
<i class="fas fa-clock text-purple-500 mr-2"></i>
|
||||
<strong>Timeline:</strong>
|
||||
<span class="ml-2">${this.getTimelineName(this.selectedTimeline.value)}</span>
|
||||
<span class="ml-auto text-sm text-gray-500">×${this.selectedTimeline.multiplier}</span>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// Animate summary appearance
|
||||
summary.style.opacity = '0';
|
||||
summary.innerHTML = summaryHTML;
|
||||
|
||||
setTimeout(() => {
|
||||
summary.style.opacity = '1';
|
||||
}, 200);
|
||||
}
|
||||
|
||||
getServiceName(service) {
|
||||
if (window.calculatorTranslations && window.calculatorTranslations.services) {
|
||||
return window.calculatorTranslations.services[service] || service;
|
||||
}
|
||||
|
||||
const names = {
|
||||
web: 'Web Development',
|
||||
mobile: 'Mobile App',
|
||||
design: 'UI/UX Design',
|
||||
marketing: 'Digital Marketing'
|
||||
};
|
||||
return names[service] || service;
|
||||
}
|
||||
|
||||
getComplexityName(complexity) {
|
||||
if (window.calculatorTranslations && window.calculatorTranslations.complexity) {
|
||||
return window.calculatorTranslations.complexity[complexity] || complexity;
|
||||
}
|
||||
|
||||
const names = {
|
||||
simple: 'Simple',
|
||||
medium: 'Medium',
|
||||
complex: 'Complex'
|
||||
};
|
||||
return names[complexity] || complexity;
|
||||
}
|
||||
|
||||
getTimelineName(timeline) {
|
||||
if (window.calculatorTranslations && window.calculatorTranslations.timeline) {
|
||||
return window.calculatorTranslations.timeline[timeline] || timeline;
|
||||
}
|
||||
|
||||
const names = {
|
||||
standard: 'Standard',
|
||||
rush: 'Rush',
|
||||
extended: 'Extended'
|
||||
};
|
||||
return names[timeline] || timeline;
|
||||
}
|
||||
|
||||
restart() {
|
||||
// Reset all selections
|
||||
this.selectedServices = [];
|
||||
this.selectedComplexity = null;
|
||||
this.selectedTimeline = null;
|
||||
this.currentStep = 1;
|
||||
|
||||
// Reset UI
|
||||
document.querySelectorAll('.service-option, .complexity-option, .timeline-option').forEach(opt => {
|
||||
opt.classList.remove('selected');
|
||||
this.animateSelection(opt, false);
|
||||
});
|
||||
|
||||
// Reset steps
|
||||
document.querySelectorAll('.calculator-step').forEach(step => {
|
||||
step.classList.remove('active');
|
||||
step.style.display = 'none';
|
||||
step.style.opacity = '1';
|
||||
step.style.transform = 'translateX(0)';
|
||||
});
|
||||
|
||||
// Show first step
|
||||
const firstStep = document.getElementById('step-1');
|
||||
if (firstStep) {
|
||||
firstStep.classList.add('active');
|
||||
firstStep.style.display = 'block';
|
||||
}
|
||||
|
||||
this.updateProgressBar();
|
||||
this.updateStepButton();
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize calculator when DOM is loaded
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Theme initialization
|
||||
const theme = localStorage.getItem('theme') || 'light';
|
||||
document.documentElement.className = theme === 'dark' ? 'dark' : '';
|
||||
|
||||
// Initialize calculator
|
||||
if (document.querySelector('.calculator-step')) {
|
||||
new ProjectCalculator();
|
||||
}
|
||||
});
|
||||
384
.history/public/js/calculator_20251019182400.js
Normal file
384
.history/public/js/calculator_20251019182400.js
Normal file
@@ -0,0 +1,384 @@
|
||||
// Calculator Logic
|
||||
class ProjectCalculator {
|
||||
constructor() {
|
||||
this.selectedServices = [];
|
||||
this.selectedComplexity = null;
|
||||
this.selectedTimeline = null;
|
||||
this.currentStep = 1;
|
||||
this.totalSteps = 3;
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.setupEventListeners();
|
||||
this.updateProgressBar();
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
// Service selection
|
||||
document.querySelectorAll('.service-option').forEach(option => {
|
||||
option.addEventListener('click', (e) => this.handleServiceSelection(e));
|
||||
});
|
||||
|
||||
// Complexity selection
|
||||
document.querySelectorAll('.complexity-option').forEach(option => {
|
||||
option.addEventListener('click', (e) => this.handleComplexitySelection(e));
|
||||
});
|
||||
|
||||
// Timeline selection
|
||||
document.querySelectorAll('.timeline-option').forEach(option => {
|
||||
option.addEventListener('click', (e) => this.handleTimelineSelection(e));
|
||||
});
|
||||
|
||||
// Navigation buttons
|
||||
document.querySelectorAll('.next-step').forEach(btn => {
|
||||
btn.addEventListener('click', () => this.nextStep());
|
||||
});
|
||||
|
||||
document.querySelectorAll('.prev-step').forEach(btn => {
|
||||
btn.addEventListener('click', () => this.prevStep());
|
||||
});
|
||||
|
||||
// Restart button
|
||||
const restartBtn = document.querySelector('.restart-calculator');
|
||||
if (restartBtn) {
|
||||
restartBtn.addEventListener('click', () => this.restart());
|
||||
}
|
||||
}
|
||||
|
||||
handleServiceSelection(e) {
|
||||
const option = e.currentTarget;
|
||||
const service = option.dataset.service;
|
||||
const price = parseInt(option.dataset.basePrice);
|
||||
|
||||
option.classList.toggle('selected');
|
||||
|
||||
if (option.classList.contains('selected')) {
|
||||
this.selectedServices.push({ service, price });
|
||||
this.animateSelection(option, true);
|
||||
} else {
|
||||
this.selectedServices = this.selectedServices.filter(s => s.service !== service);
|
||||
this.animateSelection(option, false);
|
||||
}
|
||||
|
||||
this.updateStepButton();
|
||||
}
|
||||
|
||||
handleComplexitySelection(e) {
|
||||
const option = e.currentTarget;
|
||||
|
||||
// Clear previous selections
|
||||
document.querySelectorAll('.complexity-option').forEach(opt => {
|
||||
opt.classList.remove('selected');
|
||||
this.animateSelection(opt, false);
|
||||
});
|
||||
|
||||
// Select current option
|
||||
option.classList.add('selected');
|
||||
this.animateSelection(option, true);
|
||||
|
||||
this.selectedComplexity = {
|
||||
value: option.dataset.value,
|
||||
multiplier: parseFloat(option.dataset.multiplier)
|
||||
};
|
||||
|
||||
this.updateStepButton();
|
||||
}
|
||||
|
||||
handleTimelineSelection(e) {
|
||||
const option = e.currentTarget;
|
||||
|
||||
// Clear previous selections
|
||||
document.querySelectorAll('.timeline-option').forEach(opt => {
|
||||
opt.classList.remove('selected');
|
||||
this.animateSelection(opt, false);
|
||||
});
|
||||
|
||||
// Select current option
|
||||
option.classList.add('selected');
|
||||
this.animateSelection(option, true);
|
||||
|
||||
this.selectedTimeline = {
|
||||
value: option.dataset.value,
|
||||
multiplier: parseFloat(option.dataset.multiplier)
|
||||
};
|
||||
|
||||
this.updateStepButton();
|
||||
}
|
||||
|
||||
animateSelection(element, selected) {
|
||||
if (selected) {
|
||||
element.style.borderColor = '#3B82F6';
|
||||
element.style.backgroundColor = '#EBF8FF';
|
||||
element.style.transform = 'translateY(-2px)';
|
||||
element.style.boxShadow = '0 8px 25px rgba(59, 130, 246, 0.15)';
|
||||
} else {
|
||||
element.style.borderColor = '';
|
||||
element.style.backgroundColor = '';
|
||||
element.style.transform = '';
|
||||
element.style.boxShadow = '';
|
||||
}
|
||||
}
|
||||
|
||||
updateStepButton() {
|
||||
const step1Button = document.querySelector('#step-1 .next-step');
|
||||
const step2Button = document.querySelector('#step-2 .next-step');
|
||||
|
||||
if (step1Button) {
|
||||
step1Button.disabled = this.selectedServices.length === 0;
|
||||
step1Button.style.opacity = this.selectedServices.length > 0 ? '1' : '0.6';
|
||||
}
|
||||
|
||||
if (step2Button) {
|
||||
const isValid = this.selectedComplexity && this.selectedTimeline;
|
||||
step2Button.disabled = !isValid;
|
||||
step2Button.style.opacity = isValid ? '1' : '0.6';
|
||||
}
|
||||
}
|
||||
|
||||
updateProgressBar() {
|
||||
const progressBar = document.querySelector('.calculator-progress-bar');
|
||||
if (progressBar) {
|
||||
const progress = (this.currentStep / this.totalSteps) * 100;
|
||||
progressBar.style.width = `${progress}%`;
|
||||
progressBar.className = `calculator-progress-bar step-${this.currentStep}`;
|
||||
}
|
||||
}
|
||||
|
||||
nextStep() {
|
||||
if (this.currentStep >= this.totalSteps) return;
|
||||
|
||||
const currentStepElement = document.querySelector('.calculator-step.active');
|
||||
const nextStepElement = currentStepElement.nextElementSibling;
|
||||
|
||||
if (nextStepElement && nextStepElement.classList.contains('calculator-step')) {
|
||||
// Animate out current step
|
||||
currentStepElement.style.opacity = '0';
|
||||
currentStepElement.style.transform = 'translateX(-20px)';
|
||||
|
||||
setTimeout(() => {
|
||||
currentStepElement.classList.remove('active');
|
||||
currentStepElement.style.display = 'none';
|
||||
|
||||
// Animate in next step
|
||||
nextStepElement.classList.add('active');
|
||||
nextStepElement.style.display = 'block';
|
||||
nextStepElement.style.opacity = '0';
|
||||
nextStepElement.style.transform = 'translateX(20px)';
|
||||
|
||||
setTimeout(() => {
|
||||
nextStepElement.style.opacity = '1';
|
||||
nextStepElement.style.transform = 'translateX(0)';
|
||||
}, 50);
|
||||
|
||||
this.currentStep++;
|
||||
this.updateProgressBar();
|
||||
|
||||
if (this.currentStep === 3) {
|
||||
setTimeout(() => this.calculateFinalPrice(), 300);
|
||||
}
|
||||
}, 200);
|
||||
}
|
||||
}
|
||||
|
||||
prevStep() {
|
||||
if (this.currentStep <= 1) return;
|
||||
|
||||
const currentStepElement = document.querySelector('.calculator-step.active');
|
||||
const prevStepElement = currentStepElement.previousElementSibling;
|
||||
|
||||
if (prevStepElement && prevStepElement.classList.contains('calculator-step')) {
|
||||
// Animate out current step
|
||||
currentStepElement.style.opacity = '0';
|
||||
currentStepElement.style.transform = 'translateX(20px)';
|
||||
|
||||
setTimeout(() => {
|
||||
currentStepElement.classList.remove('active');
|
||||
currentStepElement.style.display = 'none';
|
||||
|
||||
// Animate in previous step
|
||||
prevStepElement.classList.add('active');
|
||||
prevStepElement.style.display = 'block';
|
||||
prevStepElement.style.opacity = '0';
|
||||
prevStepElement.style.transform = 'translateX(-20px)';
|
||||
|
||||
setTimeout(() => {
|
||||
prevStepElement.style.opacity = '1';
|
||||
prevStepElement.style.transform = 'translateX(0)';
|
||||
}, 50);
|
||||
|
||||
this.currentStep--;
|
||||
this.updateProgressBar();
|
||||
}, 200);
|
||||
}
|
||||
}
|
||||
|
||||
calculateFinalPrice() {
|
||||
let total = 0;
|
||||
|
||||
// Calculate base price from services
|
||||
this.selectedServices.forEach(service => {
|
||||
total += service.price;
|
||||
});
|
||||
|
||||
// Apply complexity multiplier
|
||||
if (this.selectedComplexity) {
|
||||
total *= this.selectedComplexity.multiplier;
|
||||
}
|
||||
|
||||
// Apply timeline multiplier
|
||||
if (this.selectedTimeline) {
|
||||
total *= this.selectedTimeline.multiplier;
|
||||
}
|
||||
|
||||
// Animate price reveal
|
||||
const priceElement = document.getElementById('final-price');
|
||||
if (priceElement) {
|
||||
priceElement.style.opacity = '0';
|
||||
priceElement.style.transform = 'scale(0.8)';
|
||||
|
||||
setTimeout(() => {
|
||||
priceElement.textContent = '₩' + total.toLocaleString();
|
||||
priceElement.style.opacity = '1';
|
||||
priceElement.style.transform = 'scale(1)';
|
||||
}, 300);
|
||||
}
|
||||
|
||||
// Generate summary
|
||||
setTimeout(() => this.generateSummary(total), 600);
|
||||
}
|
||||
|
||||
generateSummary(total) {
|
||||
const summary = document.getElementById('project-summary');
|
||||
if (!summary) return;
|
||||
|
||||
let summaryHTML = '';
|
||||
|
||||
// Services
|
||||
const servicesLabel = window.calculatorTranslations?.labels?.selected_services || 'Selected Services';
|
||||
const complexityLabel = window.calculatorTranslations?.labels?.complexity || 'Complexity';
|
||||
const timelineLabel = window.calculatorTranslations?.labels?.timeline || 'Timeline';
|
||||
|
||||
summaryHTML += `<div class="mb-4"><strong>${servicesLabel}:</strong></div>`;
|
||||
this.selectedServices.forEach(service => {
|
||||
summaryHTML += `<div class="ml-4 mb-2 flex items-center">
|
||||
<i class="fas fa-check-circle text-green-500 mr-2"></i>
|
||||
${this.getServiceName(service.service)}
|
||||
<span class="ml-auto font-semibold">₩${service.price.toLocaleString()}</span>
|
||||
</div>`;
|
||||
});
|
||||
|
||||
// Complexity
|
||||
if (this.selectedComplexity) {
|
||||
summaryHTML += `<div class="mt-4 mb-2 flex items-center">
|
||||
<i class="fas fa-layer-group text-blue-500 mr-2"></i>
|
||||
<strong>${complexityLabel}:</strong>
|
||||
<span class="ml-2">${this.getComplexityName(this.selectedComplexity.value)}</span>
|
||||
<span class="ml-auto text-sm text-gray-500">×${this.selectedComplexity.multiplier}</span>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// Timeline
|
||||
if (this.selectedTimeline) {
|
||||
summaryHTML += `<div class="mb-2 flex items-center">
|
||||
<i class="fas fa-clock text-purple-500 mr-2"></i>
|
||||
<strong>${timelineLabel}:</strong>
|
||||
<span class="ml-2">${this.getTimelineName(this.selectedTimeline.value)}</span>
|
||||
<span class="ml-auto text-sm text-gray-500">×${this.selectedTimeline.multiplier}</span>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// Animate summary appearance
|
||||
summary.style.opacity = '0';
|
||||
summary.innerHTML = summaryHTML;
|
||||
|
||||
setTimeout(() => {
|
||||
summary.style.opacity = '1';
|
||||
}, 200);
|
||||
}
|
||||
|
||||
getServiceName(service) {
|
||||
if (window.calculatorTranslations && window.calculatorTranslations.services) {
|
||||
return window.calculatorTranslations.services[service] || service;
|
||||
}
|
||||
|
||||
const names = {
|
||||
web: 'Web Development',
|
||||
mobile: 'Mobile App',
|
||||
design: 'UI/UX Design',
|
||||
marketing: 'Digital Marketing'
|
||||
};
|
||||
return names[service] || service;
|
||||
}
|
||||
|
||||
getComplexityName(complexity) {
|
||||
if (window.calculatorTranslations && window.calculatorTranslations.complexity) {
|
||||
return window.calculatorTranslations.complexity[complexity] || complexity;
|
||||
}
|
||||
|
||||
const names = {
|
||||
simple: 'Simple',
|
||||
medium: 'Medium',
|
||||
complex: 'Complex'
|
||||
};
|
||||
return names[complexity] || complexity;
|
||||
}
|
||||
|
||||
getTimelineName(timeline) {
|
||||
if (window.calculatorTranslations && window.calculatorTranslations.timeline) {
|
||||
return window.calculatorTranslations.timeline[timeline] || timeline;
|
||||
}
|
||||
|
||||
const names = {
|
||||
standard: 'Standard',
|
||||
rush: 'Rush',
|
||||
extended: 'Extended'
|
||||
};
|
||||
return names[timeline] || timeline;
|
||||
}
|
||||
|
||||
restart() {
|
||||
// Reset all selections
|
||||
this.selectedServices = [];
|
||||
this.selectedComplexity = null;
|
||||
this.selectedTimeline = null;
|
||||
this.currentStep = 1;
|
||||
|
||||
// Reset UI
|
||||
document.querySelectorAll('.service-option, .complexity-option, .timeline-option').forEach(opt => {
|
||||
opt.classList.remove('selected');
|
||||
this.animateSelection(opt, false);
|
||||
});
|
||||
|
||||
// Reset steps
|
||||
document.querySelectorAll('.calculator-step').forEach(step => {
|
||||
step.classList.remove('active');
|
||||
step.style.display = 'none';
|
||||
step.style.opacity = '1';
|
||||
step.style.transform = 'translateX(0)';
|
||||
});
|
||||
|
||||
// Show first step
|
||||
const firstStep = document.getElementById('step-1');
|
||||
if (firstStep) {
|
||||
firstStep.classList.add('active');
|
||||
firstStep.style.display = 'block';
|
||||
}
|
||||
|
||||
this.updateProgressBar();
|
||||
this.updateStepButton();
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize calculator when DOM is loaded
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Theme initialization
|
||||
const theme = localStorage.getItem('theme') || 'light';
|
||||
document.documentElement.className = theme === 'dark' ? 'dark' : '';
|
||||
|
||||
// Initialize calculator
|
||||
if (document.querySelector('.calculator-step')) {
|
||||
new ProjectCalculator();
|
||||
}
|
||||
});
|
||||
384
.history/public/js/calculator_20251019182510.js
Normal file
384
.history/public/js/calculator_20251019182510.js
Normal file
@@ -0,0 +1,384 @@
|
||||
// Calculator Logic
|
||||
class ProjectCalculator {
|
||||
constructor() {
|
||||
this.selectedServices = [];
|
||||
this.selectedComplexity = null;
|
||||
this.selectedTimeline = null;
|
||||
this.currentStep = 1;
|
||||
this.totalSteps = 3;
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.setupEventListeners();
|
||||
this.updateProgressBar();
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
// Service selection
|
||||
document.querySelectorAll('.service-option').forEach(option => {
|
||||
option.addEventListener('click', (e) => this.handleServiceSelection(e));
|
||||
});
|
||||
|
||||
// Complexity selection
|
||||
document.querySelectorAll('.complexity-option').forEach(option => {
|
||||
option.addEventListener('click', (e) => this.handleComplexitySelection(e));
|
||||
});
|
||||
|
||||
// Timeline selection
|
||||
document.querySelectorAll('.timeline-option').forEach(option => {
|
||||
option.addEventListener('click', (e) => this.handleTimelineSelection(e));
|
||||
});
|
||||
|
||||
// Navigation buttons
|
||||
document.querySelectorAll('.next-step').forEach(btn => {
|
||||
btn.addEventListener('click', () => this.nextStep());
|
||||
});
|
||||
|
||||
document.querySelectorAll('.prev-step').forEach(btn => {
|
||||
btn.addEventListener('click', () => this.prevStep());
|
||||
});
|
||||
|
||||
// Restart button
|
||||
const restartBtn = document.querySelector('.restart-calculator');
|
||||
if (restartBtn) {
|
||||
restartBtn.addEventListener('click', () => this.restart());
|
||||
}
|
||||
}
|
||||
|
||||
handleServiceSelection(e) {
|
||||
const option = e.currentTarget;
|
||||
const service = option.dataset.service;
|
||||
const price = parseInt(option.dataset.basePrice);
|
||||
|
||||
option.classList.toggle('selected');
|
||||
|
||||
if (option.classList.contains('selected')) {
|
||||
this.selectedServices.push({ service, price });
|
||||
this.animateSelection(option, true);
|
||||
} else {
|
||||
this.selectedServices = this.selectedServices.filter(s => s.service !== service);
|
||||
this.animateSelection(option, false);
|
||||
}
|
||||
|
||||
this.updateStepButton();
|
||||
}
|
||||
|
||||
handleComplexitySelection(e) {
|
||||
const option = e.currentTarget;
|
||||
|
||||
// Clear previous selections
|
||||
document.querySelectorAll('.complexity-option').forEach(opt => {
|
||||
opt.classList.remove('selected');
|
||||
this.animateSelection(opt, false);
|
||||
});
|
||||
|
||||
// Select current option
|
||||
option.classList.add('selected');
|
||||
this.animateSelection(option, true);
|
||||
|
||||
this.selectedComplexity = {
|
||||
value: option.dataset.value,
|
||||
multiplier: parseFloat(option.dataset.multiplier)
|
||||
};
|
||||
|
||||
this.updateStepButton();
|
||||
}
|
||||
|
||||
handleTimelineSelection(e) {
|
||||
const option = e.currentTarget;
|
||||
|
||||
// Clear previous selections
|
||||
document.querySelectorAll('.timeline-option').forEach(opt => {
|
||||
opt.classList.remove('selected');
|
||||
this.animateSelection(opt, false);
|
||||
});
|
||||
|
||||
// Select current option
|
||||
option.classList.add('selected');
|
||||
this.animateSelection(option, true);
|
||||
|
||||
this.selectedTimeline = {
|
||||
value: option.dataset.value,
|
||||
multiplier: parseFloat(option.dataset.multiplier)
|
||||
};
|
||||
|
||||
this.updateStepButton();
|
||||
}
|
||||
|
||||
animateSelection(element, selected) {
|
||||
if (selected) {
|
||||
element.style.borderColor = '#3B82F6';
|
||||
element.style.backgroundColor = '#EBF8FF';
|
||||
element.style.transform = 'translateY(-2px)';
|
||||
element.style.boxShadow = '0 8px 25px rgba(59, 130, 246, 0.15)';
|
||||
} else {
|
||||
element.style.borderColor = '';
|
||||
element.style.backgroundColor = '';
|
||||
element.style.transform = '';
|
||||
element.style.boxShadow = '';
|
||||
}
|
||||
}
|
||||
|
||||
updateStepButton() {
|
||||
const step1Button = document.querySelector('#step-1 .next-step');
|
||||
const step2Button = document.querySelector('#step-2 .next-step');
|
||||
|
||||
if (step1Button) {
|
||||
step1Button.disabled = this.selectedServices.length === 0;
|
||||
step1Button.style.opacity = this.selectedServices.length > 0 ? '1' : '0.6';
|
||||
}
|
||||
|
||||
if (step2Button) {
|
||||
const isValid = this.selectedComplexity && this.selectedTimeline;
|
||||
step2Button.disabled = !isValid;
|
||||
step2Button.style.opacity = isValid ? '1' : '0.6';
|
||||
}
|
||||
}
|
||||
|
||||
updateProgressBar() {
|
||||
const progressBar = document.querySelector('.calculator-progress-bar');
|
||||
if (progressBar) {
|
||||
const progress = (this.currentStep / this.totalSteps) * 100;
|
||||
progressBar.style.width = `${progress}%`;
|
||||
progressBar.className = `calculator-progress-bar step-${this.currentStep}`;
|
||||
}
|
||||
}
|
||||
|
||||
nextStep() {
|
||||
if (this.currentStep >= this.totalSteps) return;
|
||||
|
||||
const currentStepElement = document.querySelector('.calculator-step.active');
|
||||
const nextStepElement = currentStepElement.nextElementSibling;
|
||||
|
||||
if (nextStepElement && nextStepElement.classList.contains('calculator-step')) {
|
||||
// Animate out current step
|
||||
currentStepElement.style.opacity = '0';
|
||||
currentStepElement.style.transform = 'translateX(-20px)';
|
||||
|
||||
setTimeout(() => {
|
||||
currentStepElement.classList.remove('active');
|
||||
currentStepElement.style.display = 'none';
|
||||
|
||||
// Animate in next step
|
||||
nextStepElement.classList.add('active');
|
||||
nextStepElement.style.display = 'block';
|
||||
nextStepElement.style.opacity = '0';
|
||||
nextStepElement.style.transform = 'translateX(20px)';
|
||||
|
||||
setTimeout(() => {
|
||||
nextStepElement.style.opacity = '1';
|
||||
nextStepElement.style.transform = 'translateX(0)';
|
||||
}, 50);
|
||||
|
||||
this.currentStep++;
|
||||
this.updateProgressBar();
|
||||
|
||||
if (this.currentStep === 3) {
|
||||
setTimeout(() => this.calculateFinalPrice(), 300);
|
||||
}
|
||||
}, 200);
|
||||
}
|
||||
}
|
||||
|
||||
prevStep() {
|
||||
if (this.currentStep <= 1) return;
|
||||
|
||||
const currentStepElement = document.querySelector('.calculator-step.active');
|
||||
const prevStepElement = currentStepElement.previousElementSibling;
|
||||
|
||||
if (prevStepElement && prevStepElement.classList.contains('calculator-step')) {
|
||||
// Animate out current step
|
||||
currentStepElement.style.opacity = '0';
|
||||
currentStepElement.style.transform = 'translateX(20px)';
|
||||
|
||||
setTimeout(() => {
|
||||
currentStepElement.classList.remove('active');
|
||||
currentStepElement.style.display = 'none';
|
||||
|
||||
// Animate in previous step
|
||||
prevStepElement.classList.add('active');
|
||||
prevStepElement.style.display = 'block';
|
||||
prevStepElement.style.opacity = '0';
|
||||
prevStepElement.style.transform = 'translateX(-20px)';
|
||||
|
||||
setTimeout(() => {
|
||||
prevStepElement.style.opacity = '1';
|
||||
prevStepElement.style.transform = 'translateX(0)';
|
||||
}, 50);
|
||||
|
||||
this.currentStep--;
|
||||
this.updateProgressBar();
|
||||
}, 200);
|
||||
}
|
||||
}
|
||||
|
||||
calculateFinalPrice() {
|
||||
let total = 0;
|
||||
|
||||
// Calculate base price from services
|
||||
this.selectedServices.forEach(service => {
|
||||
total += service.price;
|
||||
});
|
||||
|
||||
// Apply complexity multiplier
|
||||
if (this.selectedComplexity) {
|
||||
total *= this.selectedComplexity.multiplier;
|
||||
}
|
||||
|
||||
// Apply timeline multiplier
|
||||
if (this.selectedTimeline) {
|
||||
total *= this.selectedTimeline.multiplier;
|
||||
}
|
||||
|
||||
// Animate price reveal
|
||||
const priceElement = document.getElementById('final-price');
|
||||
if (priceElement) {
|
||||
priceElement.style.opacity = '0';
|
||||
priceElement.style.transform = 'scale(0.8)';
|
||||
|
||||
setTimeout(() => {
|
||||
priceElement.textContent = '₩' + total.toLocaleString();
|
||||
priceElement.style.opacity = '1';
|
||||
priceElement.style.transform = 'scale(1)';
|
||||
}, 300);
|
||||
}
|
||||
|
||||
// Generate summary
|
||||
setTimeout(() => this.generateSummary(total), 600);
|
||||
}
|
||||
|
||||
generateSummary(total) {
|
||||
const summary = document.getElementById('project-summary');
|
||||
if (!summary) return;
|
||||
|
||||
let summaryHTML = '';
|
||||
|
||||
// Services
|
||||
const servicesLabel = window.calculatorTranslations?.result?.selected_services || 'Selected Services';
|
||||
const complexityLabel = window.calculatorTranslations?.result?.complexity || 'Complexity';
|
||||
const timelineLabel = window.calculatorTranslations?.result?.timeline || 'Timeline';
|
||||
|
||||
summaryHTML += `<div class="mb-4"><strong>${servicesLabel}:</strong></div>`;
|
||||
this.selectedServices.forEach(service => {
|
||||
summaryHTML += `<div class="ml-4 mb-2 flex items-center">
|
||||
<i class="fas fa-check-circle text-green-500 mr-2"></i>
|
||||
${this.getServiceName(service.service)}
|
||||
<span class="ml-auto font-semibold">₩${service.price.toLocaleString()}</span>
|
||||
</div>`;
|
||||
});
|
||||
|
||||
// Complexity
|
||||
if (this.selectedComplexity) {
|
||||
summaryHTML += `<div class="mt-4 mb-2 flex items-center">
|
||||
<i class="fas fa-layer-group text-blue-500 mr-2"></i>
|
||||
<strong>${complexityLabel}:</strong>
|
||||
<span class="ml-2">${this.getComplexityName(this.selectedComplexity.value)}</span>
|
||||
<span class="ml-auto text-sm text-gray-500">×${this.selectedComplexity.multiplier}</span>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// Timeline
|
||||
if (this.selectedTimeline) {
|
||||
summaryHTML += `<div class="mb-2 flex items-center">
|
||||
<i class="fas fa-clock text-purple-500 mr-2"></i>
|
||||
<strong>${timelineLabel}:</strong>
|
||||
<span class="ml-2">${this.getTimelineName(this.selectedTimeline.value)}</span>
|
||||
<span class="ml-auto text-sm text-gray-500">×${this.selectedTimeline.multiplier}</span>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// Animate summary appearance
|
||||
summary.style.opacity = '0';
|
||||
summary.innerHTML = summaryHTML;
|
||||
|
||||
setTimeout(() => {
|
||||
summary.style.opacity = '1';
|
||||
}, 200);
|
||||
}
|
||||
|
||||
getServiceName(service) {
|
||||
if (window.calculatorTranslations && window.calculatorTranslations.services) {
|
||||
return window.calculatorTranslations.services[service] || service;
|
||||
}
|
||||
|
||||
const names = {
|
||||
web: 'Web Development',
|
||||
mobile: 'Mobile App',
|
||||
design: 'UI/UX Design',
|
||||
marketing: 'Digital Marketing'
|
||||
};
|
||||
return names[service] || service;
|
||||
}
|
||||
|
||||
getComplexityName(complexity) {
|
||||
if (window.calculatorTranslations && window.calculatorTranslations.complexity) {
|
||||
return window.calculatorTranslations.complexity[complexity] || complexity;
|
||||
}
|
||||
|
||||
const names = {
|
||||
simple: 'Simple',
|
||||
medium: 'Medium',
|
||||
complex: 'Complex'
|
||||
};
|
||||
return names[complexity] || complexity;
|
||||
}
|
||||
|
||||
getTimelineName(timeline) {
|
||||
if (window.calculatorTranslations && window.calculatorTranslations.timeline) {
|
||||
return window.calculatorTranslations.timeline[timeline] || timeline;
|
||||
}
|
||||
|
||||
const names = {
|
||||
standard: 'Standard',
|
||||
rush: 'Rush',
|
||||
extended: 'Extended'
|
||||
};
|
||||
return names[timeline] || timeline;
|
||||
}
|
||||
|
||||
restart() {
|
||||
// Reset all selections
|
||||
this.selectedServices = [];
|
||||
this.selectedComplexity = null;
|
||||
this.selectedTimeline = null;
|
||||
this.currentStep = 1;
|
||||
|
||||
// Reset UI
|
||||
document.querySelectorAll('.service-option, .complexity-option, .timeline-option').forEach(opt => {
|
||||
opt.classList.remove('selected');
|
||||
this.animateSelection(opt, false);
|
||||
});
|
||||
|
||||
// Reset steps
|
||||
document.querySelectorAll('.calculator-step').forEach(step => {
|
||||
step.classList.remove('active');
|
||||
step.style.display = 'none';
|
||||
step.style.opacity = '1';
|
||||
step.style.transform = 'translateX(0)';
|
||||
});
|
||||
|
||||
// Show first step
|
||||
const firstStep = document.getElementById('step-1');
|
||||
if (firstStep) {
|
||||
firstStep.classList.add('active');
|
||||
firstStep.style.display = 'block';
|
||||
}
|
||||
|
||||
this.updateProgressBar();
|
||||
this.updateStepButton();
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize calculator when DOM is loaded
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Theme initialization
|
||||
const theme = localStorage.getItem('theme') || 'light';
|
||||
document.documentElement.className = theme === 'dark' ? 'dark' : '';
|
||||
|
||||
// Initialize calculator
|
||||
if (document.querySelector('.calculator-step')) {
|
||||
new ProjectCalculator();
|
||||
}
|
||||
});
|
||||
384
.history/public/js/calculator_20251019182628.js
Normal file
384
.history/public/js/calculator_20251019182628.js
Normal file
@@ -0,0 +1,384 @@
|
||||
// Calculator Logic
|
||||
class ProjectCalculator {
|
||||
constructor() {
|
||||
this.selectedServices = [];
|
||||
this.selectedComplexity = null;
|
||||
this.selectedTimeline = null;
|
||||
this.currentStep = 1;
|
||||
this.totalSteps = 3;
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.setupEventListeners();
|
||||
this.updateProgressBar();
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
// Service selection
|
||||
document.querySelectorAll('.service-option').forEach(option => {
|
||||
option.addEventListener('click', (e) => this.handleServiceSelection(e));
|
||||
});
|
||||
|
||||
// Complexity selection
|
||||
document.querySelectorAll('.complexity-option').forEach(option => {
|
||||
option.addEventListener('click', (e) => this.handleComplexitySelection(e));
|
||||
});
|
||||
|
||||
// Timeline selection
|
||||
document.querySelectorAll('.timeline-option').forEach(option => {
|
||||
option.addEventListener('click', (e) => this.handleTimelineSelection(e));
|
||||
});
|
||||
|
||||
// Navigation buttons
|
||||
document.querySelectorAll('.next-step').forEach(btn => {
|
||||
btn.addEventListener('click', () => this.nextStep());
|
||||
});
|
||||
|
||||
document.querySelectorAll('.prev-step').forEach(btn => {
|
||||
btn.addEventListener('click', () => this.prevStep());
|
||||
});
|
||||
|
||||
// Restart button
|
||||
const restartBtn = document.querySelector('.restart-calculator');
|
||||
if (restartBtn) {
|
||||
restartBtn.addEventListener('click', () => this.restart());
|
||||
}
|
||||
}
|
||||
|
||||
handleServiceSelection(e) {
|
||||
const option = e.currentTarget;
|
||||
const service = option.dataset.service;
|
||||
const price = parseInt(option.dataset.basePrice);
|
||||
|
||||
option.classList.toggle('selected');
|
||||
|
||||
if (option.classList.contains('selected')) {
|
||||
this.selectedServices.push({ service, price });
|
||||
this.animateSelection(option, true);
|
||||
} else {
|
||||
this.selectedServices = this.selectedServices.filter(s => s.service !== service);
|
||||
this.animateSelection(option, false);
|
||||
}
|
||||
|
||||
this.updateStepButton();
|
||||
}
|
||||
|
||||
handleComplexitySelection(e) {
|
||||
const option = e.currentTarget;
|
||||
|
||||
// Clear previous selections
|
||||
document.querySelectorAll('.complexity-option').forEach(opt => {
|
||||
opt.classList.remove('selected');
|
||||
this.animateSelection(opt, false);
|
||||
});
|
||||
|
||||
// Select current option
|
||||
option.classList.add('selected');
|
||||
this.animateSelection(option, true);
|
||||
|
||||
this.selectedComplexity = {
|
||||
value: option.dataset.value,
|
||||
multiplier: parseFloat(option.dataset.multiplier)
|
||||
};
|
||||
|
||||
this.updateStepButton();
|
||||
}
|
||||
|
||||
handleTimelineSelection(e) {
|
||||
const option = e.currentTarget;
|
||||
|
||||
// Clear previous selections
|
||||
document.querySelectorAll('.timeline-option').forEach(opt => {
|
||||
opt.classList.remove('selected');
|
||||
this.animateSelection(opt, false);
|
||||
});
|
||||
|
||||
// Select current option
|
||||
option.classList.add('selected');
|
||||
this.animateSelection(option, true);
|
||||
|
||||
this.selectedTimeline = {
|
||||
value: option.dataset.value,
|
||||
multiplier: parseFloat(option.dataset.multiplier)
|
||||
};
|
||||
|
||||
this.updateStepButton();
|
||||
}
|
||||
|
||||
animateSelection(element, selected) {
|
||||
if (selected) {
|
||||
element.style.borderColor = '#3B82F6';
|
||||
element.style.backgroundColor = '#EBF8FF';
|
||||
element.style.transform = 'translateY(-2px)';
|
||||
element.style.boxShadow = '0 8px 25px rgba(59, 130, 246, 0.15)';
|
||||
} else {
|
||||
element.style.borderColor = '';
|
||||
element.style.backgroundColor = '';
|
||||
element.style.transform = '';
|
||||
element.style.boxShadow = '';
|
||||
}
|
||||
}
|
||||
|
||||
updateStepButton() {
|
||||
const step1Button = document.querySelector('#step-1 .next-step');
|
||||
const step2Button = document.querySelector('#step-2 .next-step');
|
||||
|
||||
if (step1Button) {
|
||||
step1Button.disabled = this.selectedServices.length === 0;
|
||||
step1Button.style.opacity = this.selectedServices.length > 0 ? '1' : '0.6';
|
||||
}
|
||||
|
||||
if (step2Button) {
|
||||
const isValid = this.selectedComplexity && this.selectedTimeline;
|
||||
step2Button.disabled = !isValid;
|
||||
step2Button.style.opacity = isValid ? '1' : '0.6';
|
||||
}
|
||||
}
|
||||
|
||||
updateProgressBar() {
|
||||
const progressBar = document.querySelector('.calculator-progress-bar');
|
||||
if (progressBar) {
|
||||
const progress = (this.currentStep / this.totalSteps) * 100;
|
||||
progressBar.style.width = `${progress}%`;
|
||||
progressBar.className = `calculator-progress-bar step-${this.currentStep}`;
|
||||
}
|
||||
}
|
||||
|
||||
nextStep() {
|
||||
if (this.currentStep >= this.totalSteps) return;
|
||||
|
||||
const currentStepElement = document.querySelector('.calculator-step.active');
|
||||
const nextStepElement = currentStepElement.nextElementSibling;
|
||||
|
||||
if (nextStepElement && nextStepElement.classList.contains('calculator-step')) {
|
||||
// Animate out current step
|
||||
currentStepElement.style.opacity = '0';
|
||||
currentStepElement.style.transform = 'translateX(-20px)';
|
||||
|
||||
setTimeout(() => {
|
||||
currentStepElement.classList.remove('active');
|
||||
currentStepElement.style.display = 'none';
|
||||
|
||||
// Animate in next step
|
||||
nextStepElement.classList.add('active');
|
||||
nextStepElement.style.display = 'block';
|
||||
nextStepElement.style.opacity = '0';
|
||||
nextStepElement.style.transform = 'translateX(20px)';
|
||||
|
||||
setTimeout(() => {
|
||||
nextStepElement.style.opacity = '1';
|
||||
nextStepElement.style.transform = 'translateX(0)';
|
||||
}, 50);
|
||||
|
||||
this.currentStep++;
|
||||
this.updateProgressBar();
|
||||
|
||||
if (this.currentStep === 3) {
|
||||
setTimeout(() => this.calculateFinalPrice(), 300);
|
||||
}
|
||||
}, 200);
|
||||
}
|
||||
}
|
||||
|
||||
prevStep() {
|
||||
if (this.currentStep <= 1) return;
|
||||
|
||||
const currentStepElement = document.querySelector('.calculator-step.active');
|
||||
const prevStepElement = currentStepElement.previousElementSibling;
|
||||
|
||||
if (prevStepElement && prevStepElement.classList.contains('calculator-step')) {
|
||||
// Animate out current step
|
||||
currentStepElement.style.opacity = '0';
|
||||
currentStepElement.style.transform = 'translateX(20px)';
|
||||
|
||||
setTimeout(() => {
|
||||
currentStepElement.classList.remove('active');
|
||||
currentStepElement.style.display = 'none';
|
||||
|
||||
// Animate in previous step
|
||||
prevStepElement.classList.add('active');
|
||||
prevStepElement.style.display = 'block';
|
||||
prevStepElement.style.opacity = '0';
|
||||
prevStepElement.style.transform = 'translateX(-20px)';
|
||||
|
||||
setTimeout(() => {
|
||||
prevStepElement.style.opacity = '1';
|
||||
prevStepElement.style.transform = 'translateX(0)';
|
||||
}, 50);
|
||||
|
||||
this.currentStep--;
|
||||
this.updateProgressBar();
|
||||
}, 200);
|
||||
}
|
||||
}
|
||||
|
||||
calculateFinalPrice() {
|
||||
let total = 0;
|
||||
|
||||
// Calculate base price from services
|
||||
this.selectedServices.forEach(service => {
|
||||
total += service.price;
|
||||
});
|
||||
|
||||
// Apply complexity multiplier
|
||||
if (this.selectedComplexity) {
|
||||
total *= this.selectedComplexity.multiplier;
|
||||
}
|
||||
|
||||
// Apply timeline multiplier
|
||||
if (this.selectedTimeline) {
|
||||
total *= this.selectedTimeline.multiplier;
|
||||
}
|
||||
|
||||
// Animate price reveal
|
||||
const priceElement = document.getElementById('final-price');
|
||||
if (priceElement) {
|
||||
priceElement.style.opacity = '0';
|
||||
priceElement.style.transform = 'scale(0.8)';
|
||||
|
||||
setTimeout(() => {
|
||||
priceElement.textContent = '₩' + total.toLocaleString();
|
||||
priceElement.style.opacity = '1';
|
||||
priceElement.style.transform = 'scale(1)';
|
||||
}, 300);
|
||||
}
|
||||
|
||||
// Generate summary
|
||||
setTimeout(() => this.generateSummary(total), 600);
|
||||
}
|
||||
|
||||
generateSummary(total) {
|
||||
const summary = document.getElementById('project-summary');
|
||||
if (!summary) return;
|
||||
|
||||
let summaryHTML = '';
|
||||
|
||||
// Services
|
||||
const servicesLabel = window.calculatorTranslations?.result?.selected_services || 'Selected Services';
|
||||
const complexityLabel = window.calculatorTranslations?.result?.complexity || 'Complexity';
|
||||
const timelineLabel = window.calculatorTranslations?.result?.timeline || 'Timeline';
|
||||
|
||||
summaryHTML += `<div class="mb-4"><strong>${servicesLabel}:</strong></div>`;
|
||||
this.selectedServices.forEach(service => {
|
||||
summaryHTML += `<div class="ml-4 mb-2 flex items-center">
|
||||
<i class="fas fa-check-circle text-green-500 mr-2"></i>
|
||||
${this.getServiceName(service.service)}
|
||||
<span class="ml-auto font-semibold">₩${service.price.toLocaleString()}</span>
|
||||
</div>`;
|
||||
});
|
||||
|
||||
// Complexity
|
||||
if (this.selectedComplexity) {
|
||||
summaryHTML += `<div class="mt-4 mb-2 flex items-center">
|
||||
<i class="fas fa-layer-group text-blue-500 mr-2"></i>
|
||||
<strong>${complexityLabel}:</strong>
|
||||
<span class="ml-2">${this.getComplexityName(this.selectedComplexity.value)}</span>
|
||||
<span class="ml-auto text-sm text-gray-500">×${this.selectedComplexity.multiplier}</span>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// Timeline
|
||||
if (this.selectedTimeline) {
|
||||
summaryHTML += `<div class="mb-2 flex items-center">
|
||||
<i class="fas fa-clock text-purple-500 mr-2"></i>
|
||||
<strong>${timelineLabel}:</strong>
|
||||
<span class="ml-2">${this.getTimelineName(this.selectedTimeline.value)}</span>
|
||||
<span class="ml-auto text-sm text-gray-500">×${this.selectedTimeline.multiplier}</span>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// Animate summary appearance
|
||||
summary.style.opacity = '0';
|
||||
summary.innerHTML = summaryHTML;
|
||||
|
||||
setTimeout(() => {
|
||||
summary.style.opacity = '1';
|
||||
}, 200);
|
||||
}
|
||||
|
||||
getServiceName(service) {
|
||||
if (window.calculatorTranslations && window.calculatorTranslations.services) {
|
||||
return window.calculatorTranslations.services[service] || service;
|
||||
}
|
||||
|
||||
const names = {
|
||||
web: 'Web Development',
|
||||
mobile: 'Mobile App',
|
||||
design: 'UI/UX Design',
|
||||
marketing: 'Digital Marketing'
|
||||
};
|
||||
return names[service] || service;
|
||||
}
|
||||
|
||||
getComplexityName(complexity) {
|
||||
if (window.calculatorTranslations && window.calculatorTranslations.complexity) {
|
||||
return window.calculatorTranslations.complexity[complexity] || complexity;
|
||||
}
|
||||
|
||||
const names = {
|
||||
simple: 'Simple',
|
||||
medium: 'Medium',
|
||||
complex: 'Complex'
|
||||
};
|
||||
return names[complexity] || complexity;
|
||||
}
|
||||
|
||||
getTimelineName(timeline) {
|
||||
if (window.calculatorTranslations && window.calculatorTranslations.timeline) {
|
||||
return window.calculatorTranslations.timeline[timeline] || timeline;
|
||||
}
|
||||
|
||||
const names = {
|
||||
standard: 'Standard',
|
||||
rush: 'Rush',
|
||||
extended: 'Extended'
|
||||
};
|
||||
return names[timeline] || timeline;
|
||||
}
|
||||
|
||||
restart() {
|
||||
// Reset all selections
|
||||
this.selectedServices = [];
|
||||
this.selectedComplexity = null;
|
||||
this.selectedTimeline = null;
|
||||
this.currentStep = 1;
|
||||
|
||||
// Reset UI
|
||||
document.querySelectorAll('.service-option, .complexity-option, .timeline-option').forEach(opt => {
|
||||
opt.classList.remove('selected');
|
||||
this.animateSelection(opt, false);
|
||||
});
|
||||
|
||||
// Reset steps
|
||||
document.querySelectorAll('.calculator-step').forEach(step => {
|
||||
step.classList.remove('active');
|
||||
step.style.display = 'none';
|
||||
step.style.opacity = '1';
|
||||
step.style.transform = 'translateX(0)';
|
||||
});
|
||||
|
||||
// Show first step
|
||||
const firstStep = document.getElementById('step-1');
|
||||
if (firstStep) {
|
||||
firstStep.classList.add('active');
|
||||
firstStep.style.display = 'block';
|
||||
}
|
||||
|
||||
this.updateProgressBar();
|
||||
this.updateStepButton();
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize calculator when DOM is loaded
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Theme initialization
|
||||
const theme = localStorage.getItem('theme') || 'light';
|
||||
document.documentElement.className = theme === 'dark' ? 'dark' : '';
|
||||
|
||||
// Initialize calculator
|
||||
if (document.querySelector('.calculator-step')) {
|
||||
new ProjectCalculator();
|
||||
}
|
||||
});
|
||||
544
.history/public/js/main_20251019161602.js
Normal file
544
.history/public/js/main_20251019161602.js
Normal file
@@ -0,0 +1,544 @@
|
||||
// Main JavaScript for SmartSolTech Website
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Initialize AOS (Animate On Scroll)
|
||||
if (typeof AOS !== 'undefined') {
|
||||
AOS.init({
|
||||
duration: 800,
|
||||
easing: 'ease-in-out',
|
||||
once: true,
|
||||
offset: 100
|
||||
});
|
||||
}
|
||||
|
||||
// Mobile Navigation Toggle
|
||||
const mobileMenuButton = document.querySelector('.mobile-menu-button');
|
||||
const mobileMenu = document.querySelector('.mobile-menu');
|
||||
|
||||
if (mobileMenuButton && mobileMenu) {
|
||||
mobileMenuButton.addEventListener('click', function() {
|
||||
mobileMenu.classList.toggle('show');
|
||||
const isOpen = mobileMenu.classList.contains('show');
|
||||
|
||||
// Toggle button icon
|
||||
const icon = mobileMenuButton.querySelector('svg');
|
||||
if (icon) {
|
||||
icon.innerHTML = isOpen
|
||||
? '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />'
|
||||
: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />';
|
||||
}
|
||||
|
||||
// Accessibility
|
||||
mobileMenuButton.setAttribute('aria-expanded', isOpen);
|
||||
});
|
||||
|
||||
// Close mobile menu when clicking outside
|
||||
document.addEventListener('click', function(e) {
|
||||
if (!mobileMenuButton.contains(e.target) && !mobileMenu.contains(e.target)) {
|
||||
mobileMenu.classList.remove('show');
|
||||
mobileMenuButton.setAttribute('aria-expanded', 'false');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Navbar Scroll Effect
|
||||
const navbar = document.querySelector('nav');
|
||||
let lastScrollY = window.scrollY;
|
||||
|
||||
window.addEventListener('scroll', function() {
|
||||
const currentScrollY = window.scrollY;
|
||||
|
||||
if (navbar) {
|
||||
if (currentScrollY > 100) {
|
||||
navbar.classList.add('navbar-scrolled');
|
||||
} else {
|
||||
navbar.classList.remove('navbar-scrolled');
|
||||
}
|
||||
|
||||
// Hide/show navbar on scroll
|
||||
if (currentScrollY > lastScrollY && currentScrollY > 200) {
|
||||
navbar.style.transform = 'translateY(-100%)';
|
||||
} else {
|
||||
navbar.style.transform = 'translateY(0)';
|
||||
}
|
||||
}
|
||||
|
||||
lastScrollY = currentScrollY;
|
||||
});
|
||||
|
||||
// Smooth Scrolling for Anchor Links
|
||||
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
|
||||
anchor.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
const target = document.querySelector(this.getAttribute('href'));
|
||||
if (target) {
|
||||
const offsetTop = target.offsetTop - (navbar ? navbar.offsetHeight : 0);
|
||||
window.scrollTo({
|
||||
top: offsetTop,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Contact Form Handler
|
||||
const quickContactForm = document.getElementById('quick-contact-form');
|
||||
if (quickContactForm) {
|
||||
quickContactForm.addEventListener('submit', handleContactSubmit);
|
||||
}
|
||||
|
||||
const mainContactForm = document.getElementById('contact-form');
|
||||
if (mainContactForm) {
|
||||
mainContactForm.addEventListener('submit', handleContactSubmit);
|
||||
}
|
||||
|
||||
async function handleContactSubmit(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const form = e.target;
|
||||
const submitButton = form.querySelector('button[type="submit"]');
|
||||
const originalText = submitButton.textContent;
|
||||
|
||||
// Show loading state
|
||||
submitButton.disabled = true;
|
||||
submitButton.classList.add('btn-loading');
|
||||
submitButton.textContent = '전송 중...';
|
||||
|
||||
try {
|
||||
const formData = new FormData(form);
|
||||
const data = Object.fromEntries(formData.entries());
|
||||
|
||||
const response = await fetch('/api/contact/submit', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
showNotification('메시지가 성공적으로 전송되었습니다! 곧 연락드리겠습니다.', 'success');
|
||||
form.reset();
|
||||
} else {
|
||||
showNotification('메시지 전송 중 오류가 발생했습니다. 다시 시도해주세요.', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Contact form error:', error);
|
||||
showNotification('메시지 전송 중 오류가 발생했습니다. 다시 시도해주세요.', 'error');
|
||||
} finally {
|
||||
// Reset button state
|
||||
submitButton.disabled = false;
|
||||
submitButton.classList.remove('btn-loading');
|
||||
submitButton.textContent = originalText;
|
||||
}
|
||||
}
|
||||
|
||||
// Notification System
|
||||
function showNotification(message, type = 'info') {
|
||||
const notification = document.createElement('div');
|
||||
notification.className = `notification notification-${type}`;
|
||||
notification.innerHTML = `
|
||||
<div class="notification-content">
|
||||
<span class="notification-message">${message}</span>
|
||||
<button class="notification-close" aria-label="Close notification">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Styles
|
||||
notification.style.cssText = `
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
z-index: 9999;
|
||||
max-width: 400px;
|
||||
padding: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
color: white;
|
||||
font-weight: 500;
|
||||
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
|
||||
transform: translateX(100%);
|
||||
transition: transform 0.3s ease;
|
||||
${type === 'success' ? 'background: #10b981;' : ''}
|
||||
${type === 'error' ? 'background: #ef4444;' : ''}
|
||||
${type === 'info' ? 'background: #3b82f6;' : ''}
|
||||
`;
|
||||
|
||||
document.body.appendChild(notification);
|
||||
|
||||
// Animate in
|
||||
setTimeout(() => {
|
||||
notification.style.transform = 'translateX(0)';
|
||||
}, 100);
|
||||
|
||||
// Close button handler
|
||||
const closeButton = notification.querySelector('.notification-close');
|
||||
closeButton.addEventListener('click', () => {
|
||||
closeNotification(notification);
|
||||
});
|
||||
|
||||
// Auto close after 5 seconds
|
||||
setTimeout(() => {
|
||||
closeNotification(notification);
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
function closeNotification(notification) {
|
||||
notification.style.transform = 'translateX(100%)';
|
||||
setTimeout(() => {
|
||||
if (notification.parentNode) {
|
||||
notification.parentNode.removeChild(notification);
|
||||
}
|
||||
}, 300);
|
||||
}
|
||||
|
||||
// Portfolio Filter (if on portfolio page)
|
||||
const portfolioFilters = document.querySelectorAll('.portfolio-filter');
|
||||
const portfolioItems = document.querySelectorAll('.portfolio-item');
|
||||
|
||||
portfolioFilters.forEach(filter => {
|
||||
filter.addEventListener('click', function() {
|
||||
const category = this.dataset.category;
|
||||
|
||||
// Update active filter
|
||||
portfolioFilters.forEach(f => f.classList.remove('active'));
|
||||
this.classList.add('active');
|
||||
|
||||
// Filter items
|
||||
portfolioItems.forEach(item => {
|
||||
if (category === 'all' || item.dataset.category === category) {
|
||||
item.style.display = 'block';
|
||||
item.style.animation = 'fadeIn 0.5s ease';
|
||||
} else {
|
||||
item.style.display = 'none';
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Image Lazy Loading
|
||||
const images = document.querySelectorAll('img[data-src]');
|
||||
const imageObserver = new IntersectionObserver((entries, observer) => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting) {
|
||||
const img = entry.target;
|
||||
img.src = img.dataset.src;
|
||||
img.classList.remove('lazy');
|
||||
imageObserver.unobserve(img);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
images.forEach(img => imageObserver.observe(img));
|
||||
|
||||
// Service Worker Registration for PWA
|
||||
if ('serviceWorker' in navigator && 'PushManager' in window) {
|
||||
navigator.serviceWorker.register('/sw.js')
|
||||
.then(registration => {
|
||||
console.log('Service Worker registered successfully:', registration);
|
||||
})
|
||||
.catch(error => {
|
||||
console.log('Service Worker registration failed:', error);
|
||||
});
|
||||
}
|
||||
|
||||
// Performance Monitoring
|
||||
if ('performance' in window) {
|
||||
window.addEventListener('load', () => {
|
||||
setTimeout(() => {
|
||||
const perfData = performance.getEntriesByType('navigation')[0];
|
||||
const loadTime = perfData.loadEventEnd - perfData.loadEventStart;
|
||||
|
||||
if (loadTime > 3000) {
|
||||
console.warn('Page load time is slow:', loadTime + 'ms');
|
||||
}
|
||||
}, 1000);
|
||||
});
|
||||
}
|
||||
|
||||
// Cookie Consent (if needed)
|
||||
function initCookieConsent() {
|
||||
const consent = localStorage.getItem('cookieConsent');
|
||||
if (!consent) {
|
||||
showCookieConsent();
|
||||
}
|
||||
}
|
||||
|
||||
function showCookieConsent() {
|
||||
const banner = document.createElement('div');
|
||||
banner.className = 'cookie-consent';
|
||||
banner.innerHTML = `
|
||||
<div class="cookie-content">
|
||||
<p>이 웹사이트는 더 나은 서비스 제공을 위해 쿠키를 사용합니다.</p>
|
||||
<div class="cookie-buttons">
|
||||
<button id="accept-cookies" class="btn btn-primary">동의</button>
|
||||
<button id="decline-cookies" class="btn btn-secondary">거부</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
banner.style.cssText = `
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: rgba(0, 0, 0, 0.9);
|
||||
color: white;
|
||||
padding: 1rem;
|
||||
z-index: 9999;
|
||||
transform: translateY(100%);
|
||||
transition: transform 0.3s ease;
|
||||
`;
|
||||
|
||||
document.body.appendChild(banner);
|
||||
|
||||
setTimeout(() => {
|
||||
banner.style.transform = 'translateY(0)';
|
||||
}, 100);
|
||||
|
||||
document.getElementById('accept-cookies').addEventListener('click', () => {
|
||||
localStorage.setItem('cookieConsent', 'accepted');
|
||||
banner.style.transform = 'translateY(100%)';
|
||||
setTimeout(() => banner.remove(), 300);
|
||||
});
|
||||
|
||||
document.getElementById('decline-cookies').addEventListener('click', () => {
|
||||
localStorage.setItem('cookieConsent', 'declined');
|
||||
banner.style.transform = 'translateY(100%)';
|
||||
setTimeout(() => banner.remove(), 300);
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize cookie consent
|
||||
// initCookieConsent();
|
||||
|
||||
// Parallax Effect
|
||||
const parallaxElements = document.querySelectorAll('.parallax');
|
||||
|
||||
function updateParallax() {
|
||||
const scrollY = window.pageYOffset;
|
||||
|
||||
parallaxElements.forEach(element => {
|
||||
const speed = element.dataset.speed || 0.5;
|
||||
const yPos = -(scrollY * speed);
|
||||
element.style.transform = `translateY(${yPos}px)`;
|
||||
});
|
||||
}
|
||||
|
||||
if (parallaxElements.length > 0) {
|
||||
window.addEventListener('scroll', updateParallax);
|
||||
}
|
||||
|
||||
// Counter Animation
|
||||
const counters = document.querySelectorAll('.counter');
|
||||
const counterObserver = new IntersectionObserver((entries) => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting) {
|
||||
animateCounter(entry.target);
|
||||
counterObserver.unobserve(entry.target);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
counters.forEach(counter => counterObserver.observe(counter));
|
||||
|
||||
function animateCounter(element) {
|
||||
const target = parseInt(element.dataset.count);
|
||||
const duration = 2000;
|
||||
const start = performance.now();
|
||||
|
||||
function updateCounter(currentTime) {
|
||||
const elapsed = currentTime - start;
|
||||
const progress = Math.min(elapsed / duration, 1);
|
||||
|
||||
const current = Math.floor(progress * target);
|
||||
element.textContent = current.toLocaleString();
|
||||
|
||||
if (progress < 1) {
|
||||
requestAnimationFrame(updateCounter);
|
||||
}
|
||||
}
|
||||
|
||||
requestAnimationFrame(updateCounter);
|
||||
}
|
||||
|
||||
// Typing Effect for Hero Text
|
||||
const typingElements = document.querySelectorAll('.typing-effect');
|
||||
|
||||
typingElements.forEach(element => {
|
||||
const text = element.textContent;
|
||||
element.textContent = '';
|
||||
element.style.borderRight = '2px solid';
|
||||
|
||||
let i = 0;
|
||||
const timer = setInterval(() => {
|
||||
element.textContent += text[i];
|
||||
i++;
|
||||
|
||||
if (i >= text.length) {
|
||||
clearInterval(timer);
|
||||
element.style.borderRight = 'none';
|
||||
}
|
||||
}, 100);
|
||||
});
|
||||
|
||||
// Handle form validation
|
||||
const forms = document.querySelectorAll('form[data-validate]');
|
||||
forms.forEach(form => {
|
||||
form.addEventListener('submit', function(e) {
|
||||
if (!validateForm(this)) {
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
|
||||
// Real-time validation
|
||||
const inputs = form.querySelectorAll('input, textarea');
|
||||
inputs.forEach(input => {
|
||||
input.addEventListener('blur', () => validateField(input));
|
||||
input.addEventListener('input', () => clearFieldError(input));
|
||||
});
|
||||
});
|
||||
|
||||
function validateForm(form) {
|
||||
let isValid = true;
|
||||
const inputs = form.querySelectorAll('input[required], textarea[required]');
|
||||
|
||||
inputs.forEach(input => {
|
||||
if (!validateField(input)) {
|
||||
isValid = false;
|
||||
}
|
||||
});
|
||||
|
||||
return isValid;
|
||||
}
|
||||
|
||||
function validateField(field) {
|
||||
const value = field.value.trim();
|
||||
const type = field.type;
|
||||
let isValid = true;
|
||||
let message = '';
|
||||
|
||||
// Required field check
|
||||
if (field.hasAttribute('required') && !value) {
|
||||
isValid = false;
|
||||
message = '이 필드는 필수입니다.';
|
||||
}
|
||||
|
||||
// Email validation
|
||||
if (type === 'email' && value) {
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (!emailRegex.test(value)) {
|
||||
isValid = false;
|
||||
message = '올바른 이메일 형식을 입력해주세요.';
|
||||
}
|
||||
}
|
||||
|
||||
// Phone validation
|
||||
if (type === 'tel' && value) {
|
||||
const phoneRegex = /^[0-9-+\s()]+$/;
|
||||
if (!phoneRegex.test(value)) {
|
||||
isValid = false;
|
||||
message = '올바른 전화번호 형식을 입력해주세요.';
|
||||
}
|
||||
}
|
||||
|
||||
// Show/hide error
|
||||
if (!isValid) {
|
||||
showFieldError(field, message);
|
||||
} else {
|
||||
clearFieldError(field);
|
||||
}
|
||||
|
||||
return isValid;
|
||||
}
|
||||
|
||||
function showFieldError(field, message) {
|
||||
clearFieldError(field);
|
||||
|
||||
field.classList.add('error');
|
||||
const errorDiv = document.createElement('div');
|
||||
errorDiv.className = 'field-error';
|
||||
errorDiv.textContent = message;
|
||||
errorDiv.style.cssText = 'color: #ef4444; font-size: 0.875rem; margin-top: 0.25rem;';
|
||||
|
||||
field.parentNode.appendChild(errorDiv);
|
||||
}
|
||||
|
||||
function clearFieldError(field) {
|
||||
field.classList.remove('error');
|
||||
const errorDiv = field.parentNode.querySelector('.field-error');
|
||||
if (errorDiv) {
|
||||
errorDiv.remove();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Utility Functions
|
||||
const utils = {
|
||||
// Debounce function
|
||||
debounce: function(func, wait) {
|
||||
let timeout;
|
||||
return function executedFunction(...args) {
|
||||
const later = () => {
|
||||
clearTimeout(timeout);
|
||||
func(...args);
|
||||
};
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(later, wait);
|
||||
};
|
||||
},
|
||||
|
||||
// Throttle function
|
||||
throttle: function(func, limit) {
|
||||
let inThrottle;
|
||||
return function() {
|
||||
const args = arguments;
|
||||
const context = this;
|
||||
if (!inThrottle) {
|
||||
func.apply(context, args);
|
||||
inThrottle = true;
|
||||
setTimeout(() => inThrottle = false, limit);
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
// Format currency
|
||||
formatCurrency: function(amount, currency = 'KRW') {
|
||||
return new Intl.NumberFormat('ko-KR', {
|
||||
style: 'currency',
|
||||
currency: currency,
|
||||
minimumFractionDigits: 0
|
||||
}).format(amount);
|
||||
},
|
||||
|
||||
// Format date
|
||||
formatDate: function(date, options = {}) {
|
||||
const defaultOptions = {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
};
|
||||
return new Intl.DateTimeFormat('ko-KR', { ...defaultOptions, ...options }).format(new Date(date));
|
||||
}
|
||||
};
|
||||
|
||||
// Global error handler
|
||||
window.addEventListener('error', function(e) {
|
||||
console.error('Global error:', e.error);
|
||||
// Could send error to analytics service
|
||||
});
|
||||
|
||||
// Unhandled promise rejection handler
|
||||
window.addEventListener('unhandledrejection', function(e) {
|
||||
console.error('Unhandled promise rejection:', e.reason);
|
||||
// Could send error to analytics service
|
||||
});
|
||||
|
||||
// Export for use in other modules
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = utils;
|
||||
}
|
||||
544
.history/public/js/main_20251019162545.js
Normal file
544
.history/public/js/main_20251019162545.js
Normal file
@@ -0,0 +1,544 @@
|
||||
// Main JavaScript for SmartSolTech Website
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Initialize AOS (Animate On Scroll)
|
||||
if (typeof AOS !== 'undefined') {
|
||||
AOS.init({
|
||||
duration: 800,
|
||||
easing: 'ease-in-out',
|
||||
once: true,
|
||||
offset: 100
|
||||
});
|
||||
}
|
||||
|
||||
// Mobile Navigation Toggle
|
||||
const mobileMenuButton = document.querySelector('.mobile-menu-button');
|
||||
const mobileMenu = document.querySelector('.mobile-menu');
|
||||
|
||||
if (mobileMenuButton && mobileMenu) {
|
||||
mobileMenuButton.addEventListener('click', function() {
|
||||
mobileMenu.classList.toggle('show');
|
||||
const isOpen = mobileMenu.classList.contains('show');
|
||||
|
||||
// Toggle button icon
|
||||
const icon = mobileMenuButton.querySelector('svg');
|
||||
if (icon) {
|
||||
icon.innerHTML = isOpen
|
||||
? '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />'
|
||||
: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />';
|
||||
}
|
||||
|
||||
// Accessibility
|
||||
mobileMenuButton.setAttribute('aria-expanded', isOpen);
|
||||
});
|
||||
|
||||
// Close mobile menu when clicking outside
|
||||
document.addEventListener('click', function(e) {
|
||||
if (!mobileMenuButton.contains(e.target) && !mobileMenu.contains(e.target)) {
|
||||
mobileMenu.classList.remove('show');
|
||||
mobileMenuButton.setAttribute('aria-expanded', 'false');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Navbar Scroll Effect
|
||||
const navbar = document.querySelector('nav');
|
||||
let lastScrollY = window.scrollY;
|
||||
|
||||
window.addEventListener('scroll', function() {
|
||||
const currentScrollY = window.scrollY;
|
||||
|
||||
if (navbar) {
|
||||
if (currentScrollY > 100) {
|
||||
navbar.classList.add('navbar-scrolled');
|
||||
} else {
|
||||
navbar.classList.remove('navbar-scrolled');
|
||||
}
|
||||
|
||||
// Hide/show navbar on scroll
|
||||
if (currentScrollY > lastScrollY && currentScrollY > 200) {
|
||||
navbar.style.transform = 'translateY(-100%)';
|
||||
} else {
|
||||
navbar.style.transform = 'translateY(0)';
|
||||
}
|
||||
}
|
||||
|
||||
lastScrollY = currentScrollY;
|
||||
});
|
||||
|
||||
// Smooth Scrolling for Anchor Links
|
||||
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
|
||||
anchor.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
const target = document.querySelector(this.getAttribute('href'));
|
||||
if (target) {
|
||||
const offsetTop = target.offsetTop - (navbar ? navbar.offsetHeight : 0);
|
||||
window.scrollTo({
|
||||
top: offsetTop,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Contact Form Handler
|
||||
const quickContactForm = document.getElementById('quick-contact-form');
|
||||
if (quickContactForm) {
|
||||
quickContactForm.addEventListener('submit', handleContactSubmit);
|
||||
}
|
||||
|
||||
const mainContactForm = document.getElementById('contact-form');
|
||||
if (mainContactForm) {
|
||||
mainContactForm.addEventListener('submit', handleContactSubmit);
|
||||
}
|
||||
|
||||
async function handleContactSubmit(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const form = e.target;
|
||||
const submitButton = form.querySelector('button[type="submit"]');
|
||||
const originalText = submitButton.textContent;
|
||||
|
||||
// Show loading state
|
||||
submitButton.disabled = true;
|
||||
submitButton.classList.add('btn-loading');
|
||||
submitButton.textContent = '전송 중...';
|
||||
|
||||
try {
|
||||
const formData = new FormData(form);
|
||||
const data = Object.fromEntries(formData.entries());
|
||||
|
||||
const response = await fetch('/api/contact/submit', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
showNotification('메시지가 성공적으로 전송되었습니다! 곧 연락드리겠습니다.', 'success');
|
||||
form.reset();
|
||||
} else {
|
||||
showNotification('메시지 전송 중 오류가 발생했습니다. 다시 시도해주세요.', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Contact form error:', error);
|
||||
showNotification('메시지 전송 중 오류가 발생했습니다. 다시 시도해주세요.', 'error');
|
||||
} finally {
|
||||
// Reset button state
|
||||
submitButton.disabled = false;
|
||||
submitButton.classList.remove('btn-loading');
|
||||
submitButton.textContent = originalText;
|
||||
}
|
||||
}
|
||||
|
||||
// Notification System
|
||||
function showNotification(message, type = 'info') {
|
||||
const notification = document.createElement('div');
|
||||
notification.className = `notification notification-${type}`;
|
||||
notification.innerHTML = `
|
||||
<div class="notification-content">
|
||||
<span class="notification-message">${message}</span>
|
||||
<button class="notification-close" aria-label="Close notification">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Styles
|
||||
notification.style.cssText = `
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
z-index: 9999;
|
||||
max-width: 400px;
|
||||
padding: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
color: white;
|
||||
font-weight: 500;
|
||||
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
|
||||
transform: translateX(100%);
|
||||
transition: transform 0.3s ease;
|
||||
${type === 'success' ? 'background: #10b981;' : ''}
|
||||
${type === 'error' ? 'background: #ef4444;' : ''}
|
||||
${type === 'info' ? 'background: #3b82f6;' : ''}
|
||||
`;
|
||||
|
||||
document.body.appendChild(notification);
|
||||
|
||||
// Animate in
|
||||
setTimeout(() => {
|
||||
notification.style.transform = 'translateX(0)';
|
||||
}, 100);
|
||||
|
||||
// Close button handler
|
||||
const closeButton = notification.querySelector('.notification-close');
|
||||
closeButton.addEventListener('click', () => {
|
||||
closeNotification(notification);
|
||||
});
|
||||
|
||||
// Auto close after 5 seconds
|
||||
setTimeout(() => {
|
||||
closeNotification(notification);
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
function closeNotification(notification) {
|
||||
notification.style.transform = 'translateX(100%)';
|
||||
setTimeout(() => {
|
||||
if (notification.parentNode) {
|
||||
notification.parentNode.removeChild(notification);
|
||||
}
|
||||
}, 300);
|
||||
}
|
||||
|
||||
// Portfolio Filter (if on portfolio page)
|
||||
const portfolioFilters = document.querySelectorAll('.portfolio-filter');
|
||||
const portfolioItems = document.querySelectorAll('.portfolio-item');
|
||||
|
||||
portfolioFilters.forEach(filter => {
|
||||
filter.addEventListener('click', function() {
|
||||
const category = this.dataset.category;
|
||||
|
||||
// Update active filter
|
||||
portfolioFilters.forEach(f => f.classList.remove('active'));
|
||||
this.classList.add('active');
|
||||
|
||||
// Filter items
|
||||
portfolioItems.forEach(item => {
|
||||
if (category === 'all' || item.dataset.category === category) {
|
||||
item.style.display = 'block';
|
||||
item.style.animation = 'fadeIn 0.5s ease';
|
||||
} else {
|
||||
item.style.display = 'none';
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Image Lazy Loading
|
||||
const images = document.querySelectorAll('img[data-src]');
|
||||
const imageObserver = new IntersectionObserver((entries, observer) => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting) {
|
||||
const img = entry.target;
|
||||
img.src = img.dataset.src;
|
||||
img.classList.remove('lazy');
|
||||
imageObserver.unobserve(img);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
images.forEach(img => imageObserver.observe(img));
|
||||
|
||||
// Service Worker Registration for PWA
|
||||
if ('serviceWorker' in navigator && 'PushManager' in window) {
|
||||
navigator.serviceWorker.register('/sw.js')
|
||||
.then(registration => {
|
||||
console.log('Service Worker registered successfully:', registration);
|
||||
})
|
||||
.catch(error => {
|
||||
console.log('Service Worker registration failed:', error);
|
||||
});
|
||||
}
|
||||
|
||||
// Performance Monitoring
|
||||
if ('performance' in window) {
|
||||
window.addEventListener('load', () => {
|
||||
setTimeout(() => {
|
||||
const perfData = performance.getEntriesByType('navigation')[0];
|
||||
const loadTime = perfData.loadEventEnd - perfData.loadEventStart;
|
||||
|
||||
if (loadTime > 3000) {
|
||||
console.warn('Page load time is slow:', loadTime + 'ms');
|
||||
}
|
||||
}, 1000);
|
||||
});
|
||||
}
|
||||
|
||||
// Cookie Consent (if needed)
|
||||
function initCookieConsent() {
|
||||
const consent = localStorage.getItem('cookieConsent');
|
||||
if (!consent) {
|
||||
showCookieConsent();
|
||||
}
|
||||
}
|
||||
|
||||
function showCookieConsent() {
|
||||
const banner = document.createElement('div');
|
||||
banner.className = 'cookie-consent';
|
||||
banner.innerHTML = `
|
||||
<div class="cookie-content">
|
||||
<p>이 웹사이트는 더 나은 서비스 제공을 위해 쿠키를 사용합니다.</p>
|
||||
<div class="cookie-buttons">
|
||||
<button id="accept-cookies" class="btn btn-primary">동의</button>
|
||||
<button id="decline-cookies" class="btn btn-secondary">거부</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
banner.style.cssText = `
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: rgba(0, 0, 0, 0.9);
|
||||
color: white;
|
||||
padding: 1rem;
|
||||
z-index: 9999;
|
||||
transform: translateY(100%);
|
||||
transition: transform 0.3s ease;
|
||||
`;
|
||||
|
||||
document.body.appendChild(banner);
|
||||
|
||||
setTimeout(() => {
|
||||
banner.style.transform = 'translateY(0)';
|
||||
}, 100);
|
||||
|
||||
document.getElementById('accept-cookies').addEventListener('click', () => {
|
||||
localStorage.setItem('cookieConsent', 'accepted');
|
||||
banner.style.transform = 'translateY(100%)';
|
||||
setTimeout(() => banner.remove(), 300);
|
||||
});
|
||||
|
||||
document.getElementById('decline-cookies').addEventListener('click', () => {
|
||||
localStorage.setItem('cookieConsent', 'declined');
|
||||
banner.style.transform = 'translateY(100%)';
|
||||
setTimeout(() => banner.remove(), 300);
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize cookie consent
|
||||
// initCookieConsent();
|
||||
|
||||
// Parallax Effect
|
||||
const parallaxElements = document.querySelectorAll('.parallax');
|
||||
|
||||
function updateParallax() {
|
||||
const scrollY = window.pageYOffset;
|
||||
|
||||
parallaxElements.forEach(element => {
|
||||
const speed = element.dataset.speed || 0.5;
|
||||
const yPos = -(scrollY * speed);
|
||||
element.style.transform = `translateY(${yPos}px)`;
|
||||
});
|
||||
}
|
||||
|
||||
if (parallaxElements.length > 0) {
|
||||
window.addEventListener('scroll', updateParallax);
|
||||
}
|
||||
|
||||
// Counter Animation
|
||||
const counters = document.querySelectorAll('.counter');
|
||||
const counterObserver = new IntersectionObserver((entries) => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting) {
|
||||
animateCounter(entry.target);
|
||||
counterObserver.unobserve(entry.target);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
counters.forEach(counter => counterObserver.observe(counter));
|
||||
|
||||
function animateCounter(element) {
|
||||
const target = parseInt(element.dataset.count);
|
||||
const duration = 2000;
|
||||
const start = performance.now();
|
||||
|
||||
function updateCounter(currentTime) {
|
||||
const elapsed = currentTime - start;
|
||||
const progress = Math.min(elapsed / duration, 1);
|
||||
|
||||
const current = Math.floor(progress * target);
|
||||
element.textContent = current.toLocaleString();
|
||||
|
||||
if (progress < 1) {
|
||||
requestAnimationFrame(updateCounter);
|
||||
}
|
||||
}
|
||||
|
||||
requestAnimationFrame(updateCounter);
|
||||
}
|
||||
|
||||
// Typing Effect for Hero Text
|
||||
const typingElements = document.querySelectorAll('.typing-effect');
|
||||
|
||||
typingElements.forEach(element => {
|
||||
const text = element.textContent;
|
||||
element.textContent = '';
|
||||
element.style.borderRight = '2px solid';
|
||||
|
||||
let i = 0;
|
||||
const timer = setInterval(() => {
|
||||
element.textContent += text[i];
|
||||
i++;
|
||||
|
||||
if (i >= text.length) {
|
||||
clearInterval(timer);
|
||||
element.style.borderRight = 'none';
|
||||
}
|
||||
}, 100);
|
||||
});
|
||||
|
||||
// Handle form validation
|
||||
const forms = document.querySelectorAll('form[data-validate]');
|
||||
forms.forEach(form => {
|
||||
form.addEventListener('submit', function(e) {
|
||||
if (!validateForm(this)) {
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
|
||||
// Real-time validation
|
||||
const inputs = form.querySelectorAll('input, textarea');
|
||||
inputs.forEach(input => {
|
||||
input.addEventListener('blur', () => validateField(input));
|
||||
input.addEventListener('input', () => clearFieldError(input));
|
||||
});
|
||||
});
|
||||
|
||||
function validateForm(form) {
|
||||
let isValid = true;
|
||||
const inputs = form.querySelectorAll('input[required], textarea[required]');
|
||||
|
||||
inputs.forEach(input => {
|
||||
if (!validateField(input)) {
|
||||
isValid = false;
|
||||
}
|
||||
});
|
||||
|
||||
return isValid;
|
||||
}
|
||||
|
||||
function validateField(field) {
|
||||
const value = field.value.trim();
|
||||
const type = field.type;
|
||||
let isValid = true;
|
||||
let message = '';
|
||||
|
||||
// Required field check
|
||||
if (field.hasAttribute('required') && !value) {
|
||||
isValid = false;
|
||||
message = '이 필드는 필수입니다.';
|
||||
}
|
||||
|
||||
// Email validation
|
||||
if (type === 'email' && value) {
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (!emailRegex.test(value)) {
|
||||
isValid = false;
|
||||
message = '올바른 이메일 형식을 입력해주세요.';
|
||||
}
|
||||
}
|
||||
|
||||
// Phone validation
|
||||
if (type === 'tel' && value) {
|
||||
const phoneRegex = /^[0-9-+\s()]+$/;
|
||||
if (!phoneRegex.test(value)) {
|
||||
isValid = false;
|
||||
message = '올바른 전화번호 형식을 입력해주세요.';
|
||||
}
|
||||
}
|
||||
|
||||
// Show/hide error
|
||||
if (!isValid) {
|
||||
showFieldError(field, message);
|
||||
} else {
|
||||
clearFieldError(field);
|
||||
}
|
||||
|
||||
return isValid;
|
||||
}
|
||||
|
||||
function showFieldError(field, message) {
|
||||
clearFieldError(field);
|
||||
|
||||
field.classList.add('error');
|
||||
const errorDiv = document.createElement('div');
|
||||
errorDiv.className = 'field-error';
|
||||
errorDiv.textContent = message;
|
||||
errorDiv.style.cssText = 'color: #ef4444; font-size: 0.875rem; margin-top: 0.25rem;';
|
||||
|
||||
field.parentNode.appendChild(errorDiv);
|
||||
}
|
||||
|
||||
function clearFieldError(field) {
|
||||
field.classList.remove('error');
|
||||
const errorDiv = field.parentNode.querySelector('.field-error');
|
||||
if (errorDiv) {
|
||||
errorDiv.remove();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Utility Functions
|
||||
const utils = {
|
||||
// Debounce function
|
||||
debounce: function(func, wait) {
|
||||
let timeout;
|
||||
return function executedFunction(...args) {
|
||||
const later = () => {
|
||||
clearTimeout(timeout);
|
||||
func(...args);
|
||||
};
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(later, wait);
|
||||
};
|
||||
},
|
||||
|
||||
// Throttle function
|
||||
throttle: function(func, limit) {
|
||||
let inThrottle;
|
||||
return function() {
|
||||
const args = arguments;
|
||||
const context = this;
|
||||
if (!inThrottle) {
|
||||
func.apply(context, args);
|
||||
inThrottle = true;
|
||||
setTimeout(() => inThrottle = false, limit);
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
// Format currency
|
||||
formatCurrency: function(amount, currency = 'KRW') {
|
||||
return new Intl.NumberFormat('ko-KR', {
|
||||
style: 'currency',
|
||||
currency: currency,
|
||||
minimumFractionDigits: 0
|
||||
}).format(amount);
|
||||
},
|
||||
|
||||
// Format date
|
||||
formatDate: function(date, options = {}) {
|
||||
const defaultOptions = {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
};
|
||||
return new Intl.DateTimeFormat('ko-KR', { ...defaultOptions, ...options }).format(new Date(date));
|
||||
}
|
||||
};
|
||||
|
||||
// Global error handler
|
||||
window.addEventListener('error', function(e) {
|
||||
console.error('Global error:', e.error);
|
||||
// Could send error to analytics service
|
||||
});
|
||||
|
||||
// Unhandled promise rejection handler
|
||||
window.addEventListener('unhandledrejection', function(e) {
|
||||
console.error('Unhandled promise rejection:', e.reason);
|
||||
// Could send error to analytics service
|
||||
});
|
||||
|
||||
// Export for use in other modules
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = utils;
|
||||
}
|
||||
161
.history/public/manifest_20251019161703.json
Normal file
161
.history/public/manifest_20251019161703.json
Normal file
@@ -0,0 +1,161 @@
|
||||
{
|
||||
"name": "SmartSolTech - Technology Solutions",
|
||||
"short_name": "SmartSolTech",
|
||||
"description": "Professional web development, mobile apps, and digital solutions in Korea",
|
||||
"start_url": "/",
|
||||
"display": "standalone",
|
||||
"orientation": "portrait-primary",
|
||||
"theme_color": "#3b82f6",
|
||||
"background_color": "#ffffff",
|
||||
"lang": "ko",
|
||||
"scope": "/",
|
||||
"categories": ["business", "productivity", "technology"],
|
||||
"icons": [
|
||||
{
|
||||
"src": "/images/icon-72x72.png",
|
||||
"sizes": "72x72",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "/images/icon-96x96.png",
|
||||
"sizes": "96x96",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "/images/icon-128x128.png",
|
||||
"sizes": "128x128",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "/images/icon-144x144.png",
|
||||
"sizes": "144x144",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "/images/icon-152x152.png",
|
||||
"sizes": "152x152",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "/images/icon-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "/images/icon-384x384.png",
|
||||
"sizes": "384x384",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "/images/icon-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
}
|
||||
],
|
||||
"screenshots": [
|
||||
{
|
||||
"src": "/images/screenshot-desktop.png",
|
||||
"sizes": "1280x720",
|
||||
"type": "image/png",
|
||||
"form_factor": "wide",
|
||||
"label": "SmartSolTech Desktop View"
|
||||
},
|
||||
{
|
||||
"src": "/images/screenshot-mobile.png",
|
||||
"sizes": "375x812",
|
||||
"type": "image/png",
|
||||
"form_factor": "narrow",
|
||||
"label": "SmartSolTech Mobile View"
|
||||
}
|
||||
],
|
||||
"shortcuts": [
|
||||
{
|
||||
"name": "Portfolio",
|
||||
"short_name": "Portfolio",
|
||||
"description": "View our latest projects",
|
||||
"url": "/portfolio",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/images/shortcut-portfolio.png",
|
||||
"sizes": "96x96",
|
||||
"type": "image/png"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Calculator",
|
||||
"short_name": "Calculator",
|
||||
"description": "Calculate project costs",
|
||||
"url": "/calculator",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/images/shortcut-calculator.png",
|
||||
"sizes": "96x96",
|
||||
"type": "image/png"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Contact",
|
||||
"short_name": "Contact",
|
||||
"description": "Get in touch with us",
|
||||
"url": "/contact",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/images/shortcut-contact.png",
|
||||
"sizes": "96x96",
|
||||
"type": "image/png"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"related_applications": [
|
||||
{
|
||||
"platform": "webapp",
|
||||
"url": "https://smartsoltech.kr/manifest.json"
|
||||
}
|
||||
],
|
||||
"prefer_related_applications": false,
|
||||
"edge_side_panel": {
|
||||
"preferred_width": 400
|
||||
},
|
||||
"protocol_handlers": [
|
||||
{
|
||||
"protocol": "mailto",
|
||||
"url": "/contact?email=%s"
|
||||
}
|
||||
],
|
||||
"file_handlers": [
|
||||
{
|
||||
"action": "/open-file",
|
||||
"accept": {
|
||||
"image/*": [".jpg", ".jpeg", ".png", ".gif", ".webp"],
|
||||
"application/pdf": [".pdf"]
|
||||
}
|
||||
}
|
||||
],
|
||||
"share_target": {
|
||||
"action": "/share",
|
||||
"method": "POST",
|
||||
"enctype": "multipart/form-data",
|
||||
"params": {
|
||||
"title": "title",
|
||||
"text": "text",
|
||||
"url": "url",
|
||||
"files": [
|
||||
{
|
||||
"name": "images",
|
||||
"accept": ["image/*"]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
161
.history/public/manifest_20251019162545.json
Normal file
161
.history/public/manifest_20251019162545.json
Normal file
@@ -0,0 +1,161 @@
|
||||
{
|
||||
"name": "SmartSolTech - Technology Solutions",
|
||||
"short_name": "SmartSolTech",
|
||||
"description": "Professional web development, mobile apps, and digital solutions in Korea",
|
||||
"start_url": "/",
|
||||
"display": "standalone",
|
||||
"orientation": "portrait-primary",
|
||||
"theme_color": "#3b82f6",
|
||||
"background_color": "#ffffff",
|
||||
"lang": "ko",
|
||||
"scope": "/",
|
||||
"categories": ["business", "productivity", "technology"],
|
||||
"icons": [
|
||||
{
|
||||
"src": "/images/icon-72x72.png",
|
||||
"sizes": "72x72",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "/images/icon-96x96.png",
|
||||
"sizes": "96x96",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "/images/icon-128x128.png",
|
||||
"sizes": "128x128",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "/images/icon-144x144.png",
|
||||
"sizes": "144x144",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "/images/icon-152x152.png",
|
||||
"sizes": "152x152",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "/images/icon-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "/images/icon-384x384.png",
|
||||
"sizes": "384x384",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "/images/icon-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
}
|
||||
],
|
||||
"screenshots": [
|
||||
{
|
||||
"src": "/images/screenshot-desktop.png",
|
||||
"sizes": "1280x720",
|
||||
"type": "image/png",
|
||||
"form_factor": "wide",
|
||||
"label": "SmartSolTech Desktop View"
|
||||
},
|
||||
{
|
||||
"src": "/images/screenshot-mobile.png",
|
||||
"sizes": "375x812",
|
||||
"type": "image/png",
|
||||
"form_factor": "narrow",
|
||||
"label": "SmartSolTech Mobile View"
|
||||
}
|
||||
],
|
||||
"shortcuts": [
|
||||
{
|
||||
"name": "Portfolio",
|
||||
"short_name": "Portfolio",
|
||||
"description": "View our latest projects",
|
||||
"url": "/portfolio",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/images/shortcut-portfolio.png",
|
||||
"sizes": "96x96",
|
||||
"type": "image/png"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Calculator",
|
||||
"short_name": "Calculator",
|
||||
"description": "Calculate project costs",
|
||||
"url": "/calculator",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/images/shortcut-calculator.png",
|
||||
"sizes": "96x96",
|
||||
"type": "image/png"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Contact",
|
||||
"short_name": "Contact",
|
||||
"description": "Get in touch with us",
|
||||
"url": "/contact",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/images/shortcut-contact.png",
|
||||
"sizes": "96x96",
|
||||
"type": "image/png"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"related_applications": [
|
||||
{
|
||||
"platform": "webapp",
|
||||
"url": "https://smartsoltech.kr/manifest.json"
|
||||
}
|
||||
],
|
||||
"prefer_related_applications": false,
|
||||
"edge_side_panel": {
|
||||
"preferred_width": 400
|
||||
},
|
||||
"protocol_handlers": [
|
||||
{
|
||||
"protocol": "mailto",
|
||||
"url": "/contact?email=%s"
|
||||
}
|
||||
],
|
||||
"file_handlers": [
|
||||
{
|
||||
"action": "/open-file",
|
||||
"accept": {
|
||||
"image/*": [".jpg", ".jpeg", ".png", ".gif", ".webp"],
|
||||
"application/pdf": [".pdf"]
|
||||
}
|
||||
}
|
||||
],
|
||||
"share_target": {
|
||||
"action": "/share",
|
||||
"method": "POST",
|
||||
"enctype": "multipart/form-data",
|
||||
"params": {
|
||||
"title": "title",
|
||||
"text": "text",
|
||||
"url": "url",
|
||||
"files": [
|
||||
{
|
||||
"name": "images",
|
||||
"accept": ["image/*"]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
396
.history/public/sw_20251019161644.js
Normal file
396
.history/public/sw_20251019161644.js
Normal file
@@ -0,0 +1,396 @@
|
||||
// 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';
|
||||
|
||||
// Files to cache immediately
|
||||
const STATIC_FILES = [
|
||||
'/',
|
||||
'/css/main.css',
|
||||
'/js/main.js',
|
||||
'/images/logo.png',
|
||||
'/images/icon-192x192.png',
|
||||
'/images/icon-512x512.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'
|
||||
];
|
||||
|
||||
// Routes to cache dynamically
|
||||
const DYNAMIC_ROUTES = [
|
||||
'/about',
|
||||
'/services',
|
||||
'/portfolio',
|
||||
'/calculator',
|
||||
'/contact'
|
||||
];
|
||||
|
||||
// API endpoints to cache
|
||||
const API_CACHE_PATTERNS = [
|
||||
/^\/api\/portfolio/,
|
||||
/^\/api\/services/,
|
||||
/^\/api\/calculator\/services/
|
||||
];
|
||||
|
||||
// Install event - cache static files
|
||||
self.addEventListener('install', event => {
|
||||
console.log('Service Worker: Installing...');
|
||||
|
||||
event.waitUntil(
|
||||
caches.open(STATIC_CACHE_NAME)
|
||||
.then(cache => {
|
||||
console.log('Service Worker: Caching static files');
|
||||
return cache.addAll(STATIC_FILES);
|
||||
})
|
||||
.then(() => {
|
||||
console.log('Service Worker: Static files cached');
|
||||
return self.skipWaiting();
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Service Worker: Error caching static files', error);
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
// Activate event - clean up old caches
|
||||
self.addEventListener('activate', event => {
|
||||
console.log('Service Worker: Activating...');
|
||||
|
||||
event.waitUntil(
|
||||
caches.keys()
|
||||
.then(cacheNames => {
|
||||
return Promise.all(
|
||||
cacheNames.map(cacheName => {
|
||||
if (cacheName !== STATIC_CACHE_NAME &&
|
||||
cacheName !== DYNAMIC_CACHE_NAME) {
|
||||
console.log('Service Worker: Deleting old cache', cacheName);
|
||||
return caches.delete(cacheName);
|
||||
}
|
||||
})
|
||||
);
|
||||
})
|
||||
.then(() => {
|
||||
console.log('Service Worker: Activated');
|
||||
return self.clients.claim();
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
// Fetch event - serve cached files or fetch from network
|
||||
self.addEventListener('fetch', event => {
|
||||
const request = event.request;
|
||||
const url = new URL(request.url);
|
||||
|
||||
// Skip non-GET requests
|
||||
if (request.method !== 'GET') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip Chrome extension requests
|
||||
if (url.protocol === 'chrome-extension:') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle different types of requests
|
||||
if (isStaticFile(request.url)) {
|
||||
event.respondWith(cacheFirst(request));
|
||||
} else if (isAPIRequest(request.url)) {
|
||||
event.respondWith(networkFirst(request));
|
||||
} else if (isDynamicRoute(request.url)) {
|
||||
event.respondWith(staleWhileRevalidate(request));
|
||||
} else {
|
||||
event.respondWith(networkFirst(request));
|
||||
}
|
||||
});
|
||||
|
||||
// Cache strategies
|
||||
async function cacheFirst(request) {
|
||||
try {
|
||||
const cachedResponse = await caches.match(request);
|
||||
if (cachedResponse) {
|
||||
return cachedResponse;
|
||||
}
|
||||
|
||||
const networkResponse = await fetch(request);
|
||||
const cache = await caches.open(STATIC_CACHE_NAME);
|
||||
cache.put(request, networkResponse.clone());
|
||||
return networkResponse;
|
||||
} catch (error) {
|
||||
console.error('Cache first strategy failed:', error);
|
||||
return new Response('Offline', { status: 503 });
|
||||
}
|
||||
}
|
||||
|
||||
async function networkFirst(request) {
|
||||
try {
|
||||
const networkResponse = await fetch(request);
|
||||
|
||||
// Cache successful responses
|
||||
if (networkResponse.ok) {
|
||||
const cache = await caches.open(DYNAMIC_CACHE_NAME);
|
||||
cache.put(request, networkResponse.clone());
|
||||
}
|
||||
|
||||
return networkResponse;
|
||||
} catch (error) {
|
||||
console.log('Network first: Falling back to cache for', request.url);
|
||||
|
||||
const cachedResponse = await caches.match(request);
|
||||
if (cachedResponse) {
|
||||
return cachedResponse;
|
||||
}
|
||||
|
||||
// Return offline page for navigation requests
|
||||
if (request.mode === 'navigate') {
|
||||
return caches.match('/offline.html') || new Response('Offline', {
|
||||
status: 503,
|
||||
headers: { 'Content-Type': 'text/html' }
|
||||
});
|
||||
}
|
||||
|
||||
return new Response('Network Error', { status: 503 });
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
function isStaticFile(url) {
|
||||
return url.includes('/css/') ||
|
||||
url.includes('/js/') ||
|
||||
url.includes('/images/') ||
|
||||
url.includes('/fonts/') ||
|
||||
url.includes('googleapis.com') ||
|
||||
url.includes('cdnjs.cloudflare.com');
|
||||
}
|
||||
|
||||
function isAPIRequest(url) {
|
||||
return url.includes('/api/') ||
|
||||
API_CACHE_PATTERNS.some(pattern => pattern.test(url));
|
||||
}
|
||||
|
||||
function isDynamicRoute(url) {
|
||||
const pathname = new URL(url).pathname;
|
||||
return DYNAMIC_ROUTES.includes(pathname) ||
|
||||
pathname.startsWith('/portfolio/') ||
|
||||
pathname.startsWith('/services/');
|
||||
}
|
||||
|
||||
// Background sync for form submissions
|
||||
self.addEventListener('sync', event => {
|
||||
console.log('Service Worker: Background sync triggered', event.tag);
|
||||
|
||||
if (event.tag === 'contact-form-sync') {
|
||||
event.waitUntil(syncContactForms());
|
||||
}
|
||||
});
|
||||
|
||||
async function syncContactForms() {
|
||||
try {
|
||||
const cache = await caches.open(DYNAMIC_CACHE_NAME);
|
||||
const requests = await cache.keys();
|
||||
|
||||
const contactRequests = requests.filter(request =>
|
||||
request.url.includes('/api/contact/submit')
|
||||
);
|
||||
|
||||
for (const request of contactRequests) {
|
||||
try {
|
||||
await fetch(request);
|
||||
await cache.delete(request);
|
||||
console.log('Contact form synced successfully');
|
||||
} catch (error) {
|
||||
console.error('Failed to sync contact form:', error);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Background sync failed:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Push notification handling
|
||||
self.addEventListener('push', event => {
|
||||
console.log('Service Worker: Push received', event);
|
||||
|
||||
let data = {};
|
||||
if (event.data) {
|
||||
data = event.data.json();
|
||||
}
|
||||
|
||||
const title = data.title || 'SmartSolTech';
|
||||
const options = {
|
||||
body: data.body || 'You have a new notification',
|
||||
icon: '/images/icon-192x192.png',
|
||||
badge: '/images/icon-72x72.png',
|
||||
tag: data.tag || 'default',
|
||||
data: data.url || '/',
|
||||
actions: [
|
||||
{
|
||||
action: 'open',
|
||||
title: '열기',
|
||||
icon: '/images/icon-open.png'
|
||||
},
|
||||
{
|
||||
action: 'close',
|
||||
title: '닫기',
|
||||
icon: '/images/icon-close.png'
|
||||
}
|
||||
],
|
||||
requireInteraction: data.requireInteraction || false,
|
||||
silent: data.silent || false,
|
||||
vibrate: data.vibrate || [200, 100, 200]
|
||||
};
|
||||
|
||||
event.waitUntil(
|
||||
self.registration.showNotification(title, options)
|
||||
);
|
||||
});
|
||||
|
||||
// Notification click handling
|
||||
self.addEventListener('notificationclick', event => {
|
||||
console.log('Service Worker: Notification clicked', event);
|
||||
|
||||
event.notification.close();
|
||||
|
||||
if (event.action === 'close') {
|
||||
return;
|
||||
}
|
||||
|
||||
const url = event.notification.data || '/';
|
||||
|
||||
event.waitUntil(
|
||||
clients.matchAll({ type: 'window' }).then(clientList => {
|
||||
// Check if window is already open
|
||||
for (const client of clientList) {
|
||||
if (client.url === url && 'focus' in client) {
|
||||
return client.focus();
|
||||
}
|
||||
}
|
||||
|
||||
// Open new window
|
||||
if (clients.openWindow) {
|
||||
return clients.openWindow(url);
|
||||
}
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
// Handle messages from main thread
|
||||
self.addEventListener('message', event => {
|
||||
console.log('Service Worker: Message received', event.data);
|
||||
|
||||
if (event.data && event.data.type === 'SKIP_WAITING') {
|
||||
self.skipWaiting();
|
||||
}
|
||||
|
||||
if (event.data && event.data.type === 'CACHE_URLS') {
|
||||
cacheUrls(event.data.urls);
|
||||
}
|
||||
});
|
||||
|
||||
async function cacheUrls(urls) {
|
||||
try {
|
||||
const cache = await caches.open(DYNAMIC_CACHE_NAME);
|
||||
await cache.addAll(urls);
|
||||
console.log('URLs cached successfully:', urls);
|
||||
} catch (error) {
|
||||
console.error('Failed to cache URLs:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Periodic background sync (if supported)
|
||||
self.addEventListener('periodicsync', event => {
|
||||
console.log('Service Worker: Periodic sync triggered', event.tag);
|
||||
|
||||
if (event.tag === 'content-sync') {
|
||||
event.waitUntil(syncContent());
|
||||
}
|
||||
});
|
||||
|
||||
async function syncContent() {
|
||||
try {
|
||||
// Fetch fresh portfolio and services data
|
||||
const portfolioResponse = await fetch('/api/portfolio?featured=true');
|
||||
const servicesResponse = await fetch('/api/services?featured=true');
|
||||
|
||||
if (portfolioResponse.ok && servicesResponse.ok) {
|
||||
const cache = await caches.open(DYNAMIC_CACHE_NAME);
|
||||
cache.put('/api/portfolio?featured=true', portfolioResponse.clone());
|
||||
cache.put('/api/services?featured=true', servicesResponse.clone());
|
||||
console.log('Content synced successfully');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Content sync failed:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Cache management utilities
|
||||
async function cleanupCaches() {
|
||||
const cacheNames = await caches.keys();
|
||||
const currentCaches = [STATIC_CACHE_NAME, DYNAMIC_CACHE_NAME];
|
||||
|
||||
return Promise.all(
|
||||
cacheNames.map(cacheName => {
|
||||
if (!currentCaches.includes(cacheName)) {
|
||||
console.log('Deleting old cache:', cacheName);
|
||||
return caches.delete(cacheName);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// Limit cache size
|
||||
async function limitCacheSize(cacheName, maxItems) {
|
||||
const cache = await caches.open(cacheName);
|
||||
const keys = await cache.keys();
|
||||
|
||||
if (keys.length > maxItems) {
|
||||
const keysToDelete = keys.slice(0, keys.length - maxItems);
|
||||
return Promise.all(keysToDelete.map(key => cache.delete(key)));
|
||||
}
|
||||
}
|
||||
|
||||
// Performance monitoring
|
||||
self.addEventListener('fetch', event => {
|
||||
if (event.request.url.includes('/api/')) {
|
||||
const start = performance.now();
|
||||
|
||||
event.respondWith(
|
||||
fetch(event.request).then(response => {
|
||||
const duration = performance.now() - start;
|
||||
|
||||
// Log slow API requests
|
||||
if (duration > 2000) {
|
||||
console.warn('Slow API request:', event.request.url, duration + 'ms');
|
||||
}
|
||||
|
||||
return response;
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Error tracking
|
||||
self.addEventListener('error', event => {
|
||||
console.error('Service Worker error:', event.error);
|
||||
// Could send to analytics service
|
||||
});
|
||||
|
||||
self.addEventListener('unhandledrejection', event => {
|
||||
console.error('Service Worker unhandled rejection:', event.reason);
|
||||
// Could send to analytics service
|
||||
});
|
||||
396
.history/public/sw_20251019162545.js
Normal file
396
.history/public/sw_20251019162545.js
Normal file
@@ -0,0 +1,396 @@
|
||||
// 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';
|
||||
|
||||
// Files to cache immediately
|
||||
const STATIC_FILES = [
|
||||
'/',
|
||||
'/css/main.css',
|
||||
'/js/main.js',
|
||||
'/images/logo.png',
|
||||
'/images/icon-192x192.png',
|
||||
'/images/icon-512x512.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'
|
||||
];
|
||||
|
||||
// Routes to cache dynamically
|
||||
const DYNAMIC_ROUTES = [
|
||||
'/about',
|
||||
'/services',
|
||||
'/portfolio',
|
||||
'/calculator',
|
||||
'/contact'
|
||||
];
|
||||
|
||||
// API endpoints to cache
|
||||
const API_CACHE_PATTERNS = [
|
||||
/^\/api\/portfolio/,
|
||||
/^\/api\/services/,
|
||||
/^\/api\/calculator\/services/
|
||||
];
|
||||
|
||||
// Install event - cache static files
|
||||
self.addEventListener('install', event => {
|
||||
console.log('Service Worker: Installing...');
|
||||
|
||||
event.waitUntil(
|
||||
caches.open(STATIC_CACHE_NAME)
|
||||
.then(cache => {
|
||||
console.log('Service Worker: Caching static files');
|
||||
return cache.addAll(STATIC_FILES);
|
||||
})
|
||||
.then(() => {
|
||||
console.log('Service Worker: Static files cached');
|
||||
return self.skipWaiting();
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Service Worker: Error caching static files', error);
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
// Activate event - clean up old caches
|
||||
self.addEventListener('activate', event => {
|
||||
console.log('Service Worker: Activating...');
|
||||
|
||||
event.waitUntil(
|
||||
caches.keys()
|
||||
.then(cacheNames => {
|
||||
return Promise.all(
|
||||
cacheNames.map(cacheName => {
|
||||
if (cacheName !== STATIC_CACHE_NAME &&
|
||||
cacheName !== DYNAMIC_CACHE_NAME) {
|
||||
console.log('Service Worker: Deleting old cache', cacheName);
|
||||
return caches.delete(cacheName);
|
||||
}
|
||||
})
|
||||
);
|
||||
})
|
||||
.then(() => {
|
||||
console.log('Service Worker: Activated');
|
||||
return self.clients.claim();
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
// Fetch event - serve cached files or fetch from network
|
||||
self.addEventListener('fetch', event => {
|
||||
const request = event.request;
|
||||
const url = new URL(request.url);
|
||||
|
||||
// Skip non-GET requests
|
||||
if (request.method !== 'GET') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip Chrome extension requests
|
||||
if (url.protocol === 'chrome-extension:') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle different types of requests
|
||||
if (isStaticFile(request.url)) {
|
||||
event.respondWith(cacheFirst(request));
|
||||
} else if (isAPIRequest(request.url)) {
|
||||
event.respondWith(networkFirst(request));
|
||||
} else if (isDynamicRoute(request.url)) {
|
||||
event.respondWith(staleWhileRevalidate(request));
|
||||
} else {
|
||||
event.respondWith(networkFirst(request));
|
||||
}
|
||||
});
|
||||
|
||||
// Cache strategies
|
||||
async function cacheFirst(request) {
|
||||
try {
|
||||
const cachedResponse = await caches.match(request);
|
||||
if (cachedResponse) {
|
||||
return cachedResponse;
|
||||
}
|
||||
|
||||
const networkResponse = await fetch(request);
|
||||
const cache = await caches.open(STATIC_CACHE_NAME);
|
||||
cache.put(request, networkResponse.clone());
|
||||
return networkResponse;
|
||||
} catch (error) {
|
||||
console.error('Cache first strategy failed:', error);
|
||||
return new Response('Offline', { status: 503 });
|
||||
}
|
||||
}
|
||||
|
||||
async function networkFirst(request) {
|
||||
try {
|
||||
const networkResponse = await fetch(request);
|
||||
|
||||
// Cache successful responses
|
||||
if (networkResponse.ok) {
|
||||
const cache = await caches.open(DYNAMIC_CACHE_NAME);
|
||||
cache.put(request, networkResponse.clone());
|
||||
}
|
||||
|
||||
return networkResponse;
|
||||
} catch (error) {
|
||||
console.log('Network first: Falling back to cache for', request.url);
|
||||
|
||||
const cachedResponse = await caches.match(request);
|
||||
if (cachedResponse) {
|
||||
return cachedResponse;
|
||||
}
|
||||
|
||||
// Return offline page for navigation requests
|
||||
if (request.mode === 'navigate') {
|
||||
return caches.match('/offline.html') || new Response('Offline', {
|
||||
status: 503,
|
||||
headers: { 'Content-Type': 'text/html' }
|
||||
});
|
||||
}
|
||||
|
||||
return new Response('Network Error', { status: 503 });
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
function isStaticFile(url) {
|
||||
return url.includes('/css/') ||
|
||||
url.includes('/js/') ||
|
||||
url.includes('/images/') ||
|
||||
url.includes('/fonts/') ||
|
||||
url.includes('googleapis.com') ||
|
||||
url.includes('cdnjs.cloudflare.com');
|
||||
}
|
||||
|
||||
function isAPIRequest(url) {
|
||||
return url.includes('/api/') ||
|
||||
API_CACHE_PATTERNS.some(pattern => pattern.test(url));
|
||||
}
|
||||
|
||||
function isDynamicRoute(url) {
|
||||
const pathname = new URL(url).pathname;
|
||||
return DYNAMIC_ROUTES.includes(pathname) ||
|
||||
pathname.startsWith('/portfolio/') ||
|
||||
pathname.startsWith('/services/');
|
||||
}
|
||||
|
||||
// Background sync for form submissions
|
||||
self.addEventListener('sync', event => {
|
||||
console.log('Service Worker: Background sync triggered', event.tag);
|
||||
|
||||
if (event.tag === 'contact-form-sync') {
|
||||
event.waitUntil(syncContactForms());
|
||||
}
|
||||
});
|
||||
|
||||
async function syncContactForms() {
|
||||
try {
|
||||
const cache = await caches.open(DYNAMIC_CACHE_NAME);
|
||||
const requests = await cache.keys();
|
||||
|
||||
const contactRequests = requests.filter(request =>
|
||||
request.url.includes('/api/contact/submit')
|
||||
);
|
||||
|
||||
for (const request of contactRequests) {
|
||||
try {
|
||||
await fetch(request);
|
||||
await cache.delete(request);
|
||||
console.log('Contact form synced successfully');
|
||||
} catch (error) {
|
||||
console.error('Failed to sync contact form:', error);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Background sync failed:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Push notification handling
|
||||
self.addEventListener('push', event => {
|
||||
console.log('Service Worker: Push received', event);
|
||||
|
||||
let data = {};
|
||||
if (event.data) {
|
||||
data = event.data.json();
|
||||
}
|
||||
|
||||
const title = data.title || 'SmartSolTech';
|
||||
const options = {
|
||||
body: data.body || 'You have a new notification',
|
||||
icon: '/images/icon-192x192.png',
|
||||
badge: '/images/icon-72x72.png',
|
||||
tag: data.tag || 'default',
|
||||
data: data.url || '/',
|
||||
actions: [
|
||||
{
|
||||
action: 'open',
|
||||
title: '열기',
|
||||
icon: '/images/icon-open.png'
|
||||
},
|
||||
{
|
||||
action: 'close',
|
||||
title: '닫기',
|
||||
icon: '/images/icon-close.png'
|
||||
}
|
||||
],
|
||||
requireInteraction: data.requireInteraction || false,
|
||||
silent: data.silent || false,
|
||||
vibrate: data.vibrate || [200, 100, 200]
|
||||
};
|
||||
|
||||
event.waitUntil(
|
||||
self.registration.showNotification(title, options)
|
||||
);
|
||||
});
|
||||
|
||||
// Notification click handling
|
||||
self.addEventListener('notificationclick', event => {
|
||||
console.log('Service Worker: Notification clicked', event);
|
||||
|
||||
event.notification.close();
|
||||
|
||||
if (event.action === 'close') {
|
||||
return;
|
||||
}
|
||||
|
||||
const url = event.notification.data || '/';
|
||||
|
||||
event.waitUntil(
|
||||
clients.matchAll({ type: 'window' }).then(clientList => {
|
||||
// Check if window is already open
|
||||
for (const client of clientList) {
|
||||
if (client.url === url && 'focus' in client) {
|
||||
return client.focus();
|
||||
}
|
||||
}
|
||||
|
||||
// Open new window
|
||||
if (clients.openWindow) {
|
||||
return clients.openWindow(url);
|
||||
}
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
// Handle messages from main thread
|
||||
self.addEventListener('message', event => {
|
||||
console.log('Service Worker: Message received', event.data);
|
||||
|
||||
if (event.data && event.data.type === 'SKIP_WAITING') {
|
||||
self.skipWaiting();
|
||||
}
|
||||
|
||||
if (event.data && event.data.type === 'CACHE_URLS') {
|
||||
cacheUrls(event.data.urls);
|
||||
}
|
||||
});
|
||||
|
||||
async function cacheUrls(urls) {
|
||||
try {
|
||||
const cache = await caches.open(DYNAMIC_CACHE_NAME);
|
||||
await cache.addAll(urls);
|
||||
console.log('URLs cached successfully:', urls);
|
||||
} catch (error) {
|
||||
console.error('Failed to cache URLs:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Periodic background sync (if supported)
|
||||
self.addEventListener('periodicsync', event => {
|
||||
console.log('Service Worker: Periodic sync triggered', event.tag);
|
||||
|
||||
if (event.tag === 'content-sync') {
|
||||
event.waitUntil(syncContent());
|
||||
}
|
||||
});
|
||||
|
||||
async function syncContent() {
|
||||
try {
|
||||
// Fetch fresh portfolio and services data
|
||||
const portfolioResponse = await fetch('/api/portfolio?featured=true');
|
||||
const servicesResponse = await fetch('/api/services?featured=true');
|
||||
|
||||
if (portfolioResponse.ok && servicesResponse.ok) {
|
||||
const cache = await caches.open(DYNAMIC_CACHE_NAME);
|
||||
cache.put('/api/portfolio?featured=true', portfolioResponse.clone());
|
||||
cache.put('/api/services?featured=true', servicesResponse.clone());
|
||||
console.log('Content synced successfully');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Content sync failed:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Cache management utilities
|
||||
async function cleanupCaches() {
|
||||
const cacheNames = await caches.keys();
|
||||
const currentCaches = [STATIC_CACHE_NAME, DYNAMIC_CACHE_NAME];
|
||||
|
||||
return Promise.all(
|
||||
cacheNames.map(cacheName => {
|
||||
if (!currentCaches.includes(cacheName)) {
|
||||
console.log('Deleting old cache:', cacheName);
|
||||
return caches.delete(cacheName);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// Limit cache size
|
||||
async function limitCacheSize(cacheName, maxItems) {
|
||||
const cache = await caches.open(cacheName);
|
||||
const keys = await cache.keys();
|
||||
|
||||
if (keys.length > maxItems) {
|
||||
const keysToDelete = keys.slice(0, keys.length - maxItems);
|
||||
return Promise.all(keysToDelete.map(key => cache.delete(key)));
|
||||
}
|
||||
}
|
||||
|
||||
// Performance monitoring
|
||||
self.addEventListener('fetch', event => {
|
||||
if (event.request.url.includes('/api/')) {
|
||||
const start = performance.now();
|
||||
|
||||
event.respondWith(
|
||||
fetch(event.request).then(response => {
|
||||
const duration = performance.now() - start;
|
||||
|
||||
// Log slow API requests
|
||||
if (duration > 2000) {
|
||||
console.warn('Slow API request:', event.request.url, duration + 'ms');
|
||||
}
|
||||
|
||||
return response;
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Error tracking
|
||||
self.addEventListener('error', event => {
|
||||
console.error('Service Worker error:', event.error);
|
||||
// Could send to analytics service
|
||||
});
|
||||
|
||||
self.addEventListener('unhandledrejection', event => {
|
||||
console.error('Service Worker unhandled rejection:', event.reason);
|
||||
// Could send to analytics service
|
||||
});
|
||||
387
.history/routes/admin_20251019160958.js
Normal file
387
.history/routes/admin_20251019160958.js
Normal file
@@ -0,0 +1,387 @@
|
||||
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');
|
||||
|
||||
// Authentication middleware
|
||||
const requireAuth = (req, res, next) => {
|
||||
if (!req.session.user) {
|
||||
return res.redirect('/admin/login');
|
||||
}
|
||||
next();
|
||||
};
|
||||
|
||||
// Admin login page
|
||||
router.get('/login', (req, res) => {
|
||||
if (req.session.user) {
|
||||
return res.redirect('/admin/dashboard');
|
||||
}
|
||||
|
||||
res.render('admin/login', {
|
||||
title: 'Admin Login - SmartSolTech',
|
||||
layout: 'admin/layout',
|
||||
error: null
|
||||
});
|
||||
});
|
||||
|
||||
// Admin login POST
|
||||
router.post('/login', async (req, res) => {
|
||||
try {
|
||||
const { email, password } = req.body;
|
||||
|
||||
const user = await User.findOne({ 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'
|
||||
});
|
||||
}
|
||||
|
||||
await user.updateLastLogin();
|
||||
|
||||
req.session.user = {
|
||||
id: user._id,
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
role: user.role
|
||||
};
|
||||
|
||||
res.redirect('/admin/dashboard');
|
||||
} 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.'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Admin logout
|
||||
router.post('/logout', (req, res) => {
|
||||
req.session.destroy(err => {
|
||||
if (err) {
|
||||
console.error('Logout error:', err);
|
||||
}
|
||||
res.redirect('/admin/login');
|
||||
});
|
||||
});
|
||||
|
||||
// Dashboard
|
||||
router.get('/dashboard', requireAuth, async (req, res) => {
|
||||
try {
|
||||
const [
|
||||
portfolioCount,
|
||||
servicesCount,
|
||||
contactsCount,
|
||||
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)
|
||||
]);
|
||||
|
||||
const stats = {
|
||||
portfolio: portfolioCount,
|
||||
services: servicesCount,
|
||||
contacts: contactsCount,
|
||||
unreadContacts: await Contact.countDocuments({ isRead: false })
|
||||
};
|
||||
|
||||
res.render('admin/dashboard', {
|
||||
title: 'Dashboard - Admin Panel',
|
||||
layout: 'admin/layout',
|
||||
user: req.session.user,
|
||||
stats,
|
||||
recentContacts,
|
||||
recentPortfolio
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Dashboard error:', error);
|
||||
res.status(500).render('admin/error', {
|
||||
title: 'Error - Admin Panel',
|
||||
layout: 'admin/layout',
|
||||
message: 'Error loading dashboard'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Portfolio management
|
||||
router.get('/portfolio', requireAuth, async (req, res) => {
|
||||
try {
|
||||
const page = parseInt(req.query.page) || 1;
|
||||
const limit = 20;
|
||||
const skip = (page - 1) * limit;
|
||||
|
||||
const [portfolio, total] = await Promise.all([
|
||||
Portfolio.find()
|
||||
.sort({ createdAt: -1 })
|
||||
.skip(skip)
|
||||
.limit(limit),
|
||||
Portfolio.countDocuments()
|
||||
]);
|
||||
|
||||
const totalPages = Math.ceil(total / limit);
|
||||
|
||||
res.render('admin/portfolio/list', {
|
||||
title: 'Portfolio Management - Admin Panel',
|
||||
layout: 'admin/layout',
|
||||
user: req.session.user,
|
||||
portfolio,
|
||||
pagination: {
|
||||
current: page,
|
||||
total: totalPages,
|
||||
hasNext: page < totalPages,
|
||||
hasPrev: page > 1
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Portfolio list error:', error);
|
||||
res.status(500).render('admin/error', {
|
||||
title: 'Error - Admin Panel',
|
||||
layout: 'admin/layout',
|
||||
message: 'Error loading portfolio'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 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
|
||||
});
|
||||
});
|
||||
|
||||
// Edit portfolio item
|
||||
router.get('/portfolio/edit/:id', requireAuth, async (req, res) => {
|
||||
try {
|
||||
const portfolio = await Portfolio.findById(req.params.id);
|
||||
|
||||
if (!portfolio) {
|
||||
return res.status(404).render('admin/error', {
|
||||
title: 'Error - Admin Panel',
|
||||
layout: 'admin/layout',
|
||||
message: 'Portfolio item not found'
|
||||
});
|
||||
}
|
||||
|
||||
res.render('admin/portfolio/edit', {
|
||||
title: 'Edit Portfolio Item - Admin Panel',
|
||||
layout: 'admin/layout',
|
||||
user: req.session.user,
|
||||
portfolio
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Portfolio edit error:', error);
|
||||
res.status(500).render('admin/error', {
|
||||
title: 'Error - Admin Panel',
|
||||
layout: 'admin/layout',
|
||||
message: 'Error loading portfolio item'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Services management
|
||||
router.get('/services', requireAuth, async (req, res) => {
|
||||
try {
|
||||
const page = parseInt(req.query.page) || 1;
|
||||
const limit = 20;
|
||||
const skip = (page - 1) * limit;
|
||||
|
||||
const [services, total] = await Promise.all([
|
||||
Service.find()
|
||||
.sort({ createdAt: -1 })
|
||||
.skip(skip)
|
||||
.limit(limit),
|
||||
Service.countDocuments()
|
||||
]);
|
||||
|
||||
const totalPages = Math.ceil(total / limit);
|
||||
|
||||
res.render('admin/services/list', {
|
||||
title: 'Services Management - Admin Panel',
|
||||
layout: 'admin/layout',
|
||||
user: req.session.user,
|
||||
services,
|
||||
pagination: {
|
||||
current: page,
|
||||
total: totalPages,
|
||||
hasNext: page < totalPages,
|
||||
hasPrev: page > 1
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Services list error:', error);
|
||||
res.status(500).render('admin/error', {
|
||||
title: 'Error - Admin Panel',
|
||||
layout: 'admin/layout',
|
||||
message: 'Error loading services'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Add service
|
||||
router.get('/services/add', requireAuth, (req, res) => {
|
||||
res.render('admin/services/add', {
|
||||
title: 'Add Service - Admin Panel',
|
||||
layout: 'admin/layout',
|
||||
user: req.session.user
|
||||
});
|
||||
});
|
||||
|
||||
// Edit service
|
||||
router.get('/services/edit/:id', requireAuth, async (req, res) => {
|
||||
try {
|
||||
const service = await Service.findById(req.params.id)
|
||||
.populate('portfolio', 'title');
|
||||
|
||||
if (!service) {
|
||||
return res.status(404).render('admin/error', {
|
||||
title: 'Error - Admin Panel',
|
||||
layout: 'admin/layout',
|
||||
message: 'Service not found'
|
||||
});
|
||||
}
|
||||
|
||||
const availablePortfolio = await Portfolio.find({ isPublished: true })
|
||||
.select('title category');
|
||||
|
||||
res.render('admin/services/edit', {
|
||||
title: 'Edit Service - Admin Panel',
|
||||
layout: 'admin/layout',
|
||||
user: req.session.user,
|
||||
service,
|
||||
availablePortfolio
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Service edit error:', error);
|
||||
res.status(500).render('admin/error', {
|
||||
title: 'Error - Admin Panel',
|
||||
layout: 'admin/layout',
|
||||
message: 'Error loading service'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Contacts management
|
||||
router.get('/contacts', requireAuth, async (req, res) => {
|
||||
try {
|
||||
const page = parseInt(req.query.page) || 1;
|
||||
const limit = 20;
|
||||
const skip = (page - 1) * limit;
|
||||
const status = req.query.status;
|
||||
|
||||
let query = {};
|
||||
if (status && status !== 'all') {
|
||||
query.status = status;
|
||||
}
|
||||
|
||||
const [contacts, total] = await Promise.all([
|
||||
Contact.find(query)
|
||||
.sort({ createdAt: -1 })
|
||||
.skip(skip)
|
||||
.limit(limit),
|
||||
Contact.countDocuments(query)
|
||||
]);
|
||||
|
||||
const totalPages = Math.ceil(total / limit);
|
||||
|
||||
res.render('admin/contacts/list', {
|
||||
title: 'Contacts Management - Admin Panel',
|
||||
layout: 'admin/layout',
|
||||
user: req.session.user,
|
||||
contacts,
|
||||
currentStatus: status || 'all',
|
||||
pagination: {
|
||||
current: page,
|
||||
total: totalPages,
|
||||
hasNext: page < totalPages,
|
||||
hasPrev: page > 1
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Contacts list error:', error);
|
||||
res.status(500).render('admin/error', {
|
||||
title: 'Error - Admin Panel',
|
||||
layout: 'admin/layout',
|
||||
message: 'Error loading contacts'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// View contact details
|
||||
router.get('/contacts/:id', requireAuth, async (req, res) => {
|
||||
try {
|
||||
const contact = await Contact.findById(req.params.id);
|
||||
|
||||
if (!contact) {
|
||||
return res.status(404).render('admin/error', {
|
||||
title: 'Error - Admin Panel',
|
||||
layout: 'admin/layout',
|
||||
message: 'Contact not found'
|
||||
});
|
||||
}
|
||||
|
||||
// Mark as read
|
||||
if (!contact.isRead) {
|
||||
contact.isRead = true;
|
||||
await contact.save();
|
||||
}
|
||||
|
||||
res.render('admin/contacts/view', {
|
||||
title: 'Contact Details - Admin Panel',
|
||||
layout: 'admin/layout',
|
||||
user: req.session.user,
|
||||
contact
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Contact view error:', error);
|
||||
res.status(500).render('admin/error', {
|
||||
title: 'Error - Admin Panel',
|
||||
layout: 'admin/layout',
|
||||
message: 'Error loading contact'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Settings
|
||||
router.get('/settings', requireAuth, async (req, res) => {
|
||||
try {
|
||||
const settings = await SiteSettings.findOne() || new SiteSettings();
|
||||
|
||||
res.render('admin/settings', {
|
||||
title: 'Site Settings - Admin Panel',
|
||||
layout: 'admin/layout',
|
||||
user: req.session.user,
|
||||
settings
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Settings error:', error);
|
||||
res.status(500).render('admin/error', {
|
||||
title: 'Error - Admin Panel',
|
||||
layout: 'admin/layout',
|
||||
message: 'Error loading settings'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Media gallery
|
||||
router.get('/media', requireAuth, (req, res) => {
|
||||
res.render('admin/media', {
|
||||
title: 'Media Gallery - Admin Panel',
|
||||
layout: 'admin/layout',
|
||||
user: req.session.user
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
387
.history/routes/admin_20251019162544.js
Normal file
387
.history/routes/admin_20251019162544.js
Normal file
@@ -0,0 +1,387 @@
|
||||
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');
|
||||
|
||||
// Authentication middleware
|
||||
const requireAuth = (req, res, next) => {
|
||||
if (!req.session.user) {
|
||||
return res.redirect('/admin/login');
|
||||
}
|
||||
next();
|
||||
};
|
||||
|
||||
// Admin login page
|
||||
router.get('/login', (req, res) => {
|
||||
if (req.session.user) {
|
||||
return res.redirect('/admin/dashboard');
|
||||
}
|
||||
|
||||
res.render('admin/login', {
|
||||
title: 'Admin Login - SmartSolTech',
|
||||
layout: 'admin/layout',
|
||||
error: null
|
||||
});
|
||||
});
|
||||
|
||||
// Admin login POST
|
||||
router.post('/login', async (req, res) => {
|
||||
try {
|
||||
const { email, password } = req.body;
|
||||
|
||||
const user = await User.findOne({ 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'
|
||||
});
|
||||
}
|
||||
|
||||
await user.updateLastLogin();
|
||||
|
||||
req.session.user = {
|
||||
id: user._id,
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
role: user.role
|
||||
};
|
||||
|
||||
res.redirect('/admin/dashboard');
|
||||
} 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.'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Admin logout
|
||||
router.post('/logout', (req, res) => {
|
||||
req.session.destroy(err => {
|
||||
if (err) {
|
||||
console.error('Logout error:', err);
|
||||
}
|
||||
res.redirect('/admin/login');
|
||||
});
|
||||
});
|
||||
|
||||
// Dashboard
|
||||
router.get('/dashboard', requireAuth, async (req, res) => {
|
||||
try {
|
||||
const [
|
||||
portfolioCount,
|
||||
servicesCount,
|
||||
contactsCount,
|
||||
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)
|
||||
]);
|
||||
|
||||
const stats = {
|
||||
portfolio: portfolioCount,
|
||||
services: servicesCount,
|
||||
contacts: contactsCount,
|
||||
unreadContacts: await Contact.countDocuments({ isRead: false })
|
||||
};
|
||||
|
||||
res.render('admin/dashboard', {
|
||||
title: 'Dashboard - Admin Panel',
|
||||
layout: 'admin/layout',
|
||||
user: req.session.user,
|
||||
stats,
|
||||
recentContacts,
|
||||
recentPortfolio
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Dashboard error:', error);
|
||||
res.status(500).render('admin/error', {
|
||||
title: 'Error - Admin Panel',
|
||||
layout: 'admin/layout',
|
||||
message: 'Error loading dashboard'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Portfolio management
|
||||
router.get('/portfolio', requireAuth, async (req, res) => {
|
||||
try {
|
||||
const page = parseInt(req.query.page) || 1;
|
||||
const limit = 20;
|
||||
const skip = (page - 1) * limit;
|
||||
|
||||
const [portfolio, total] = await Promise.all([
|
||||
Portfolio.find()
|
||||
.sort({ createdAt: -1 })
|
||||
.skip(skip)
|
||||
.limit(limit),
|
||||
Portfolio.countDocuments()
|
||||
]);
|
||||
|
||||
const totalPages = Math.ceil(total / limit);
|
||||
|
||||
res.render('admin/portfolio/list', {
|
||||
title: 'Portfolio Management - Admin Panel',
|
||||
layout: 'admin/layout',
|
||||
user: req.session.user,
|
||||
portfolio,
|
||||
pagination: {
|
||||
current: page,
|
||||
total: totalPages,
|
||||
hasNext: page < totalPages,
|
||||
hasPrev: page > 1
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Portfolio list error:', error);
|
||||
res.status(500).render('admin/error', {
|
||||
title: 'Error - Admin Panel',
|
||||
layout: 'admin/layout',
|
||||
message: 'Error loading portfolio'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 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
|
||||
});
|
||||
});
|
||||
|
||||
// Edit portfolio item
|
||||
router.get('/portfolio/edit/:id', requireAuth, async (req, res) => {
|
||||
try {
|
||||
const portfolio = await Portfolio.findById(req.params.id);
|
||||
|
||||
if (!portfolio) {
|
||||
return res.status(404).render('admin/error', {
|
||||
title: 'Error - Admin Panel',
|
||||
layout: 'admin/layout',
|
||||
message: 'Portfolio item not found'
|
||||
});
|
||||
}
|
||||
|
||||
res.render('admin/portfolio/edit', {
|
||||
title: 'Edit Portfolio Item - Admin Panel',
|
||||
layout: 'admin/layout',
|
||||
user: req.session.user,
|
||||
portfolio
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Portfolio edit error:', error);
|
||||
res.status(500).render('admin/error', {
|
||||
title: 'Error - Admin Panel',
|
||||
layout: 'admin/layout',
|
||||
message: 'Error loading portfolio item'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Services management
|
||||
router.get('/services', requireAuth, async (req, res) => {
|
||||
try {
|
||||
const page = parseInt(req.query.page) || 1;
|
||||
const limit = 20;
|
||||
const skip = (page - 1) * limit;
|
||||
|
||||
const [services, total] = await Promise.all([
|
||||
Service.find()
|
||||
.sort({ createdAt: -1 })
|
||||
.skip(skip)
|
||||
.limit(limit),
|
||||
Service.countDocuments()
|
||||
]);
|
||||
|
||||
const totalPages = Math.ceil(total / limit);
|
||||
|
||||
res.render('admin/services/list', {
|
||||
title: 'Services Management - Admin Panel',
|
||||
layout: 'admin/layout',
|
||||
user: req.session.user,
|
||||
services,
|
||||
pagination: {
|
||||
current: page,
|
||||
total: totalPages,
|
||||
hasNext: page < totalPages,
|
||||
hasPrev: page > 1
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Services list error:', error);
|
||||
res.status(500).render('admin/error', {
|
||||
title: 'Error - Admin Panel',
|
||||
layout: 'admin/layout',
|
||||
message: 'Error loading services'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Add service
|
||||
router.get('/services/add', requireAuth, (req, res) => {
|
||||
res.render('admin/services/add', {
|
||||
title: 'Add Service - Admin Panel',
|
||||
layout: 'admin/layout',
|
||||
user: req.session.user
|
||||
});
|
||||
});
|
||||
|
||||
// Edit service
|
||||
router.get('/services/edit/:id', requireAuth, async (req, res) => {
|
||||
try {
|
||||
const service = await Service.findById(req.params.id)
|
||||
.populate('portfolio', 'title');
|
||||
|
||||
if (!service) {
|
||||
return res.status(404).render('admin/error', {
|
||||
title: 'Error - Admin Panel',
|
||||
layout: 'admin/layout',
|
||||
message: 'Service not found'
|
||||
});
|
||||
}
|
||||
|
||||
const availablePortfolio = await Portfolio.find({ isPublished: true })
|
||||
.select('title category');
|
||||
|
||||
res.render('admin/services/edit', {
|
||||
title: 'Edit Service - Admin Panel',
|
||||
layout: 'admin/layout',
|
||||
user: req.session.user,
|
||||
service,
|
||||
availablePortfolio
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Service edit error:', error);
|
||||
res.status(500).render('admin/error', {
|
||||
title: 'Error - Admin Panel',
|
||||
layout: 'admin/layout',
|
||||
message: 'Error loading service'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Contacts management
|
||||
router.get('/contacts', requireAuth, async (req, res) => {
|
||||
try {
|
||||
const page = parseInt(req.query.page) || 1;
|
||||
const limit = 20;
|
||||
const skip = (page - 1) * limit;
|
||||
const status = req.query.status;
|
||||
|
||||
let query = {};
|
||||
if (status && status !== 'all') {
|
||||
query.status = status;
|
||||
}
|
||||
|
||||
const [contacts, total] = await Promise.all([
|
||||
Contact.find(query)
|
||||
.sort({ createdAt: -1 })
|
||||
.skip(skip)
|
||||
.limit(limit),
|
||||
Contact.countDocuments(query)
|
||||
]);
|
||||
|
||||
const totalPages = Math.ceil(total / limit);
|
||||
|
||||
res.render('admin/contacts/list', {
|
||||
title: 'Contacts Management - Admin Panel',
|
||||
layout: 'admin/layout',
|
||||
user: req.session.user,
|
||||
contacts,
|
||||
currentStatus: status || 'all',
|
||||
pagination: {
|
||||
current: page,
|
||||
total: totalPages,
|
||||
hasNext: page < totalPages,
|
||||
hasPrev: page > 1
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Contacts list error:', error);
|
||||
res.status(500).render('admin/error', {
|
||||
title: 'Error - Admin Panel',
|
||||
layout: 'admin/layout',
|
||||
message: 'Error loading contacts'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// View contact details
|
||||
router.get('/contacts/:id', requireAuth, async (req, res) => {
|
||||
try {
|
||||
const contact = await Contact.findById(req.params.id);
|
||||
|
||||
if (!contact) {
|
||||
return res.status(404).render('admin/error', {
|
||||
title: 'Error - Admin Panel',
|
||||
layout: 'admin/layout',
|
||||
message: 'Contact not found'
|
||||
});
|
||||
}
|
||||
|
||||
// Mark as read
|
||||
if (!contact.isRead) {
|
||||
contact.isRead = true;
|
||||
await contact.save();
|
||||
}
|
||||
|
||||
res.render('admin/contacts/view', {
|
||||
title: 'Contact Details - Admin Panel',
|
||||
layout: 'admin/layout',
|
||||
user: req.session.user,
|
||||
contact
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Contact view error:', error);
|
||||
res.status(500).render('admin/error', {
|
||||
title: 'Error - Admin Panel',
|
||||
layout: 'admin/layout',
|
||||
message: 'Error loading contact'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Settings
|
||||
router.get('/settings', requireAuth, async (req, res) => {
|
||||
try {
|
||||
const settings = await SiteSettings.findOne() || new SiteSettings();
|
||||
|
||||
res.render('admin/settings', {
|
||||
title: 'Site Settings - Admin Panel',
|
||||
layout: 'admin/layout',
|
||||
user: req.session.user,
|
||||
settings
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Settings error:', error);
|
||||
res.status(500).render('admin/error', {
|
||||
title: 'Error - Admin Panel',
|
||||
layout: 'admin/layout',
|
||||
message: 'Error loading settings'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Media gallery
|
||||
router.get('/media', requireAuth, (req, res) => {
|
||||
res.render('admin/media', {
|
||||
title: 'Media Gallery - Admin Panel',
|
||||
layout: 'admin/layout',
|
||||
user: req.session.user
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
201
.history/routes/auth_20251019160707.js
Normal file
201
.history/routes/auth_20251019160707.js
Normal file
@@ -0,0 +1,201 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const jwt = require('jsonwebtoken');
|
||||
const { body, validationResult } = require('express-validator');
|
||||
const User = require('../models/User');
|
||||
|
||||
// Login validation rules
|
||||
const loginValidation = [
|
||||
body('email').isEmail().normalizeEmail(),
|
||||
body('password').isLength({ min: 6 })
|
||||
];
|
||||
|
||||
// Login
|
||||
router.post('/login', loginValidation, async (req, res) => {
|
||||
try {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Invalid input data',
|
||||
errors: errors.array()
|
||||
});
|
||||
}
|
||||
|
||||
const { email, password } = req.body;
|
||||
|
||||
// Find user
|
||||
const user = await User.findOne({ email, isActive: true });
|
||||
if (!user) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: 'Invalid credentials'
|
||||
});
|
||||
}
|
||||
|
||||
// Check password
|
||||
const isValidPassword = await user.comparePassword(password);
|
||||
if (!isValidPassword) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: 'Invalid credentials'
|
||||
});
|
||||
}
|
||||
|
||||
// Update last login
|
||||
await user.updateLastLogin();
|
||||
|
||||
// Create JWT token
|
||||
const token = jwt.sign(
|
||||
{ userId: user._id, email: user.email, role: user.role },
|
||||
process.env.JWT_SECRET,
|
||||
{ expiresIn: '7d' }
|
||||
);
|
||||
|
||||
// Set session
|
||||
req.session.user = {
|
||||
id: user._id,
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
role: user.role
|
||||
};
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Login successful',
|
||||
token,
|
||||
user: {
|
||||
id: user._id,
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
role: user.role,
|
||||
avatar: user.avatar
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Login error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Server error'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Logout
|
||||
router.post('/logout', (req, res) => {
|
||||
req.session.destroy(err => {
|
||||
if (err) {
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: 'Could not log out'
|
||||
});
|
||||
}
|
||||
|
||||
res.clearCookie('connect.sid');
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Logout successful'
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Check authentication status
|
||||
router.get('/me', async (req, res) => {
|
||||
try {
|
||||
if (!req.session.user) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: 'Not authenticated'
|
||||
});
|
||||
}
|
||||
|
||||
const user = await User.findById(req.session.user.id)
|
||||
.select('-password');
|
||||
|
||||
if (!user || !user.isActive) {
|
||||
req.session.destroy();
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: 'User not found or inactive'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
user: {
|
||||
id: user._id,
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
role: user.role,
|
||||
avatar: user.avatar,
|
||||
lastLogin: user.lastLogin
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Auth check error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Server error'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Change password
|
||||
router.put('/change-password', [
|
||||
body('currentPassword').isLength({ min: 6 }),
|
||||
body('newPassword').isLength({ min: 6 })
|
||||
], async (req, res) => {
|
||||
try {
|
||||
if (!req.session.user) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: 'Not authenticated'
|
||||
});
|
||||
}
|
||||
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Invalid input data',
|
||||
errors: errors.array()
|
||||
});
|
||||
}
|
||||
|
||||
const { currentPassword, newPassword } = req.body;
|
||||
const user = await User.findById(req.session.user.id);
|
||||
|
||||
if (!user) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'User not found'
|
||||
});
|
||||
}
|
||||
|
||||
// Verify current password
|
||||
const isValidPassword = await user.comparePassword(currentPassword);
|
||||
if (!isValidPassword) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Current password is incorrect'
|
||||
});
|
||||
}
|
||||
|
||||
// Update password
|
||||
user.password = newPassword;
|
||||
await user.save();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Password updated successfully'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Change password error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Server error'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
201
.history/routes/auth_20251019162544.js
Normal file
201
.history/routes/auth_20251019162544.js
Normal file
@@ -0,0 +1,201 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const jwt = require('jsonwebtoken');
|
||||
const { body, validationResult } = require('express-validator');
|
||||
const User = require('../models/User');
|
||||
|
||||
// Login validation rules
|
||||
const loginValidation = [
|
||||
body('email').isEmail().normalizeEmail(),
|
||||
body('password').isLength({ min: 6 })
|
||||
];
|
||||
|
||||
// Login
|
||||
router.post('/login', loginValidation, async (req, res) => {
|
||||
try {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Invalid input data',
|
||||
errors: errors.array()
|
||||
});
|
||||
}
|
||||
|
||||
const { email, password } = req.body;
|
||||
|
||||
// Find user
|
||||
const user = await User.findOne({ email, isActive: true });
|
||||
if (!user) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: 'Invalid credentials'
|
||||
});
|
||||
}
|
||||
|
||||
// Check password
|
||||
const isValidPassword = await user.comparePassword(password);
|
||||
if (!isValidPassword) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: 'Invalid credentials'
|
||||
});
|
||||
}
|
||||
|
||||
// Update last login
|
||||
await user.updateLastLogin();
|
||||
|
||||
// Create JWT token
|
||||
const token = jwt.sign(
|
||||
{ userId: user._id, email: user.email, role: user.role },
|
||||
process.env.JWT_SECRET,
|
||||
{ expiresIn: '7d' }
|
||||
);
|
||||
|
||||
// Set session
|
||||
req.session.user = {
|
||||
id: user._id,
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
role: user.role
|
||||
};
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Login successful',
|
||||
token,
|
||||
user: {
|
||||
id: user._id,
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
role: user.role,
|
||||
avatar: user.avatar
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Login error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Server error'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Logout
|
||||
router.post('/logout', (req, res) => {
|
||||
req.session.destroy(err => {
|
||||
if (err) {
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: 'Could not log out'
|
||||
});
|
||||
}
|
||||
|
||||
res.clearCookie('connect.sid');
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Logout successful'
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Check authentication status
|
||||
router.get('/me', async (req, res) => {
|
||||
try {
|
||||
if (!req.session.user) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: 'Not authenticated'
|
||||
});
|
||||
}
|
||||
|
||||
const user = await User.findById(req.session.user.id)
|
||||
.select('-password');
|
||||
|
||||
if (!user || !user.isActive) {
|
||||
req.session.destroy();
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: 'User not found or inactive'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
user: {
|
||||
id: user._id,
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
role: user.role,
|
||||
avatar: user.avatar,
|
||||
lastLogin: user.lastLogin
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Auth check error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Server error'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Change password
|
||||
router.put('/change-password', [
|
||||
body('currentPassword').isLength({ min: 6 }),
|
||||
body('newPassword').isLength({ min: 6 })
|
||||
], async (req, res) => {
|
||||
try {
|
||||
if (!req.session.user) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: 'Not authenticated'
|
||||
});
|
||||
}
|
||||
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Invalid input data',
|
||||
errors: errors.array()
|
||||
});
|
||||
}
|
||||
|
||||
const { currentPassword, newPassword } = req.body;
|
||||
const user = await User.findById(req.session.user.id);
|
||||
|
||||
if (!user) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'User not found'
|
||||
});
|
||||
}
|
||||
|
||||
// Verify current password
|
||||
const isValidPassword = await user.comparePassword(currentPassword);
|
||||
if (!isValidPassword) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Current password is incorrect'
|
||||
});
|
||||
}
|
||||
|
||||
// Update password
|
||||
user.password = newPassword;
|
||||
await user.save();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Password updated successfully'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Change password error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Server error'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
312
.history/routes/calculator_20251019160816.js
Normal file
312
.history/routes/calculator_20251019160816.js
Normal file
@@ -0,0 +1,312 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const Service = require('../models/Service');
|
||||
|
||||
// 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 servicesByCategory = services.reduce((acc, service) => {
|
||||
if (!acc[service.category]) {
|
||||
acc[service.category] = [];
|
||||
}
|
||||
acc[service.category].push(service);
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
services: servicesByCategory,
|
||||
allServices: services
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Calculator services error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Error fetching services'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Calculate project estimate
|
||||
router.post('/calculate', async (req, res) => {
|
||||
try {
|
||||
const {
|
||||
selectedServices = [],
|
||||
projectComplexity = 'medium',
|
||||
timeline = 'standard',
|
||||
additionalFeatures = [],
|
||||
customRequirements = ''
|
||||
} = req.body;
|
||||
|
||||
if (!selectedServices.length) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Please select at least one service'
|
||||
});
|
||||
}
|
||||
|
||||
// Get selected services details
|
||||
const services = await Service.find({
|
||||
_id: { $in: selectedServices },
|
||||
isActive: true
|
||||
});
|
||||
|
||||
if (services.length !== selectedServices.length) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Some selected services are not available'
|
||||
});
|
||||
}
|
||||
|
||||
// Calculate base cost
|
||||
let baseCost = 0;
|
||||
let totalTime = 0; // in days
|
||||
|
||||
services.forEach(service => {
|
||||
baseCost += service.pricing.basePrice;
|
||||
totalTime += (service.estimatedTime.min + service.estimatedTime.max) / 2;
|
||||
});
|
||||
|
||||
// Apply complexity multiplier
|
||||
const complexityMultipliers = {
|
||||
'simple': 0.7,
|
||||
'medium': 1.0,
|
||||
'complex': 1.5,
|
||||
'enterprise': 2.0
|
||||
};
|
||||
|
||||
const complexityMultiplier = complexityMultipliers[projectComplexity] || 1.0;
|
||||
|
||||
// Apply timeline multiplier
|
||||
const timelineMultipliers = {
|
||||
'rush': 1.8, // Less than 2 weeks
|
||||
'fast': 1.4, // 2-4 weeks
|
||||
'standard': 1.0, // 1-3 months
|
||||
'flexible': 0.8 // 3+ months
|
||||
};
|
||||
|
||||
const timelineMultiplier = timelineMultipliers[timeline] || 1.0;
|
||||
|
||||
// Additional features cost
|
||||
const featureCosts = {
|
||||
'seo-optimization': 300000,
|
||||
'analytics-setup': 150000,
|
||||
'social-integration': 200000,
|
||||
'payment-gateway': 500000,
|
||||
'multilingual': 400000,
|
||||
'admin-panel': 600000,
|
||||
'api-integration': 350000,
|
||||
'mobile-responsive': 250000,
|
||||
'ssl-certificate': 100000,
|
||||
'backup-system': 200000
|
||||
};
|
||||
|
||||
let additionalCost = 0;
|
||||
additionalFeatures.forEach(feature => {
|
||||
additionalCost += featureCosts[feature] || 0;
|
||||
});
|
||||
|
||||
// Custom requirements cost (estimated based on description length and complexity)
|
||||
let customCost = 0;
|
||||
if (customRequirements && customRequirements.length > 50) {
|
||||
customCost = Math.min(customRequirements.length * 1000, 1000000); // Max 1M KRW
|
||||
}
|
||||
|
||||
// Calculate final estimate
|
||||
const subtotal = baseCost * complexityMultiplier * timelineMultiplier;
|
||||
const total = subtotal + additionalCost + customCost;
|
||||
|
||||
// Calculate time estimate
|
||||
const timeMultiplier = complexityMultiplier * timelineMultiplier;
|
||||
const estimatedDays = Math.ceil(totalTime * timeMultiplier);
|
||||
const estimatedWeeks = Math.ceil(estimatedDays / 7);
|
||||
|
||||
// Create price ranges (±20%)
|
||||
const minPrice = Math.round(total * 0.8);
|
||||
const maxPrice = Math.round(total * 1.2);
|
||||
|
||||
// Breakdown
|
||||
const breakdown = {
|
||||
baseServices: Math.round(baseCost),
|
||||
complexityAdjustment: Math.round(baseCost * (complexityMultiplier - 1)),
|
||||
timelineAdjustment: Math.round(baseCost * complexityMultiplier * (timelineMultiplier - 1)),
|
||||
additionalFeatures: additionalCost,
|
||||
customRequirements: customCost,
|
||||
subtotal: Math.round(subtotal),
|
||||
total: Math.round(total)
|
||||
};
|
||||
|
||||
const result = {
|
||||
success: true,
|
||||
estimate: {
|
||||
total: Math.round(total),
|
||||
range: {
|
||||
min: minPrice,
|
||||
max: maxPrice,
|
||||
formatted: `₩${minPrice.toLocaleString()} - ₩${maxPrice.toLocaleString()}`
|
||||
},
|
||||
breakdown,
|
||||
timeline: {
|
||||
days: estimatedDays,
|
||||
weeks: estimatedWeeks,
|
||||
formatted: estimatedWeeks === 1 ? '1 week' : `${estimatedWeeks} weeks`
|
||||
},
|
||||
currency: 'KRW',
|
||||
confidence: calculateConfidence(services.length, projectComplexity, customRequirements),
|
||||
recommendations: generateRecommendations(projectComplexity, timeline, services)
|
||||
},
|
||||
selectedServices: services.map(s => ({
|
||||
id: s._id,
|
||||
name: s.name,
|
||||
category: s.category,
|
||||
basePrice: s.pricing.basePrice
|
||||
})),
|
||||
calculatedAt: new Date()
|
||||
};
|
||||
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
console.error('Calculator calculation error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Error calculating estimate'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Helper function to calculate confidence level
|
||||
function calculateConfidence(serviceCount, complexity, customRequirements) {
|
||||
let confidence = 85; // Base confidence
|
||||
|
||||
// Adjust based on service count
|
||||
if (serviceCount === 1) confidence += 10;
|
||||
else if (serviceCount > 5) confidence -= 10;
|
||||
|
||||
// Adjust based on complexity
|
||||
if (complexity === 'simple') confidence += 10;
|
||||
else if (complexity === 'enterprise') confidence -= 15;
|
||||
|
||||
// Adjust based on custom requirements
|
||||
if (customRequirements && customRequirements.length > 200) {
|
||||
confidence -= 20;
|
||||
}
|
||||
|
||||
return Math.max(60, Math.min(95, confidence));
|
||||
}
|
||||
|
||||
// Helper function to generate recommendations
|
||||
function generateRecommendations(complexity, timeline, services) {
|
||||
const recommendations = [];
|
||||
|
||||
if (complexity === 'enterprise' && timeline === 'rush') {
|
||||
recommendations.push('Consider extending timeline for enterprise-level projects to ensure quality');
|
||||
}
|
||||
|
||||
if (services.length > 6) {
|
||||
recommendations.push('Consider breaking down into phases for better project management');
|
||||
}
|
||||
|
||||
if (timeline === 'flexible') {
|
||||
recommendations.push('Flexible timeline allows for better cost optimization and quality assurance');
|
||||
}
|
||||
|
||||
const hasDesign = services.some(s => s.category === 'design');
|
||||
const hasDevelopment = services.some(s => s.category === 'development');
|
||||
|
||||
if (hasDevelopment && !hasDesign) {
|
||||
recommendations.push('Consider adding UI/UX design services for better user experience');
|
||||
}
|
||||
|
||||
return recommendations;
|
||||
}
|
||||
|
||||
// Get pricing guidelines
|
||||
router.get('/pricing-guide', async (req, res) => {
|
||||
try {
|
||||
const pricingGuide = {
|
||||
serviceCategories: {
|
||||
'development': {
|
||||
name: 'Web Development',
|
||||
priceRange: '₩500,000 - ₩5,000,000',
|
||||
description: 'Custom web applications and websites'
|
||||
},
|
||||
'design': {
|
||||
name: 'UI/UX Design',
|
||||
priceRange: '₩300,000 - ₩2,000,000',
|
||||
description: 'User interface and experience design'
|
||||
},
|
||||
'marketing': {
|
||||
name: 'Digital Marketing',
|
||||
priceRange: '₩200,000 - ₩1,500,000',
|
||||
description: 'SEO, social media, and online marketing'
|
||||
},
|
||||
'consulting': {
|
||||
name: 'Technical Consulting',
|
||||
priceRange: '₩150,000 - ₩1,000,000',
|
||||
description: 'Technology strategy and consultation'
|
||||
}
|
||||
},
|
||||
complexityFactors: {
|
||||
'simple': {
|
||||
name: 'Simple Project',
|
||||
multiplier: '0.7x',
|
||||
description: 'Basic functionality, standard design'
|
||||
},
|
||||
'medium': {
|
||||
name: 'Medium Project',
|
||||
multiplier: '1.0x',
|
||||
description: 'Moderate complexity, custom features'
|
||||
},
|
||||
'complex': {
|
||||
name: 'Complex Project',
|
||||
multiplier: '1.5x',
|
||||
description: 'Advanced features, integrations'
|
||||
},
|
||||
'enterprise': {
|
||||
name: 'Enterprise Project',
|
||||
multiplier: '2.0x',
|
||||
description: 'Large scale, high complexity'
|
||||
}
|
||||
},
|
||||
timelineImpact: {
|
||||
'rush': {
|
||||
name: 'Rush (< 2 weeks)',
|
||||
multiplier: '+80%',
|
||||
description: 'Requires overtime and priority handling'
|
||||
},
|
||||
'fast': {
|
||||
name: 'Fast (2-4 weeks)',
|
||||
multiplier: '+40%',
|
||||
description: 'Accelerated timeline'
|
||||
},
|
||||
'standard': {
|
||||
name: 'Standard (1-3 months)',
|
||||
multiplier: 'Standard',
|
||||
description: 'Normal project timeline'
|
||||
},
|
||||
'flexible': {
|
||||
name: 'Flexible (3+ months)',
|
||||
multiplier: '-20%',
|
||||
description: 'Extended timeline allows optimization'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
pricingGuide
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Pricing guide error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Error fetching pricing guide'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
312
.history/routes/calculator_20251019162544.js
Normal file
312
.history/routes/calculator_20251019162544.js
Normal file
@@ -0,0 +1,312 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const Service = require('../models/Service');
|
||||
|
||||
// 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 servicesByCategory = services.reduce((acc, service) => {
|
||||
if (!acc[service.category]) {
|
||||
acc[service.category] = [];
|
||||
}
|
||||
acc[service.category].push(service);
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
services: servicesByCategory,
|
||||
allServices: services
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Calculator services error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Error fetching services'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Calculate project estimate
|
||||
router.post('/calculate', async (req, res) => {
|
||||
try {
|
||||
const {
|
||||
selectedServices = [],
|
||||
projectComplexity = 'medium',
|
||||
timeline = 'standard',
|
||||
additionalFeatures = [],
|
||||
customRequirements = ''
|
||||
} = req.body;
|
||||
|
||||
if (!selectedServices.length) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Please select at least one service'
|
||||
});
|
||||
}
|
||||
|
||||
// Get selected services details
|
||||
const services = await Service.find({
|
||||
_id: { $in: selectedServices },
|
||||
isActive: true
|
||||
});
|
||||
|
||||
if (services.length !== selectedServices.length) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Some selected services are not available'
|
||||
});
|
||||
}
|
||||
|
||||
// Calculate base cost
|
||||
let baseCost = 0;
|
||||
let totalTime = 0; // in days
|
||||
|
||||
services.forEach(service => {
|
||||
baseCost += service.pricing.basePrice;
|
||||
totalTime += (service.estimatedTime.min + service.estimatedTime.max) / 2;
|
||||
});
|
||||
|
||||
// Apply complexity multiplier
|
||||
const complexityMultipliers = {
|
||||
'simple': 0.7,
|
||||
'medium': 1.0,
|
||||
'complex': 1.5,
|
||||
'enterprise': 2.0
|
||||
};
|
||||
|
||||
const complexityMultiplier = complexityMultipliers[projectComplexity] || 1.0;
|
||||
|
||||
// Apply timeline multiplier
|
||||
const timelineMultipliers = {
|
||||
'rush': 1.8, // Less than 2 weeks
|
||||
'fast': 1.4, // 2-4 weeks
|
||||
'standard': 1.0, // 1-3 months
|
||||
'flexible': 0.8 // 3+ months
|
||||
};
|
||||
|
||||
const timelineMultiplier = timelineMultipliers[timeline] || 1.0;
|
||||
|
||||
// Additional features cost
|
||||
const featureCosts = {
|
||||
'seo-optimization': 300000,
|
||||
'analytics-setup': 150000,
|
||||
'social-integration': 200000,
|
||||
'payment-gateway': 500000,
|
||||
'multilingual': 400000,
|
||||
'admin-panel': 600000,
|
||||
'api-integration': 350000,
|
||||
'mobile-responsive': 250000,
|
||||
'ssl-certificate': 100000,
|
||||
'backup-system': 200000
|
||||
};
|
||||
|
||||
let additionalCost = 0;
|
||||
additionalFeatures.forEach(feature => {
|
||||
additionalCost += featureCosts[feature] || 0;
|
||||
});
|
||||
|
||||
// Custom requirements cost (estimated based on description length and complexity)
|
||||
let customCost = 0;
|
||||
if (customRequirements && customRequirements.length > 50) {
|
||||
customCost = Math.min(customRequirements.length * 1000, 1000000); // Max 1M KRW
|
||||
}
|
||||
|
||||
// Calculate final estimate
|
||||
const subtotal = baseCost * complexityMultiplier * timelineMultiplier;
|
||||
const total = subtotal + additionalCost + customCost;
|
||||
|
||||
// Calculate time estimate
|
||||
const timeMultiplier = complexityMultiplier * timelineMultiplier;
|
||||
const estimatedDays = Math.ceil(totalTime * timeMultiplier);
|
||||
const estimatedWeeks = Math.ceil(estimatedDays / 7);
|
||||
|
||||
// Create price ranges (±20%)
|
||||
const minPrice = Math.round(total * 0.8);
|
||||
const maxPrice = Math.round(total * 1.2);
|
||||
|
||||
// Breakdown
|
||||
const breakdown = {
|
||||
baseServices: Math.round(baseCost),
|
||||
complexityAdjustment: Math.round(baseCost * (complexityMultiplier - 1)),
|
||||
timelineAdjustment: Math.round(baseCost * complexityMultiplier * (timelineMultiplier - 1)),
|
||||
additionalFeatures: additionalCost,
|
||||
customRequirements: customCost,
|
||||
subtotal: Math.round(subtotal),
|
||||
total: Math.round(total)
|
||||
};
|
||||
|
||||
const result = {
|
||||
success: true,
|
||||
estimate: {
|
||||
total: Math.round(total),
|
||||
range: {
|
||||
min: minPrice,
|
||||
max: maxPrice,
|
||||
formatted: `₩${minPrice.toLocaleString()} - ₩${maxPrice.toLocaleString()}`
|
||||
},
|
||||
breakdown,
|
||||
timeline: {
|
||||
days: estimatedDays,
|
||||
weeks: estimatedWeeks,
|
||||
formatted: estimatedWeeks === 1 ? '1 week' : `${estimatedWeeks} weeks`
|
||||
},
|
||||
currency: 'KRW',
|
||||
confidence: calculateConfidence(services.length, projectComplexity, customRequirements),
|
||||
recommendations: generateRecommendations(projectComplexity, timeline, services)
|
||||
},
|
||||
selectedServices: services.map(s => ({
|
||||
id: s._id,
|
||||
name: s.name,
|
||||
category: s.category,
|
||||
basePrice: s.pricing.basePrice
|
||||
})),
|
||||
calculatedAt: new Date()
|
||||
};
|
||||
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
console.error('Calculator calculation error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Error calculating estimate'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Helper function to calculate confidence level
|
||||
function calculateConfidence(serviceCount, complexity, customRequirements) {
|
||||
let confidence = 85; // Base confidence
|
||||
|
||||
// Adjust based on service count
|
||||
if (serviceCount === 1) confidence += 10;
|
||||
else if (serviceCount > 5) confidence -= 10;
|
||||
|
||||
// Adjust based on complexity
|
||||
if (complexity === 'simple') confidence += 10;
|
||||
else if (complexity === 'enterprise') confidence -= 15;
|
||||
|
||||
// Adjust based on custom requirements
|
||||
if (customRequirements && customRequirements.length > 200) {
|
||||
confidence -= 20;
|
||||
}
|
||||
|
||||
return Math.max(60, Math.min(95, confidence));
|
||||
}
|
||||
|
||||
// Helper function to generate recommendations
|
||||
function generateRecommendations(complexity, timeline, services) {
|
||||
const recommendations = [];
|
||||
|
||||
if (complexity === 'enterprise' && timeline === 'rush') {
|
||||
recommendations.push('Consider extending timeline for enterprise-level projects to ensure quality');
|
||||
}
|
||||
|
||||
if (services.length > 6) {
|
||||
recommendations.push('Consider breaking down into phases for better project management');
|
||||
}
|
||||
|
||||
if (timeline === 'flexible') {
|
||||
recommendations.push('Flexible timeline allows for better cost optimization and quality assurance');
|
||||
}
|
||||
|
||||
const hasDesign = services.some(s => s.category === 'design');
|
||||
const hasDevelopment = services.some(s => s.category === 'development');
|
||||
|
||||
if (hasDevelopment && !hasDesign) {
|
||||
recommendations.push('Consider adding UI/UX design services for better user experience');
|
||||
}
|
||||
|
||||
return recommendations;
|
||||
}
|
||||
|
||||
// Get pricing guidelines
|
||||
router.get('/pricing-guide', async (req, res) => {
|
||||
try {
|
||||
const pricingGuide = {
|
||||
serviceCategories: {
|
||||
'development': {
|
||||
name: 'Web Development',
|
||||
priceRange: '₩500,000 - ₩5,000,000',
|
||||
description: 'Custom web applications and websites'
|
||||
},
|
||||
'design': {
|
||||
name: 'UI/UX Design',
|
||||
priceRange: '₩300,000 - ₩2,000,000',
|
||||
description: 'User interface and experience design'
|
||||
},
|
||||
'marketing': {
|
||||
name: 'Digital Marketing',
|
||||
priceRange: '₩200,000 - ₩1,500,000',
|
||||
description: 'SEO, social media, and online marketing'
|
||||
},
|
||||
'consulting': {
|
||||
name: 'Technical Consulting',
|
||||
priceRange: '₩150,000 - ₩1,000,000',
|
||||
description: 'Technology strategy and consultation'
|
||||
}
|
||||
},
|
||||
complexityFactors: {
|
||||
'simple': {
|
||||
name: 'Simple Project',
|
||||
multiplier: '0.7x',
|
||||
description: 'Basic functionality, standard design'
|
||||
},
|
||||
'medium': {
|
||||
name: 'Medium Project',
|
||||
multiplier: '1.0x',
|
||||
description: 'Moderate complexity, custom features'
|
||||
},
|
||||
'complex': {
|
||||
name: 'Complex Project',
|
||||
multiplier: '1.5x',
|
||||
description: 'Advanced features, integrations'
|
||||
},
|
||||
'enterprise': {
|
||||
name: 'Enterprise Project',
|
||||
multiplier: '2.0x',
|
||||
description: 'Large scale, high complexity'
|
||||
}
|
||||
},
|
||||
timelineImpact: {
|
||||
'rush': {
|
||||
name: 'Rush (< 2 weeks)',
|
||||
multiplier: '+80%',
|
||||
description: 'Requires overtime and priority handling'
|
||||
},
|
||||
'fast': {
|
||||
name: 'Fast (2-4 weeks)',
|
||||
multiplier: '+40%',
|
||||
description: 'Accelerated timeline'
|
||||
},
|
||||
'standard': {
|
||||
name: 'Standard (1-3 months)',
|
||||
multiplier: 'Standard',
|
||||
description: 'Normal project timeline'
|
||||
},
|
||||
'flexible': {
|
||||
name: 'Flexible (3+ months)',
|
||||
multiplier: '-20%',
|
||||
description: 'Extended timeline allows optimization'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
pricingGuide
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Pricing guide error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Error fetching pricing guide'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
250
.history/routes/contact_20251019160738.js
Normal file
250
.history/routes/contact_20251019160738.js
Normal file
@@ -0,0 +1,250 @@
|
||||
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 });
|
||||
}
|
||||
|
||||
// Contact form validation
|
||||
const contactValidation = [
|
||||
body('name').trim().isLength({ min: 2, max: 100 }),
|
||||
body('email').isEmail().normalizeEmail(),
|
||||
body('subject').trim().isLength({ min: 5, max: 200 }),
|
||||
body('message').trim().isLength({ min: 10, max: 2000 }),
|
||||
body('phone').optional().isMobilePhone(),
|
||||
body('company').optional().trim().isLength({ max: 100 })
|
||||
];
|
||||
|
||||
// Submit contact form
|
||||
router.post('/submit', contactValidation, async (req, res) => {
|
||||
try {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Invalid input data',
|
||||
errors: errors.array()
|
||||
});
|
||||
}
|
||||
|
||||
const contactData = {
|
||||
...req.body,
|
||||
ipAddress: req.ip,
|
||||
userAgent: req.get('User-Agent'),
|
||||
source: 'website'
|
||||
};
|
||||
|
||||
// Save to database
|
||||
const contact = new Contact(contactData);
|
||||
await contact.save();
|
||||
|
||||
// Send email notification
|
||||
await sendEmailNotification(contact);
|
||||
|
||||
// Send Telegram notification
|
||||
await sendTelegramNotification(contact);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Your message has been sent successfully. We will get back to you soon!',
|
||||
contactId: contact._id
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Contact form error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Sorry, there was an error sending your message. Please try again.'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Get project estimate
|
||||
router.post('/estimate', [
|
||||
body('services').isArray().notEmpty(),
|
||||
body('projectType').notEmpty(),
|
||||
body('timeline').notEmpty(),
|
||||
body('budget').notEmpty(),
|
||||
body('description').trim().isLength({ min: 10, max: 1000 })
|
||||
], async (req, res) => {
|
||||
try {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Invalid input data',
|
||||
errors: errors.array()
|
||||
});
|
||||
}
|
||||
|
||||
const { services, projectType, timeline, budget, description, contactInfo } = req.body;
|
||||
|
||||
// Calculate estimate (simplified)
|
||||
const estimate = calculateProjectEstimate(services, projectType, timeline);
|
||||
|
||||
// Save inquiry to database
|
||||
const contactData = {
|
||||
name: contactInfo.name,
|
||||
email: contactInfo.email,
|
||||
phone: contactInfo.phone,
|
||||
company: contactInfo.company,
|
||||
subject: `Project Estimate Request - ${projectType}`,
|
||||
message: `Project Description: ${description}\n\nServices: ${services.join(', ')}\nTimeline: ${timeline}\nBudget: ${budget}\n\nCalculated Estimate: ${estimate.formatted}`,
|
||||
serviceInterest: projectType,
|
||||
budget: budget,
|
||||
timeline: timeline,
|
||||
source: 'calculator',
|
||||
ipAddress: req.ip,
|
||||
userAgent: req.get('User-Agent')
|
||||
};
|
||||
|
||||
const contact = new Contact(contactData);
|
||||
await contact.save();
|
||||
|
||||
// Send notifications
|
||||
await sendEmailNotification(contact);
|
||||
await sendTelegramNotification(contact);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Your project estimate request has been submitted successfully!',
|
||||
estimate: estimate,
|
||||
contactId: contact._id
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Estimate request error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Sorry, there was an error processing your request. Please try again.'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Helper function to send email notification
|
||||
async function sendEmailNotification(contact) {
|
||||
if (!process.env.EMAIL_HOST || !process.env.EMAIL_USER) {
|
||||
console.log('Email configuration not provided, skipping email notification');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const transporter = nodemailer.createTransporter({
|
||||
host: process.env.EMAIL_HOST,
|
||||
port: process.env.EMAIL_PORT || 587,
|
||||
secure: false,
|
||||
auth: {
|
||||
user: process.env.EMAIL_USER,
|
||||
pass: process.env.EMAIL_PASS
|
||||
}
|
||||
});
|
||||
|
||||
const mailOptions = {
|
||||
from: process.env.EMAIL_USER,
|
||||
to: process.env.ADMIN_EMAIL || process.env.EMAIL_USER,
|
||||
subject: `New Contact Form Submission: ${contact.subject}`,
|
||||
html: `
|
||||
<h2>New Contact Form Submission</h2>
|
||||
<p><strong>Name:</strong> ${contact.name}</p>
|
||||
<p><strong>Email:</strong> ${contact.email}</p>
|
||||
<p><strong>Phone:</strong> ${contact.phone || 'Not provided'}</p>
|
||||
<p><strong>Company:</strong> ${contact.company || 'Not provided'}</p>
|
||||
<p><strong>Subject:</strong> ${contact.subject}</p>
|
||||
<p><strong>Message:</strong></p>
|
||||
<p>${contact.message.replace(/\n/g, '<br>')}</p>
|
||||
<p><strong>Source:</strong> ${contact.source}</p>
|
||||
<p><strong>Submitted:</strong> ${contact.createdAt}</p>
|
||||
<hr>
|
||||
<p><a href="${process.env.SITE_URL}/admin/contacts/${contact._id}">View in Admin Panel</a></p>
|
||||
`
|
||||
};
|
||||
|
||||
await transporter.sendMail(mailOptions);
|
||||
console.log('Email notification sent successfully');
|
||||
} catch (error) {
|
||||
console.error('Email notification error:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to calculate project estimate
|
||||
function calculateProjectEstimate(services, projectType, timeline) {
|
||||
const baseRates = {
|
||||
'web-development': 50000,
|
||||
'mobile-app': 80000,
|
||||
'ui-ux-design': 30000,
|
||||
'branding': 20000,
|
||||
'e-commerce': 70000,
|
||||
'consulting': 40000
|
||||
};
|
||||
|
||||
const timelineMultipliers = {
|
||||
'asap': 1.5,
|
||||
'1-month': 1.2,
|
||||
'1-3-months': 1.0,
|
||||
'3-6-months': 0.9,
|
||||
'flexible': 0.8
|
||||
};
|
||||
|
||||
let basePrice = baseRates[projectType] || 50000;
|
||||
let multiplier = timelineMultipliers[timeline] || 1.0;
|
||||
|
||||
// Add service modifiers
|
||||
let serviceModifier = 1.0;
|
||||
if (services.length > 3) serviceModifier += 0.3;
|
||||
if (services.length > 5) serviceModifier += 0.5;
|
||||
|
||||
const totalEstimate = Math.round(basePrice * multiplier * serviceModifier);
|
||||
const rangeMin = Math.round(totalEstimate * 0.8);
|
||||
const rangeMax = Math.round(totalEstimate * 1.3);
|
||||
|
||||
return {
|
||||
base: totalEstimate,
|
||||
min: rangeMin,
|
||||
max: rangeMax,
|
||||
formatted: `₩${rangeMin.toLocaleString()} - ₩${rangeMax.toLocaleString()}`,
|
||||
currency: 'KRW'
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = router;
|
||||
250
.history/routes/contact_20251019162544.js
Normal file
250
.history/routes/contact_20251019162544.js
Normal file
@@ -0,0 +1,250 @@
|
||||
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 });
|
||||
}
|
||||
|
||||
// Contact form validation
|
||||
const contactValidation = [
|
||||
body('name').trim().isLength({ min: 2, max: 100 }),
|
||||
body('email').isEmail().normalizeEmail(),
|
||||
body('subject').trim().isLength({ min: 5, max: 200 }),
|
||||
body('message').trim().isLength({ min: 10, max: 2000 }),
|
||||
body('phone').optional().isMobilePhone(),
|
||||
body('company').optional().trim().isLength({ max: 100 })
|
||||
];
|
||||
|
||||
// Submit contact form
|
||||
router.post('/submit', contactValidation, async (req, res) => {
|
||||
try {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Invalid input data',
|
||||
errors: errors.array()
|
||||
});
|
||||
}
|
||||
|
||||
const contactData = {
|
||||
...req.body,
|
||||
ipAddress: req.ip,
|
||||
userAgent: req.get('User-Agent'),
|
||||
source: 'website'
|
||||
};
|
||||
|
||||
// Save to database
|
||||
const contact = new Contact(contactData);
|
||||
await contact.save();
|
||||
|
||||
// Send email notification
|
||||
await sendEmailNotification(contact);
|
||||
|
||||
// Send Telegram notification
|
||||
await sendTelegramNotification(contact);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Your message has been sent successfully. We will get back to you soon!',
|
||||
contactId: contact._id
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Contact form error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Sorry, there was an error sending your message. Please try again.'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Get project estimate
|
||||
router.post('/estimate', [
|
||||
body('services').isArray().notEmpty(),
|
||||
body('projectType').notEmpty(),
|
||||
body('timeline').notEmpty(),
|
||||
body('budget').notEmpty(),
|
||||
body('description').trim().isLength({ min: 10, max: 1000 })
|
||||
], async (req, res) => {
|
||||
try {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Invalid input data',
|
||||
errors: errors.array()
|
||||
});
|
||||
}
|
||||
|
||||
const { services, projectType, timeline, budget, description, contactInfo } = req.body;
|
||||
|
||||
// Calculate estimate (simplified)
|
||||
const estimate = calculateProjectEstimate(services, projectType, timeline);
|
||||
|
||||
// Save inquiry to database
|
||||
const contactData = {
|
||||
name: contactInfo.name,
|
||||
email: contactInfo.email,
|
||||
phone: contactInfo.phone,
|
||||
company: contactInfo.company,
|
||||
subject: `Project Estimate Request - ${projectType}`,
|
||||
message: `Project Description: ${description}\n\nServices: ${services.join(', ')}\nTimeline: ${timeline}\nBudget: ${budget}\n\nCalculated Estimate: ${estimate.formatted}`,
|
||||
serviceInterest: projectType,
|
||||
budget: budget,
|
||||
timeline: timeline,
|
||||
source: 'calculator',
|
||||
ipAddress: req.ip,
|
||||
userAgent: req.get('User-Agent')
|
||||
};
|
||||
|
||||
const contact = new Contact(contactData);
|
||||
await contact.save();
|
||||
|
||||
// Send notifications
|
||||
await sendEmailNotification(contact);
|
||||
await sendTelegramNotification(contact);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Your project estimate request has been submitted successfully!',
|
||||
estimate: estimate,
|
||||
contactId: contact._id
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Estimate request error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Sorry, there was an error processing your request. Please try again.'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Helper function to send email notification
|
||||
async function sendEmailNotification(contact) {
|
||||
if (!process.env.EMAIL_HOST || !process.env.EMAIL_USER) {
|
||||
console.log('Email configuration not provided, skipping email notification');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const transporter = nodemailer.createTransporter({
|
||||
host: process.env.EMAIL_HOST,
|
||||
port: process.env.EMAIL_PORT || 587,
|
||||
secure: false,
|
||||
auth: {
|
||||
user: process.env.EMAIL_USER,
|
||||
pass: process.env.EMAIL_PASS
|
||||
}
|
||||
});
|
||||
|
||||
const mailOptions = {
|
||||
from: process.env.EMAIL_USER,
|
||||
to: process.env.ADMIN_EMAIL || process.env.EMAIL_USER,
|
||||
subject: `New Contact Form Submission: ${contact.subject}`,
|
||||
html: `
|
||||
<h2>New Contact Form Submission</h2>
|
||||
<p><strong>Name:</strong> ${contact.name}</p>
|
||||
<p><strong>Email:</strong> ${contact.email}</p>
|
||||
<p><strong>Phone:</strong> ${contact.phone || 'Not provided'}</p>
|
||||
<p><strong>Company:</strong> ${contact.company || 'Not provided'}</p>
|
||||
<p><strong>Subject:</strong> ${contact.subject}</p>
|
||||
<p><strong>Message:</strong></p>
|
||||
<p>${contact.message.replace(/\n/g, '<br>')}</p>
|
||||
<p><strong>Source:</strong> ${contact.source}</p>
|
||||
<p><strong>Submitted:</strong> ${contact.createdAt}</p>
|
||||
<hr>
|
||||
<p><a href="${process.env.SITE_URL}/admin/contacts/${contact._id}">View in Admin Panel</a></p>
|
||||
`
|
||||
};
|
||||
|
||||
await transporter.sendMail(mailOptions);
|
||||
console.log('Email notification sent successfully');
|
||||
} catch (error) {
|
||||
console.error('Email notification error:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to calculate project estimate
|
||||
function calculateProjectEstimate(services, projectType, timeline) {
|
||||
const baseRates = {
|
||||
'web-development': 50000,
|
||||
'mobile-app': 80000,
|
||||
'ui-ux-design': 30000,
|
||||
'branding': 20000,
|
||||
'e-commerce': 70000,
|
||||
'consulting': 40000
|
||||
};
|
||||
|
||||
const timelineMultipliers = {
|
||||
'asap': 1.5,
|
||||
'1-month': 1.2,
|
||||
'1-3-months': 1.0,
|
||||
'3-6-months': 0.9,
|
||||
'flexible': 0.8
|
||||
};
|
||||
|
||||
let basePrice = baseRates[projectType] || 50000;
|
||||
let multiplier = timelineMultipliers[timeline] || 1.0;
|
||||
|
||||
// Add service modifiers
|
||||
let serviceModifier = 1.0;
|
||||
if (services.length > 3) serviceModifier += 0.3;
|
||||
if (services.length > 5) serviceModifier += 0.5;
|
||||
|
||||
const totalEstimate = Math.round(basePrice * multiplier * serviceModifier);
|
||||
const rangeMin = Math.round(totalEstimate * 0.8);
|
||||
const rangeMax = Math.round(totalEstimate * 1.3);
|
||||
|
||||
return {
|
||||
base: totalEstimate,
|
||||
min: rangeMin,
|
||||
max: rangeMax,
|
||||
formatted: `₩${rangeMin.toLocaleString()} - ₩${rangeMax.toLocaleString()}`,
|
||||
currency: 'KRW'
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = router;
|
||||
203
.history/routes/index_20251019160650.js
Normal file
203
.history/routes/index_20251019160650.js
Normal file
@@ -0,0 +1,203 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const Portfolio = require('../models/Portfolio');
|
||||
const Service = require('../models/Service');
|
||||
const SiteSettings = require('../models/SiteSettings');
|
||||
|
||||
// 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)
|
||||
]);
|
||||
|
||||
res.render('index', {
|
||||
title: 'SmartSolTech - Innovative Technology Solutions',
|
||||
settings: settings || {},
|
||||
featuredPortfolio,
|
||||
featuredServices,
|
||||
currentPage: 'home'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Home page error:', error);
|
||||
res.status(500).render('error', {
|
||||
title: 'Error',
|
||||
message: 'Something went wrong'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// About page
|
||||
router.get('/about', async (req, res) => {
|
||||
try {
|
||||
const settings = await SiteSettings.findOne() || {};
|
||||
|
||||
res.render('about', {
|
||||
title: 'About Us - SmartSolTech',
|
||||
settings,
|
||||
currentPage: 'about'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('About page error:', error);
|
||||
res.status(500).render('error', {
|
||||
title: 'Error',
|
||||
message: 'Something went wrong'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Portfolio page
|
||||
router.get('/portfolio', async (req, res) => {
|
||||
try {
|
||||
const page = parseInt(req.query.page) || 1;
|
||||
const limit = 12;
|
||||
const skip = (page - 1) * limit;
|
||||
const category = req.query.category;
|
||||
|
||||
let query = { isPublished: true };
|
||||
if (category && category !== 'all') {
|
||||
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 totalPages = Math.ceil(total / limit);
|
||||
|
||||
res.render('portfolio', {
|
||||
title: 'Portfolio - SmartSolTech',
|
||||
portfolio,
|
||||
categories,
|
||||
currentCategory: category || 'all',
|
||||
pagination: {
|
||||
current: page,
|
||||
total: totalPages,
|
||||
hasNext: page < totalPages,
|
||||
hasPrev: page > 1
|
||||
},
|
||||
currentPage: 'portfolio'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Portfolio page error:', error);
|
||||
res.status(500).render('error', {
|
||||
title: 'Error',
|
||||
message: 'Something went wrong'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Portfolio detail page
|
||||
router.get('/portfolio/:id', async (req, res) => {
|
||||
try {
|
||||
const portfolio = await Portfolio.findById(req.params.id);
|
||||
|
||||
if (!portfolio || !portfolio.isPublished) {
|
||||
return res.status(404).render('404', {
|
||||
title: '404 - Project Not Found',
|
||||
message: 'The requested project was not found'
|
||||
});
|
||||
}
|
||||
|
||||
// Increment view count
|
||||
portfolio.viewCount += 1;
|
||||
await portfolio.save();
|
||||
|
||||
// Get related projects
|
||||
const relatedProjects = await Portfolio.find({
|
||||
_id: { $ne: portfolio._id },
|
||||
category: portfolio.category,
|
||||
isPublished: true
|
||||
}).limit(3);
|
||||
|
||||
res.render('portfolio-detail', {
|
||||
title: `${portfolio.title} - Portfolio - SmartSolTech`,
|
||||
portfolio,
|
||||
relatedProjects,
|
||||
currentPage: 'portfolio'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Portfolio detail error:', error);
|
||||
res.status(500).render('error', {
|
||||
title: 'Error',
|
||||
message: 'Something went wrong'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 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 });
|
||||
|
||||
res.render('services', {
|
||||
title: 'Services - SmartSolTech',
|
||||
services,
|
||||
categories,
|
||||
currentPage: 'services'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Services page error:', error);
|
||||
res.status(500).render('error', {
|
||||
title: 'Error',
|
||||
message: 'Something went wrong'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 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 });
|
||||
|
||||
res.render('calculator', {
|
||||
title: 'Project Calculator - SmartSolTech',
|
||||
services,
|
||||
currentPage: 'calculator'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Calculator page error:', error);
|
||||
res.status(500).render('error', {
|
||||
title: 'Error',
|
||||
message: 'Something went wrong'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Contact page
|
||||
router.get('/contact', async (req, res) => {
|
||||
try {
|
||||
const settings = await SiteSettings.findOne() || {};
|
||||
|
||||
res.render('contact', {
|
||||
title: 'Contact Us - SmartSolTech',
|
||||
settings,
|
||||
currentPage: 'contact'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Contact page error:', error);
|
||||
res.status(500).render('error', {
|
||||
title: 'Error',
|
||||
message: 'Something went wrong'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
203
.history/routes/index_20251019162544.js
Normal file
203
.history/routes/index_20251019162544.js
Normal file
@@ -0,0 +1,203 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const Portfolio = require('../models/Portfolio');
|
||||
const Service = require('../models/Service');
|
||||
const SiteSettings = require('../models/SiteSettings');
|
||||
|
||||
// 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)
|
||||
]);
|
||||
|
||||
res.render('index', {
|
||||
title: 'SmartSolTech - Innovative Technology Solutions',
|
||||
settings: settings || {},
|
||||
featuredPortfolio,
|
||||
featuredServices,
|
||||
currentPage: 'home'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Home page error:', error);
|
||||
res.status(500).render('error', {
|
||||
title: 'Error',
|
||||
message: 'Something went wrong'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// About page
|
||||
router.get('/about', async (req, res) => {
|
||||
try {
|
||||
const settings = await SiteSettings.findOne() || {};
|
||||
|
||||
res.render('about', {
|
||||
title: 'About Us - SmartSolTech',
|
||||
settings,
|
||||
currentPage: 'about'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('About page error:', error);
|
||||
res.status(500).render('error', {
|
||||
title: 'Error',
|
||||
message: 'Something went wrong'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Portfolio page
|
||||
router.get('/portfolio', async (req, res) => {
|
||||
try {
|
||||
const page = parseInt(req.query.page) || 1;
|
||||
const limit = 12;
|
||||
const skip = (page - 1) * limit;
|
||||
const category = req.query.category;
|
||||
|
||||
let query = { isPublished: true };
|
||||
if (category && category !== 'all') {
|
||||
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 totalPages = Math.ceil(total / limit);
|
||||
|
||||
res.render('portfolio', {
|
||||
title: 'Portfolio - SmartSolTech',
|
||||
portfolio,
|
||||
categories,
|
||||
currentCategory: category || 'all',
|
||||
pagination: {
|
||||
current: page,
|
||||
total: totalPages,
|
||||
hasNext: page < totalPages,
|
||||
hasPrev: page > 1
|
||||
},
|
||||
currentPage: 'portfolio'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Portfolio page error:', error);
|
||||
res.status(500).render('error', {
|
||||
title: 'Error',
|
||||
message: 'Something went wrong'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Portfolio detail page
|
||||
router.get('/portfolio/:id', async (req, res) => {
|
||||
try {
|
||||
const portfolio = await Portfolio.findById(req.params.id);
|
||||
|
||||
if (!portfolio || !portfolio.isPublished) {
|
||||
return res.status(404).render('404', {
|
||||
title: '404 - Project Not Found',
|
||||
message: 'The requested project was not found'
|
||||
});
|
||||
}
|
||||
|
||||
// Increment view count
|
||||
portfolio.viewCount += 1;
|
||||
await portfolio.save();
|
||||
|
||||
// Get related projects
|
||||
const relatedProjects = await Portfolio.find({
|
||||
_id: { $ne: portfolio._id },
|
||||
category: portfolio.category,
|
||||
isPublished: true
|
||||
}).limit(3);
|
||||
|
||||
res.render('portfolio-detail', {
|
||||
title: `${portfolio.title} - Portfolio - SmartSolTech`,
|
||||
portfolio,
|
||||
relatedProjects,
|
||||
currentPage: 'portfolio'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Portfolio detail error:', error);
|
||||
res.status(500).render('error', {
|
||||
title: 'Error',
|
||||
message: 'Something went wrong'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 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 });
|
||||
|
||||
res.render('services', {
|
||||
title: 'Services - SmartSolTech',
|
||||
services,
|
||||
categories,
|
||||
currentPage: 'services'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Services page error:', error);
|
||||
res.status(500).render('error', {
|
||||
title: 'Error',
|
||||
message: 'Something went wrong'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 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 });
|
||||
|
||||
res.render('calculator', {
|
||||
title: 'Project Calculator - SmartSolTech',
|
||||
services,
|
||||
currentPage: 'calculator'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Calculator page error:', error);
|
||||
res.status(500).render('error', {
|
||||
title: 'Error',
|
||||
message: 'Something went wrong'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Contact page
|
||||
router.get('/contact', async (req, res) => {
|
||||
try {
|
||||
const settings = await SiteSettings.findOne() || {};
|
||||
|
||||
res.render('contact', {
|
||||
title: 'Contact Us - SmartSolTech',
|
||||
settings,
|
||||
currentPage: 'contact'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Contact page error:', error);
|
||||
res.status(500).render('error', {
|
||||
title: 'Error',
|
||||
message: 'Something went wrong'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
345
.history/routes/media_20251019160925.js
Normal file
345
.history/routes/media_20251019160925.js
Normal file
@@ -0,0 +1,345 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const multer = require('multer');
|
||||
const sharp = require('sharp');
|
||||
const path = require('path');
|
||||
const fs = require('fs').promises;
|
||||
|
||||
// Authentication middleware
|
||||
const requireAuth = (req, res, next) => {
|
||||
if (!req.session.user) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: 'Authentication required'
|
||||
});
|
||||
}
|
||||
next();
|
||||
};
|
||||
|
||||
// Configure multer for file uploads
|
||||
const storage = multer.diskStorage({
|
||||
destination: async (req, file, cb) => {
|
||||
const uploadPath = path.join(__dirname, '../public/uploads');
|
||||
try {
|
||||
await fs.mkdir(uploadPath, { recursive: true });
|
||||
cb(null, uploadPath);
|
||||
} catch (error) {
|
||||
cb(error);
|
||||
}
|
||||
},
|
||||
filename: (req, file, cb) => {
|
||||
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
|
||||
const ext = path.extname(file.originalname);
|
||||
cb(null, file.fieldname + '-' + uniqueSuffix + ext);
|
||||
}
|
||||
});
|
||||
|
||||
const upload = multer({
|
||||
storage: storage,
|
||||
limits: {
|
||||
fileSize: parseInt(process.env.MAX_FILE_SIZE) || 10 * 1024 * 1024, // 10MB
|
||||
files: 10
|
||||
},
|
||||
fileFilter: (req, file, cb) => {
|
||||
// Allow images only
|
||||
if (file.mimetype.startsWith('image/')) {
|
||||
cb(null, true);
|
||||
} else {
|
||||
cb(new Error('Only image files are allowed'), false);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Upload single image
|
||||
router.post('/upload', requireAuth, upload.single('image'), async (req, res) => {
|
||||
try {
|
||||
if (!req.file) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'No file uploaded'
|
||||
});
|
||||
}
|
||||
|
||||
const originalPath = req.file.path;
|
||||
const filename = req.file.filename;
|
||||
const nameWithoutExt = path.parse(filename).name;
|
||||
|
||||
// Create optimized versions
|
||||
const sizes = {
|
||||
thumbnail: { width: 300, height: 200 },
|
||||
medium: { width: 800, height: 600 },
|
||||
large: { width: 1200, height: 900 }
|
||||
};
|
||||
|
||||
const optimizedImages = {};
|
||||
|
||||
for (const [sizeName, dimensions] of Object.entries(sizes)) {
|
||||
const outputPath = path.join(
|
||||
path.dirname(originalPath),
|
||||
`${nameWithoutExt}-${sizeName}.webp`
|
||||
);
|
||||
|
||||
await sharp(originalPath)
|
||||
.resize(dimensions.width, dimensions.height, {
|
||||
fit: 'inside',
|
||||
withoutEnlargement: true
|
||||
})
|
||||
.webp({ quality: 85 })
|
||||
.toFile(outputPath);
|
||||
|
||||
optimizedImages[sizeName] = `/uploads/${path.basename(outputPath)}`;
|
||||
}
|
||||
|
||||
// Keep original as well (converted to webp for better compression)
|
||||
const originalWebpPath = path.join(
|
||||
path.dirname(originalPath),
|
||||
`${nameWithoutExt}-original.webp`
|
||||
);
|
||||
|
||||
await sharp(originalPath)
|
||||
.webp({ quality: 90 })
|
||||
.toFile(originalWebpPath);
|
||||
|
||||
optimizedImages.original = `/uploads/${path.basename(originalWebpPath)}`;
|
||||
|
||||
// Remove the original uploaded file
|
||||
await fs.unlink(originalPath);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Image uploaded and optimized successfully',
|
||||
images: optimizedImages,
|
||||
metadata: {
|
||||
originalName: req.file.originalname,
|
||||
mimeType: req.file.mimetype,
|
||||
size: req.file.size
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Image upload error:', error);
|
||||
|
||||
// Clean up files on error
|
||||
if (req.file && req.file.path) {
|
||||
try {
|
||||
await fs.unlink(req.file.path);
|
||||
} catch (unlinkError) {
|
||||
console.error('Error removing file:', unlinkError);
|
||||
}
|
||||
}
|
||||
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Error uploading image'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Upload multiple images
|
||||
router.post('/upload-multiple', requireAuth, upload.array('images', 10), async (req, res) => {
|
||||
try {
|
||||
if (!req.files || req.files.length === 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'No files uploaded'
|
||||
});
|
||||
}
|
||||
|
||||
const uploadedImages = [];
|
||||
|
||||
for (const file of req.files) {
|
||||
try {
|
||||
const originalPath = file.path;
|
||||
const filename = file.filename;
|
||||
const nameWithoutExt = path.parse(filename).name;
|
||||
|
||||
// Create optimized versions
|
||||
const sizes = {
|
||||
thumbnail: { width: 300, height: 200 },
|
||||
medium: { width: 800, height: 600 },
|
||||
large: { width: 1200, height: 900 }
|
||||
};
|
||||
|
||||
const optimizedImages = {};
|
||||
|
||||
for (const [sizeName, dimensions] of Object.entries(sizes)) {
|
||||
const outputPath = path.join(
|
||||
path.dirname(originalPath),
|
||||
`${nameWithoutExt}-${sizeName}.webp`
|
||||
);
|
||||
|
||||
await sharp(originalPath)
|
||||
.resize(dimensions.width, dimensions.height, {
|
||||
fit: 'inside',
|
||||
withoutEnlargement: true
|
||||
})
|
||||
.webp({ quality: 85 })
|
||||
.toFile(outputPath);
|
||||
|
||||
optimizedImages[sizeName] = `/uploads/${path.basename(outputPath)}`;
|
||||
}
|
||||
|
||||
// Original as webp
|
||||
const originalWebpPath = path.join(
|
||||
path.dirname(originalPath),
|
||||
`${nameWithoutExt}-original.webp`
|
||||
);
|
||||
|
||||
await sharp(originalPath)
|
||||
.webp({ quality: 90 })
|
||||
.toFile(originalWebpPath);
|
||||
|
||||
optimizedImages.original = `/uploads/${path.basename(originalWebpPath)}`;
|
||||
|
||||
uploadedImages.push({
|
||||
originalName: file.originalname,
|
||||
images: optimizedImages,
|
||||
metadata: {
|
||||
mimeType: file.mimetype,
|
||||
size: file.size
|
||||
}
|
||||
});
|
||||
|
||||
// Remove original file
|
||||
await fs.unlink(originalPath);
|
||||
} catch (fileError) {
|
||||
console.error(`Error processing file ${file.originalname}:`, fileError);
|
||||
// Clean up this file and continue with others
|
||||
try {
|
||||
await fs.unlink(file.path);
|
||||
} catch (unlinkError) {
|
||||
console.error('Error removing file:', unlinkError);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: `${uploadedImages.length} images uploaded and optimized successfully`,
|
||||
images: uploadedImages
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Multiple images upload error:', error);
|
||||
|
||||
// Clean up files on error
|
||||
if (req.files) {
|
||||
for (const file of req.files) {
|
||||
try {
|
||||
await fs.unlink(file.path);
|
||||
} catch (unlinkError) {
|
||||
console.error('Error removing file:', unlinkError);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Error uploading images'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Delete image
|
||||
router.delete('/:filename', requireAuth, async (req, res) => {
|
||||
try {
|
||||
const filename = req.params.filename;
|
||||
const uploadPath = path.join(__dirname, '../public/uploads');
|
||||
|
||||
// Security check - ensure filename doesn't contain path traversal
|
||||
if (filename.includes('..') || filename.includes('/') || filename.includes('\\')) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Invalid filename'
|
||||
});
|
||||
}
|
||||
|
||||
const filePath = path.join(uploadPath, filename);
|
||||
|
||||
try {
|
||||
await fs.access(filePath);
|
||||
await fs.unlink(filePath);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Image deleted successfully'
|
||||
});
|
||||
} catch (error) {
|
||||
if (error.code === 'ENOENT') {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Image not found'
|
||||
});
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Image deletion error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Error deleting image'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// List uploaded images
|
||||
router.get('/list', requireAuth, async (req, res) => {
|
||||
try {
|
||||
const uploadPath = path.join(__dirname, '../public/uploads');
|
||||
const page = parseInt(req.query.page) || 1;
|
||||
const limit = parseInt(req.query.limit) || 20;
|
||||
|
||||
const files = await fs.readdir(uploadPath);
|
||||
const imageFiles = files.filter(file =>
|
||||
/\.(jpg|jpeg|png|gif|webp)$/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) => {
|
||||
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
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(`Error getting stats for ${file}:`, error);
|
||||
return null;
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
const validImages = imagesWithStats.filter(img => img !== null);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
images: validImages,
|
||||
pagination: {
|
||||
current: page,
|
||||
total: totalPages,
|
||||
limit,
|
||||
totalItems: total,
|
||||
hasNext: page < totalPages,
|
||||
hasPrev: page > 1
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('List images error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Error listing images'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
345
.history/routes/media_20251019162544.js
Normal file
345
.history/routes/media_20251019162544.js
Normal file
@@ -0,0 +1,345 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const multer = require('multer');
|
||||
const sharp = require('sharp');
|
||||
const path = require('path');
|
||||
const fs = require('fs').promises;
|
||||
|
||||
// Authentication middleware
|
||||
const requireAuth = (req, res, next) => {
|
||||
if (!req.session.user) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
message: 'Authentication required'
|
||||
});
|
||||
}
|
||||
next();
|
||||
};
|
||||
|
||||
// Configure multer for file uploads
|
||||
const storage = multer.diskStorage({
|
||||
destination: async (req, file, cb) => {
|
||||
const uploadPath = path.join(__dirname, '../public/uploads');
|
||||
try {
|
||||
await fs.mkdir(uploadPath, { recursive: true });
|
||||
cb(null, uploadPath);
|
||||
} catch (error) {
|
||||
cb(error);
|
||||
}
|
||||
},
|
||||
filename: (req, file, cb) => {
|
||||
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
|
||||
const ext = path.extname(file.originalname);
|
||||
cb(null, file.fieldname + '-' + uniqueSuffix + ext);
|
||||
}
|
||||
});
|
||||
|
||||
const upload = multer({
|
||||
storage: storage,
|
||||
limits: {
|
||||
fileSize: parseInt(process.env.MAX_FILE_SIZE) || 10 * 1024 * 1024, // 10MB
|
||||
files: 10
|
||||
},
|
||||
fileFilter: (req, file, cb) => {
|
||||
// Allow images only
|
||||
if (file.mimetype.startsWith('image/')) {
|
||||
cb(null, true);
|
||||
} else {
|
||||
cb(new Error('Only image files are allowed'), false);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Upload single image
|
||||
router.post('/upload', requireAuth, upload.single('image'), async (req, res) => {
|
||||
try {
|
||||
if (!req.file) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'No file uploaded'
|
||||
});
|
||||
}
|
||||
|
||||
const originalPath = req.file.path;
|
||||
const filename = req.file.filename;
|
||||
const nameWithoutExt = path.parse(filename).name;
|
||||
|
||||
// Create optimized versions
|
||||
const sizes = {
|
||||
thumbnail: { width: 300, height: 200 },
|
||||
medium: { width: 800, height: 600 },
|
||||
large: { width: 1200, height: 900 }
|
||||
};
|
||||
|
||||
const optimizedImages = {};
|
||||
|
||||
for (const [sizeName, dimensions] of Object.entries(sizes)) {
|
||||
const outputPath = path.join(
|
||||
path.dirname(originalPath),
|
||||
`${nameWithoutExt}-${sizeName}.webp`
|
||||
);
|
||||
|
||||
await sharp(originalPath)
|
||||
.resize(dimensions.width, dimensions.height, {
|
||||
fit: 'inside',
|
||||
withoutEnlargement: true
|
||||
})
|
||||
.webp({ quality: 85 })
|
||||
.toFile(outputPath);
|
||||
|
||||
optimizedImages[sizeName] = `/uploads/${path.basename(outputPath)}`;
|
||||
}
|
||||
|
||||
// Keep original as well (converted to webp for better compression)
|
||||
const originalWebpPath = path.join(
|
||||
path.dirname(originalPath),
|
||||
`${nameWithoutExt}-original.webp`
|
||||
);
|
||||
|
||||
await sharp(originalPath)
|
||||
.webp({ quality: 90 })
|
||||
.toFile(originalWebpPath);
|
||||
|
||||
optimizedImages.original = `/uploads/${path.basename(originalWebpPath)}`;
|
||||
|
||||
// Remove the original uploaded file
|
||||
await fs.unlink(originalPath);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Image uploaded and optimized successfully',
|
||||
images: optimizedImages,
|
||||
metadata: {
|
||||
originalName: req.file.originalname,
|
||||
mimeType: req.file.mimetype,
|
||||
size: req.file.size
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Image upload error:', error);
|
||||
|
||||
// Clean up files on error
|
||||
if (req.file && req.file.path) {
|
||||
try {
|
||||
await fs.unlink(req.file.path);
|
||||
} catch (unlinkError) {
|
||||
console.error('Error removing file:', unlinkError);
|
||||
}
|
||||
}
|
||||
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Error uploading image'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Upload multiple images
|
||||
router.post('/upload-multiple', requireAuth, upload.array('images', 10), async (req, res) => {
|
||||
try {
|
||||
if (!req.files || req.files.length === 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'No files uploaded'
|
||||
});
|
||||
}
|
||||
|
||||
const uploadedImages = [];
|
||||
|
||||
for (const file of req.files) {
|
||||
try {
|
||||
const originalPath = file.path;
|
||||
const filename = file.filename;
|
||||
const nameWithoutExt = path.parse(filename).name;
|
||||
|
||||
// Create optimized versions
|
||||
const sizes = {
|
||||
thumbnail: { width: 300, height: 200 },
|
||||
medium: { width: 800, height: 600 },
|
||||
large: { width: 1200, height: 900 }
|
||||
};
|
||||
|
||||
const optimizedImages = {};
|
||||
|
||||
for (const [sizeName, dimensions] of Object.entries(sizes)) {
|
||||
const outputPath = path.join(
|
||||
path.dirname(originalPath),
|
||||
`${nameWithoutExt}-${sizeName}.webp`
|
||||
);
|
||||
|
||||
await sharp(originalPath)
|
||||
.resize(dimensions.width, dimensions.height, {
|
||||
fit: 'inside',
|
||||
withoutEnlargement: true
|
||||
})
|
||||
.webp({ quality: 85 })
|
||||
.toFile(outputPath);
|
||||
|
||||
optimizedImages[sizeName] = `/uploads/${path.basename(outputPath)}`;
|
||||
}
|
||||
|
||||
// Original as webp
|
||||
const originalWebpPath = path.join(
|
||||
path.dirname(originalPath),
|
||||
`${nameWithoutExt}-original.webp`
|
||||
);
|
||||
|
||||
await sharp(originalPath)
|
||||
.webp({ quality: 90 })
|
||||
.toFile(originalWebpPath);
|
||||
|
||||
optimizedImages.original = `/uploads/${path.basename(originalWebpPath)}`;
|
||||
|
||||
uploadedImages.push({
|
||||
originalName: file.originalname,
|
||||
images: optimizedImages,
|
||||
metadata: {
|
||||
mimeType: file.mimetype,
|
||||
size: file.size
|
||||
}
|
||||
});
|
||||
|
||||
// Remove original file
|
||||
await fs.unlink(originalPath);
|
||||
} catch (fileError) {
|
||||
console.error(`Error processing file ${file.originalname}:`, fileError);
|
||||
// Clean up this file and continue with others
|
||||
try {
|
||||
await fs.unlink(file.path);
|
||||
} catch (unlinkError) {
|
||||
console.error('Error removing file:', unlinkError);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: `${uploadedImages.length} images uploaded and optimized successfully`,
|
||||
images: uploadedImages
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Multiple images upload error:', error);
|
||||
|
||||
// Clean up files on error
|
||||
if (req.files) {
|
||||
for (const file of req.files) {
|
||||
try {
|
||||
await fs.unlink(file.path);
|
||||
} catch (unlinkError) {
|
||||
console.error('Error removing file:', unlinkError);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Error uploading images'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Delete image
|
||||
router.delete('/:filename', requireAuth, async (req, res) => {
|
||||
try {
|
||||
const filename = req.params.filename;
|
||||
const uploadPath = path.join(__dirname, '../public/uploads');
|
||||
|
||||
// Security check - ensure filename doesn't contain path traversal
|
||||
if (filename.includes('..') || filename.includes('/') || filename.includes('\\')) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Invalid filename'
|
||||
});
|
||||
}
|
||||
|
||||
const filePath = path.join(uploadPath, filename);
|
||||
|
||||
try {
|
||||
await fs.access(filePath);
|
||||
await fs.unlink(filePath);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Image deleted successfully'
|
||||
});
|
||||
} catch (error) {
|
||||
if (error.code === 'ENOENT') {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Image not found'
|
||||
});
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Image deletion error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Error deleting image'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// List uploaded images
|
||||
router.get('/list', requireAuth, async (req, res) => {
|
||||
try {
|
||||
const uploadPath = path.join(__dirname, '../public/uploads');
|
||||
const page = parseInt(req.query.page) || 1;
|
||||
const limit = parseInt(req.query.limit) || 20;
|
||||
|
||||
const files = await fs.readdir(uploadPath);
|
||||
const imageFiles = files.filter(file =>
|
||||
/\.(jpg|jpeg|png|gif|webp)$/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) => {
|
||||
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
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(`Error getting stats for ${file}:`, error);
|
||||
return null;
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
const validImages = imagesWithStats.filter(img => img !== null);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
images: validImages,
|
||||
pagination: {
|
||||
current: page,
|
||||
total: totalPages,
|
||||
limit,
|
||||
totalItems: total,
|
||||
hasNext: page < totalPages,
|
||||
hasPrev: page > 1
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('List images error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Error listing images'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
196
.history/routes/portfolio_20251019160838.js
Normal file
196
.history/routes/portfolio_20251019160838.js
Normal file
@@ -0,0 +1,196 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const Portfolio = require('../models/Portfolio');
|
||||
|
||||
// Get all portfolio items
|
||||
router.get('/', async (req, res) => {
|
||||
try {
|
||||
const page = parseInt(req.query.page) || 1;
|
||||
const limit = parseInt(req.query.limit) || 12;
|
||||
const skip = (page - 1) * limit;
|
||||
const category = req.query.category;
|
||||
const search = req.query.search;
|
||||
const featured = req.query.featured;
|
||||
|
||||
// Build query
|
||||
let query = { isPublished: true };
|
||||
|
||||
if (category && category !== 'all') {
|
||||
query.category = category;
|
||||
}
|
||||
|
||||
if (featured === 'true') {
|
||||
query.featured = true;
|
||||
}
|
||||
|
||||
if (search) {
|
||||
query.$text = { $search: 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)
|
||||
]);
|
||||
|
||||
const totalPages = Math.ceil(total / limit);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
portfolio,
|
||||
pagination: {
|
||||
current: page,
|
||||
total: totalPages,
|
||||
limit,
|
||||
totalItems: total,
|
||||
hasNext: page < totalPages,
|
||||
hasPrev: page > 1
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Portfolio API error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Error fetching portfolio'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Get single portfolio item
|
||||
router.get('/:id', async (req, res) => {
|
||||
try {
|
||||
const portfolio = await Portfolio.findById(req.params.id);
|
||||
|
||||
if (!portfolio || !portfolio.isPublished) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Portfolio item not found'
|
||||
});
|
||||
}
|
||||
|
||||
// Increment view count
|
||||
portfolio.viewCount += 1;
|
||||
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);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
portfolio,
|
||||
relatedProjects
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Portfolio detail API error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Error fetching portfolio item'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Get portfolio categories
|
||||
router.get('/meta/categories', async (req, res) => {
|
||||
try {
|
||||
const categories = await Portfolio.distinct('category', { isPublished: true });
|
||||
|
||||
// Get count for each category
|
||||
const categoriesWithCount = await Promise.all(
|
||||
categories.map(async (category) => {
|
||||
const count = await Portfolio.countDocuments({
|
||||
category,
|
||||
isPublished: true
|
||||
});
|
||||
return { category, count };
|
||||
})
|
||||
);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
categories: categoriesWithCount
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Portfolio categories API error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Error fetching categories'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Like portfolio item
|
||||
router.post('/:id/like', async (req, res) => {
|
||||
try {
|
||||
const portfolio = await Portfolio.findById(req.params.id);
|
||||
|
||||
if (!portfolio || !portfolio.isPublished) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Portfolio item not found'
|
||||
});
|
||||
}
|
||||
|
||||
portfolio.likes += 1;
|
||||
await portfolio.save();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
likes: portfolio.likes
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Portfolio like API error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Error liking portfolio item'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Search portfolio
|
||||
router.get('/search/:term', async (req, res) => {
|
||||
try {
|
||||
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);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
portfolio,
|
||||
searchTerm,
|
||||
count: portfolio.length
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Portfolio search API error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Error searching portfolio'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
196
.history/routes/portfolio_20251019162544.js
Normal file
196
.history/routes/portfolio_20251019162544.js
Normal file
@@ -0,0 +1,196 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const Portfolio = require('../models/Portfolio');
|
||||
|
||||
// Get all portfolio items
|
||||
router.get('/', async (req, res) => {
|
||||
try {
|
||||
const page = parseInt(req.query.page) || 1;
|
||||
const limit = parseInt(req.query.limit) || 12;
|
||||
const skip = (page - 1) * limit;
|
||||
const category = req.query.category;
|
||||
const search = req.query.search;
|
||||
const featured = req.query.featured;
|
||||
|
||||
// Build query
|
||||
let query = { isPublished: true };
|
||||
|
||||
if (category && category !== 'all') {
|
||||
query.category = category;
|
||||
}
|
||||
|
||||
if (featured === 'true') {
|
||||
query.featured = true;
|
||||
}
|
||||
|
||||
if (search) {
|
||||
query.$text = { $search: 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)
|
||||
]);
|
||||
|
||||
const totalPages = Math.ceil(total / limit);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
portfolio,
|
||||
pagination: {
|
||||
current: page,
|
||||
total: totalPages,
|
||||
limit,
|
||||
totalItems: total,
|
||||
hasNext: page < totalPages,
|
||||
hasPrev: page > 1
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Portfolio API error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Error fetching portfolio'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Get single portfolio item
|
||||
router.get('/:id', async (req, res) => {
|
||||
try {
|
||||
const portfolio = await Portfolio.findById(req.params.id);
|
||||
|
||||
if (!portfolio || !portfolio.isPublished) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Portfolio item not found'
|
||||
});
|
||||
}
|
||||
|
||||
// Increment view count
|
||||
portfolio.viewCount += 1;
|
||||
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);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
portfolio,
|
||||
relatedProjects
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Portfolio detail API error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Error fetching portfolio item'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Get portfolio categories
|
||||
router.get('/meta/categories', async (req, res) => {
|
||||
try {
|
||||
const categories = await Portfolio.distinct('category', { isPublished: true });
|
||||
|
||||
// Get count for each category
|
||||
const categoriesWithCount = await Promise.all(
|
||||
categories.map(async (category) => {
|
||||
const count = await Portfolio.countDocuments({
|
||||
category,
|
||||
isPublished: true
|
||||
});
|
||||
return { category, count };
|
||||
})
|
||||
);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
categories: categoriesWithCount
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Portfolio categories API error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Error fetching categories'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Like portfolio item
|
||||
router.post('/:id/like', async (req, res) => {
|
||||
try {
|
||||
const portfolio = await Portfolio.findById(req.params.id);
|
||||
|
||||
if (!portfolio || !portfolio.isPublished) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Portfolio item not found'
|
||||
});
|
||||
}
|
||||
|
||||
portfolio.likes += 1;
|
||||
await portfolio.save();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
likes: portfolio.likes
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Portfolio like API error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Error liking portfolio item'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Search portfolio
|
||||
router.get('/search/:term', async (req, res) => {
|
||||
try {
|
||||
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);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
portfolio,
|
||||
searchTerm,
|
||||
count: portfolio.length
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Portfolio search API error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Error searching portfolio'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
141
.history/routes/services_20251019160852.js
Normal file
141
.history/routes/services_20251019160852.js
Normal file
@@ -0,0 +1,141 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const Service = require('../models/Service');
|
||||
const Portfolio = require('../models/Portfolio');
|
||||
|
||||
// Get all services
|
||||
router.get('/', async (req, res) => {
|
||||
try {
|
||||
const category = req.query.category;
|
||||
const featured = req.query.featured;
|
||||
|
||||
let query = { isActive: true };
|
||||
|
||||
if (category && category !== 'all') {
|
||||
query.category = category;
|
||||
}
|
||||
|
||||
if (featured === 'true') {
|
||||
query.featured = true;
|
||||
}
|
||||
|
||||
const services = await Service.find(query)
|
||||
.populate('portfolio', 'title images')
|
||||
.sort({ featured: -1, order: 1 });
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
services
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Services API error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Error fetching services'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Get single service
|
||||
router.get('/:id', async (req, res) => {
|
||||
try {
|
||||
const service = await Service.findById(req.params.id)
|
||||
.populate('portfolio', 'title shortDescription images category');
|
||||
|
||||
if (!service || !service.isActive) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Service not found'
|
||||
});
|
||||
}
|
||||
|
||||
// Get related services
|
||||
const relatedServices = await Service.find({
|
||||
_id: { $ne: service._id },
|
||||
category: service.category,
|
||||
isActive: true
|
||||
})
|
||||
.select('name shortDescription icon pricing')
|
||||
.limit(3);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
service,
|
||||
relatedServices
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Service detail API error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Error fetching service'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Get service categories
|
||||
router.get('/meta/categories', async (req, res) => {
|
||||
try {
|
||||
const categories = await Service.distinct('category', { isActive: true });
|
||||
|
||||
// Get count for each category
|
||||
const categoriesWithCount = await Promise.all(
|
||||
categories.map(async (category) => {
|
||||
const count = await Service.countDocuments({
|
||||
category,
|
||||
isActive: true
|
||||
});
|
||||
return { category, count };
|
||||
})
|
||||
);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
categories: categoriesWithCount
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Service categories API error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Error fetching categories'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Search services
|
||||
router.get('/search/:term', async (req, res) => {
|
||||
try {
|
||||
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);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
services,
|
||||
searchTerm,
|
||||
count: services.length
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Service search API error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Error searching services'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
141
.history/routes/services_20251019162544.js
Normal file
141
.history/routes/services_20251019162544.js
Normal file
@@ -0,0 +1,141 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const Service = require('../models/Service');
|
||||
const Portfolio = require('../models/Portfolio');
|
||||
|
||||
// Get all services
|
||||
router.get('/', async (req, res) => {
|
||||
try {
|
||||
const category = req.query.category;
|
||||
const featured = req.query.featured;
|
||||
|
||||
let query = { isActive: true };
|
||||
|
||||
if (category && category !== 'all') {
|
||||
query.category = category;
|
||||
}
|
||||
|
||||
if (featured === 'true') {
|
||||
query.featured = true;
|
||||
}
|
||||
|
||||
const services = await Service.find(query)
|
||||
.populate('portfolio', 'title images')
|
||||
.sort({ featured: -1, order: 1 });
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
services
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Services API error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Error fetching services'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Get single service
|
||||
router.get('/:id', async (req, res) => {
|
||||
try {
|
||||
const service = await Service.findById(req.params.id)
|
||||
.populate('portfolio', 'title shortDescription images category');
|
||||
|
||||
if (!service || !service.isActive) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Service not found'
|
||||
});
|
||||
}
|
||||
|
||||
// Get related services
|
||||
const relatedServices = await Service.find({
|
||||
_id: { $ne: service._id },
|
||||
category: service.category,
|
||||
isActive: true
|
||||
})
|
||||
.select('name shortDescription icon pricing')
|
||||
.limit(3);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
service,
|
||||
relatedServices
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Service detail API error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Error fetching service'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Get service categories
|
||||
router.get('/meta/categories', async (req, res) => {
|
||||
try {
|
||||
const categories = await Service.distinct('category', { isActive: true });
|
||||
|
||||
// Get count for each category
|
||||
const categoriesWithCount = await Promise.all(
|
||||
categories.map(async (category) => {
|
||||
const count = await Service.countDocuments({
|
||||
category,
|
||||
isActive: true
|
||||
});
|
||||
return { category, count };
|
||||
})
|
||||
);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
categories: categoriesWithCount
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Service categories API error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Error fetching categories'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Search services
|
||||
router.get('/search/:term', async (req, res) => {
|
||||
try {
|
||||
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);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
services,
|
||||
searchTerm,
|
||||
count: services.length
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Service search API error:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Error searching services'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
279
.history/scripts/build_20251019162023.js
Normal file
279
.history/scripts/build_20251019162023.js
Normal file
@@ -0,0 +1,279 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Build script for production deployment
|
||||
* Handles CSS compilation, asset optimization, and production setup
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { exec } = require('child_process');
|
||||
const { promisify } = require('util');
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
// Configuration
|
||||
const BUILD_DIR = path.join(__dirname, '..', 'dist');
|
||||
const PUBLIC_DIR = path.join(__dirname, '..', 'public');
|
||||
const VIEWS_DIR = path.join(__dirname, '..', 'views');
|
||||
|
||||
async function buildForProduction() {
|
||||
try {
|
||||
console.log('🏗️ Starting production build...');
|
||||
|
||||
// Create build directory
|
||||
await createBuildDirectory();
|
||||
|
||||
// Install production dependencies
|
||||
await installProductionDependencies();
|
||||
|
||||
// Optimize assets
|
||||
await optimizeAssets();
|
||||
|
||||
// Generate service worker with workbox
|
||||
await generateServiceWorker();
|
||||
|
||||
// Create production environment file
|
||||
await createProductionEnv();
|
||||
|
||||
// Copy necessary files
|
||||
await copyProductionFiles();
|
||||
|
||||
console.log('✅ Production build completed successfully!');
|
||||
console.log('📦 Build output available in:', BUILD_DIR);
|
||||
console.log('🚀 Ready for deployment!');
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Production build failed:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
async function createBuildDirectory() {
|
||||
try {
|
||||
if (fs.existsSync(BUILD_DIR)) {
|
||||
console.log('🗑️ Cleaning existing build directory...');
|
||||
await execAsync(`rm -rf ${BUILD_DIR}`);
|
||||
}
|
||||
|
||||
fs.mkdirSync(BUILD_DIR, { recursive: true });
|
||||
console.log('📁 Created build directory');
|
||||
} catch (error) {
|
||||
console.error('❌ Error creating build directory:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function installProductionDependencies() {
|
||||
try {
|
||||
console.log('📦 Installing production dependencies...');
|
||||
await execAsync('npm ci --only=production');
|
||||
console.log('✅ Production dependencies installed');
|
||||
} catch (error) {
|
||||
console.error('❌ Error installing dependencies:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function optimizeAssets() {
|
||||
try {
|
||||
console.log('🎨 Optimizing CSS and JavaScript...');
|
||||
|
||||
// Create optimized CSS
|
||||
const cssContent = fs.readFileSync(path.join(PUBLIC_DIR, 'css', 'main.css'), 'utf8');
|
||||
const optimizedCSS = await optimizeCSS(cssContent);
|
||||
|
||||
const buildCSSDir = path.join(BUILD_DIR, 'public', 'css');
|
||||
fs.mkdirSync(buildCSSDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(buildCSSDir, 'main.min.css'), optimizedCSS);
|
||||
|
||||
// Create optimized JavaScript
|
||||
const jsContent = fs.readFileSync(path.join(PUBLIC_DIR, 'js', 'main.js'), 'utf8');
|
||||
const optimizedJS = await optimizeJS(jsContent);
|
||||
|
||||
const buildJSDir = path.join(BUILD_DIR, 'public', 'js');
|
||||
fs.mkdirSync(buildJSDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(buildJSDir, 'main.min.js'), optimizedJS);
|
||||
|
||||
// Copy other assets
|
||||
await copyDirectory(path.join(PUBLIC_DIR, 'images'), path.join(BUILD_DIR, 'public', 'images'));
|
||||
|
||||
console.log('✅ Assets optimized');
|
||||
} catch (error) {
|
||||
console.error('❌ Error optimizing assets:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function optimizeCSS(css) {
|
||||
// Simple CSS minification (remove comments, extra whitespace)
|
||||
return css
|
||||
.replace(/\/\*[\s\S]*?\*\//g, '') // Remove comments
|
||||
.replace(/\s{2,}/g, ' ') // Replace multiple spaces with single space
|
||||
.replace(/;\s*}/g, '}') // Remove semicolon before closing brace
|
||||
.replace(/\s*{\s*/g, '{') // Remove spaces around opening brace
|
||||
.replace(/;\s*/g, ';') // Remove spaces after semicolons
|
||||
.replace(/,\s*/g, ',') // Remove spaces after commas
|
||||
.trim();
|
||||
}
|
||||
|
||||
async function optimizeJS(js) {
|
||||
// Simple JS minification (remove comments, extra whitespace)
|
||||
return js
|
||||
.replace(/\/\/.*$/gm, '') // Remove single-line comments
|
||||
.replace(/\/\*[\s\S]*?\*\//g, '') // Remove multi-line comments
|
||||
.replace(/\s{2,}/g, ' ') // Replace multiple spaces with single space
|
||||
.replace(/\n\s*/g, '') // Remove newlines and indentation
|
||||
.trim();
|
||||
}
|
||||
|
||||
async function generateServiceWorker() {
|
||||
try {
|
||||
console.log('⚙️ Generating optimized service worker...');
|
||||
|
||||
const swContent = fs.readFileSync(path.join(PUBLIC_DIR, 'sw.js'), 'utf8');
|
||||
const optimizedSW = await optimizeJS(swContent);
|
||||
|
||||
fs.writeFileSync(path.join(BUILD_DIR, 'public', 'sw.js'), optimizedSW);
|
||||
|
||||
// Copy manifest
|
||||
fs.copyFileSync(
|
||||
path.join(PUBLIC_DIR, 'manifest.json'),
|
||||
path.join(BUILD_DIR, 'public', 'manifest.json')
|
||||
);
|
||||
|
||||
console.log('✅ Service worker generated');
|
||||
} catch (error) {
|
||||
console.error('❌ Error generating service worker:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function createProductionEnv() {
|
||||
try {
|
||||
console.log('🔧 Creating production environment configuration...');
|
||||
|
||||
const prodEnv = `
|
||||
# Production Environment Configuration
|
||||
NODE_ENV=production
|
||||
PORT=3000
|
||||
|
||||
# Database
|
||||
MONGODB_URI=mongodb://localhost:27017/smartsoltech
|
||||
|
||||
# Security
|
||||
SESSION_SECRET=your-super-secret-session-key-change-this-in-production
|
||||
JWT_SECRET=your-super-secret-jwt-key-change-this-in-production
|
||||
|
||||
# File Upload
|
||||
UPLOAD_PATH=./uploads
|
||||
MAX_FILE_SIZE=10485760
|
||||
|
||||
# Email Configuration
|
||||
SMTP_HOST=smtp.gmail.com
|
||||
SMTP_PORT=587
|
||||
SMTP_USER=your-email@gmail.com
|
||||
SMTP_PASS=your-app-password
|
||||
|
||||
# Telegram Bot (Optional)
|
||||
TELEGRAM_BOT_TOKEN=your-telegram-bot-token
|
||||
|
||||
# Admin Account
|
||||
ADMIN_EMAIL=admin@smartsoltech.kr
|
||||
ADMIN_PASSWORD=change-this-password
|
||||
|
||||
# Security Headers
|
||||
RATE_LIMIT_WINDOW_MS=900000
|
||||
RATE_LIMIT_MAX_REQUESTS=100
|
||||
`.trim();
|
||||
|
||||
fs.writeFileSync(path.join(BUILD_DIR, '.env.production'), prodEnv);
|
||||
console.log('✅ Production environment file created');
|
||||
} catch (error) {
|
||||
console.error('❌ Error creating production environment:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function copyProductionFiles() {
|
||||
try {
|
||||
console.log('📋 Copying production files...');
|
||||
|
||||
// Copy main application files
|
||||
const filesToCopy = [
|
||||
'server.js',
|
||||
'package.json',
|
||||
'package-lock.json'
|
||||
];
|
||||
|
||||
for (const file of filesToCopy) {
|
||||
fs.copyFileSync(
|
||||
path.join(__dirname, '..', file),
|
||||
path.join(BUILD_DIR, file)
|
||||
);
|
||||
}
|
||||
|
||||
// Copy directories
|
||||
await copyDirectory(
|
||||
path.join(__dirname, '..', 'models'),
|
||||
path.join(BUILD_DIR, 'models')
|
||||
);
|
||||
|
||||
await copyDirectory(
|
||||
path.join(__dirname, '..', 'routes'),
|
||||
path.join(BUILD_DIR, 'routes')
|
||||
);
|
||||
|
||||
await copyDirectory(
|
||||
path.join(__dirname, '..', 'views'),
|
||||
path.join(BUILD_DIR, 'views')
|
||||
);
|
||||
|
||||
await copyDirectory(
|
||||
path.join(__dirname, '..', 'middleware'),
|
||||
path.join(BUILD_DIR, 'middleware')
|
||||
);
|
||||
|
||||
// Create uploads directory
|
||||
fs.mkdirSync(path.join(BUILD_DIR, 'uploads'), { recursive: true });
|
||||
|
||||
console.log('✅ Production files copied');
|
||||
} catch (error) {
|
||||
console.error('❌ Error copying production files:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function copyDirectory(src, dest) {
|
||||
try {
|
||||
if (!fs.existsSync(src)) {
|
||||
return;
|
||||
}
|
||||
|
||||
fs.mkdirSync(dest, { recursive: true });
|
||||
|
||||
const items = fs.readdirSync(src);
|
||||
|
||||
for (const item of items) {
|
||||
const srcPath = path.join(src, item);
|
||||
const destPath = path.join(dest, item);
|
||||
|
||||
const stat = fs.statSync(srcPath);
|
||||
|
||||
if (stat.isDirectory()) {
|
||||
await copyDirectory(srcPath, destPath);
|
||||
} else {
|
||||
fs.copyFileSync(srcPath, destPath);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`❌ Error copying directory ${src}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
buildForProduction();
|
||||
}
|
||||
|
||||
module.exports = { buildForProduction };
|
||||
279
.history/scripts/build_20251019162545.js
Normal file
279
.history/scripts/build_20251019162545.js
Normal file
@@ -0,0 +1,279 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Build script for production deployment
|
||||
* Handles CSS compilation, asset optimization, and production setup
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { exec } = require('child_process');
|
||||
const { promisify } = require('util');
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
// Configuration
|
||||
const BUILD_DIR = path.join(__dirname, '..', 'dist');
|
||||
const PUBLIC_DIR = path.join(__dirname, '..', 'public');
|
||||
const VIEWS_DIR = path.join(__dirname, '..', 'views');
|
||||
|
||||
async function buildForProduction() {
|
||||
try {
|
||||
console.log('🏗️ Starting production build...');
|
||||
|
||||
// Create build directory
|
||||
await createBuildDirectory();
|
||||
|
||||
// Install production dependencies
|
||||
await installProductionDependencies();
|
||||
|
||||
// Optimize assets
|
||||
await optimizeAssets();
|
||||
|
||||
// Generate service worker with workbox
|
||||
await generateServiceWorker();
|
||||
|
||||
// Create production environment file
|
||||
await createProductionEnv();
|
||||
|
||||
// Copy necessary files
|
||||
await copyProductionFiles();
|
||||
|
||||
console.log('✅ Production build completed successfully!');
|
||||
console.log('📦 Build output available in:', BUILD_DIR);
|
||||
console.log('🚀 Ready for deployment!');
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Production build failed:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
async function createBuildDirectory() {
|
||||
try {
|
||||
if (fs.existsSync(BUILD_DIR)) {
|
||||
console.log('🗑️ Cleaning existing build directory...');
|
||||
await execAsync(`rm -rf ${BUILD_DIR}`);
|
||||
}
|
||||
|
||||
fs.mkdirSync(BUILD_DIR, { recursive: true });
|
||||
console.log('📁 Created build directory');
|
||||
} catch (error) {
|
||||
console.error('❌ Error creating build directory:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function installProductionDependencies() {
|
||||
try {
|
||||
console.log('📦 Installing production dependencies...');
|
||||
await execAsync('npm ci --only=production');
|
||||
console.log('✅ Production dependencies installed');
|
||||
} catch (error) {
|
||||
console.error('❌ Error installing dependencies:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function optimizeAssets() {
|
||||
try {
|
||||
console.log('🎨 Optimizing CSS and JavaScript...');
|
||||
|
||||
// Create optimized CSS
|
||||
const cssContent = fs.readFileSync(path.join(PUBLIC_DIR, 'css', 'main.css'), 'utf8');
|
||||
const optimizedCSS = await optimizeCSS(cssContent);
|
||||
|
||||
const buildCSSDir = path.join(BUILD_DIR, 'public', 'css');
|
||||
fs.mkdirSync(buildCSSDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(buildCSSDir, 'main.min.css'), optimizedCSS);
|
||||
|
||||
// Create optimized JavaScript
|
||||
const jsContent = fs.readFileSync(path.join(PUBLIC_DIR, 'js', 'main.js'), 'utf8');
|
||||
const optimizedJS = await optimizeJS(jsContent);
|
||||
|
||||
const buildJSDir = path.join(BUILD_DIR, 'public', 'js');
|
||||
fs.mkdirSync(buildJSDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(buildJSDir, 'main.min.js'), optimizedJS);
|
||||
|
||||
// Copy other assets
|
||||
await copyDirectory(path.join(PUBLIC_DIR, 'images'), path.join(BUILD_DIR, 'public', 'images'));
|
||||
|
||||
console.log('✅ Assets optimized');
|
||||
} catch (error) {
|
||||
console.error('❌ Error optimizing assets:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function optimizeCSS(css) {
|
||||
// Simple CSS minification (remove comments, extra whitespace)
|
||||
return css
|
||||
.replace(/\/\*[\s\S]*?\*\//g, '') // Remove comments
|
||||
.replace(/\s{2,}/g, ' ') // Replace multiple spaces with single space
|
||||
.replace(/;\s*}/g, '}') // Remove semicolon before closing brace
|
||||
.replace(/\s*{\s*/g, '{') // Remove spaces around opening brace
|
||||
.replace(/;\s*/g, ';') // Remove spaces after semicolons
|
||||
.replace(/,\s*/g, ',') // Remove spaces after commas
|
||||
.trim();
|
||||
}
|
||||
|
||||
async function optimizeJS(js) {
|
||||
// Simple JS minification (remove comments, extra whitespace)
|
||||
return js
|
||||
.replace(/\/\/.*$/gm, '') // Remove single-line comments
|
||||
.replace(/\/\*[\s\S]*?\*\//g, '') // Remove multi-line comments
|
||||
.replace(/\s{2,}/g, ' ') // Replace multiple spaces with single space
|
||||
.replace(/\n\s*/g, '') // Remove newlines and indentation
|
||||
.trim();
|
||||
}
|
||||
|
||||
async function generateServiceWorker() {
|
||||
try {
|
||||
console.log('⚙️ Generating optimized service worker...');
|
||||
|
||||
const swContent = fs.readFileSync(path.join(PUBLIC_DIR, 'sw.js'), 'utf8');
|
||||
const optimizedSW = await optimizeJS(swContent);
|
||||
|
||||
fs.writeFileSync(path.join(BUILD_DIR, 'public', 'sw.js'), optimizedSW);
|
||||
|
||||
// Copy manifest
|
||||
fs.copyFileSync(
|
||||
path.join(PUBLIC_DIR, 'manifest.json'),
|
||||
path.join(BUILD_DIR, 'public', 'manifest.json')
|
||||
);
|
||||
|
||||
console.log('✅ Service worker generated');
|
||||
} catch (error) {
|
||||
console.error('❌ Error generating service worker:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function createProductionEnv() {
|
||||
try {
|
||||
console.log('🔧 Creating production environment configuration...');
|
||||
|
||||
const prodEnv = `
|
||||
# Production Environment Configuration
|
||||
NODE_ENV=production
|
||||
PORT=3000
|
||||
|
||||
# Database
|
||||
MONGODB_URI=mongodb://localhost:27017/smartsoltech
|
||||
|
||||
# Security
|
||||
SESSION_SECRET=your-super-secret-session-key-change-this-in-production
|
||||
JWT_SECRET=your-super-secret-jwt-key-change-this-in-production
|
||||
|
||||
# File Upload
|
||||
UPLOAD_PATH=./uploads
|
||||
MAX_FILE_SIZE=10485760
|
||||
|
||||
# Email Configuration
|
||||
SMTP_HOST=smtp.gmail.com
|
||||
SMTP_PORT=587
|
||||
SMTP_USER=your-email@gmail.com
|
||||
SMTP_PASS=your-app-password
|
||||
|
||||
# Telegram Bot (Optional)
|
||||
TELEGRAM_BOT_TOKEN=your-telegram-bot-token
|
||||
|
||||
# Admin Account
|
||||
ADMIN_EMAIL=admin@smartsoltech.kr
|
||||
ADMIN_PASSWORD=change-this-password
|
||||
|
||||
# Security Headers
|
||||
RATE_LIMIT_WINDOW_MS=900000
|
||||
RATE_LIMIT_MAX_REQUESTS=100
|
||||
`.trim();
|
||||
|
||||
fs.writeFileSync(path.join(BUILD_DIR, '.env.production'), prodEnv);
|
||||
console.log('✅ Production environment file created');
|
||||
} catch (error) {
|
||||
console.error('❌ Error creating production environment:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function copyProductionFiles() {
|
||||
try {
|
||||
console.log('📋 Copying production files...');
|
||||
|
||||
// Copy main application files
|
||||
const filesToCopy = [
|
||||
'server.js',
|
||||
'package.json',
|
||||
'package-lock.json'
|
||||
];
|
||||
|
||||
for (const file of filesToCopy) {
|
||||
fs.copyFileSync(
|
||||
path.join(__dirname, '..', file),
|
||||
path.join(BUILD_DIR, file)
|
||||
);
|
||||
}
|
||||
|
||||
// Copy directories
|
||||
await copyDirectory(
|
||||
path.join(__dirname, '..', 'models'),
|
||||
path.join(BUILD_DIR, 'models')
|
||||
);
|
||||
|
||||
await copyDirectory(
|
||||
path.join(__dirname, '..', 'routes'),
|
||||
path.join(BUILD_DIR, 'routes')
|
||||
);
|
||||
|
||||
await copyDirectory(
|
||||
path.join(__dirname, '..', 'views'),
|
||||
path.join(BUILD_DIR, 'views')
|
||||
);
|
||||
|
||||
await copyDirectory(
|
||||
path.join(__dirname, '..', 'middleware'),
|
||||
path.join(BUILD_DIR, 'middleware')
|
||||
);
|
||||
|
||||
// Create uploads directory
|
||||
fs.mkdirSync(path.join(BUILD_DIR, 'uploads'), { recursive: true });
|
||||
|
||||
console.log('✅ Production files copied');
|
||||
} catch (error) {
|
||||
console.error('❌ Error copying production files:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function copyDirectory(src, dest) {
|
||||
try {
|
||||
if (!fs.existsSync(src)) {
|
||||
return;
|
||||
}
|
||||
|
||||
fs.mkdirSync(dest, { recursive: true });
|
||||
|
||||
const items = fs.readdirSync(src);
|
||||
|
||||
for (const item of items) {
|
||||
const srcPath = path.join(src, item);
|
||||
const destPath = path.join(dest, item);
|
||||
|
||||
const stat = fs.statSync(srcPath);
|
||||
|
||||
if (stat.isDirectory()) {
|
||||
await copyDirectory(srcPath, destPath);
|
||||
} else {
|
||||
fs.copyFileSync(srcPath, destPath);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`❌ Error copying directory ${src}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
buildForProduction();
|
||||
}
|
||||
|
||||
module.exports = { buildForProduction };
|
||||
73
.history/scripts/dev_20251019161952.js
Normal file
73
.history/scripts/dev_20251019161952.js
Normal file
@@ -0,0 +1,73 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Development server with hot reload for SmartSolTech
|
||||
* Uses nodemon for automatic server restart on file changes
|
||||
*/
|
||||
|
||||
const path = require('path');
|
||||
const { spawn } = require('child_process');
|
||||
|
||||
// Configuration
|
||||
const SERVER_FILE = path.join(__dirname, '..', 'server.js');
|
||||
const NODEMON_CONFIG = {
|
||||
script: SERVER_FILE,
|
||||
ext: 'js,json,ejs',
|
||||
ignore: ['node_modules/', 'public/', '.git/'],
|
||||
watch: ['models/', 'routes/', 'views/', 'server.js'],
|
||||
env: {
|
||||
NODE_ENV: 'development',
|
||||
DEBUG: 'app:*'
|
||||
},
|
||||
delay: 1000
|
||||
};
|
||||
|
||||
function startDevelopmentServer() {
|
||||
console.log('🚀 Starting SmartSolTech development server...');
|
||||
console.log('📁 Watching files for changes...');
|
||||
console.log('🔄 Server will automatically restart on file changes');
|
||||
console.log('');
|
||||
|
||||
const nodemonArgs = [
|
||||
'--script', NODEMON_CONFIG.script,
|
||||
'--ext', NODEMON_CONFIG.ext,
|
||||
'--ignore', NODEMON_CONFIG.ignore.join(','),
|
||||
'--watch', NODEMON_CONFIG.watch.join(','),
|
||||
'--delay', NODEMON_CONFIG.delay.toString()
|
||||
];
|
||||
|
||||
const nodemon = spawn('npx', ['nodemon', ...nodemonArgs], {
|
||||
stdio: 'inherit',
|
||||
env: {
|
||||
...process.env,
|
||||
...NODEMON_CONFIG.env
|
||||
}
|
||||
});
|
||||
|
||||
nodemon.on('close', (code) => {
|
||||
console.log(`\n🛑 Development server exited with code ${code}`);
|
||||
process.exit(code);
|
||||
});
|
||||
|
||||
nodemon.on('error', (error) => {
|
||||
console.error('❌ Error starting development server:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
// Handle graceful shutdown
|
||||
process.on('SIGINT', () => {
|
||||
console.log('\n🛑 Shutting down development server...');
|
||||
nodemon.kill('SIGINT');
|
||||
});
|
||||
|
||||
process.on('SIGTERM', () => {
|
||||
console.log('\n🛑 Shutting down development server...');
|
||||
nodemon.kill('SIGTERM');
|
||||
});
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
startDevelopmentServer();
|
||||
}
|
||||
|
||||
module.exports = { startDevelopmentServer };
|
||||
73
.history/scripts/dev_20251019162545.js
Normal file
73
.history/scripts/dev_20251019162545.js
Normal file
@@ -0,0 +1,73 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Development server with hot reload for SmartSolTech
|
||||
* Uses nodemon for automatic server restart on file changes
|
||||
*/
|
||||
|
||||
const path = require('path');
|
||||
const { spawn } = require('child_process');
|
||||
|
||||
// Configuration
|
||||
const SERVER_FILE = path.join(__dirname, '..', 'server.js');
|
||||
const NODEMON_CONFIG = {
|
||||
script: SERVER_FILE,
|
||||
ext: 'js,json,ejs',
|
||||
ignore: ['node_modules/', 'public/', '.git/'],
|
||||
watch: ['models/', 'routes/', 'views/', 'server.js'],
|
||||
env: {
|
||||
NODE_ENV: 'development',
|
||||
DEBUG: 'app:*'
|
||||
},
|
||||
delay: 1000
|
||||
};
|
||||
|
||||
function startDevelopmentServer() {
|
||||
console.log('🚀 Starting SmartSolTech development server...');
|
||||
console.log('📁 Watching files for changes...');
|
||||
console.log('🔄 Server will automatically restart on file changes');
|
||||
console.log('');
|
||||
|
||||
const nodemonArgs = [
|
||||
'--script', NODEMON_CONFIG.script,
|
||||
'--ext', NODEMON_CONFIG.ext,
|
||||
'--ignore', NODEMON_CONFIG.ignore.join(','),
|
||||
'--watch', NODEMON_CONFIG.watch.join(','),
|
||||
'--delay', NODEMON_CONFIG.delay.toString()
|
||||
];
|
||||
|
||||
const nodemon = spawn('npx', ['nodemon', ...nodemonArgs], {
|
||||
stdio: 'inherit',
|
||||
env: {
|
||||
...process.env,
|
||||
...NODEMON_CONFIG.env
|
||||
}
|
||||
});
|
||||
|
||||
nodemon.on('close', (code) => {
|
||||
console.log(`\n🛑 Development server exited with code ${code}`);
|
||||
process.exit(code);
|
||||
});
|
||||
|
||||
nodemon.on('error', (error) => {
|
||||
console.error('❌ Error starting development server:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
// Handle graceful shutdown
|
||||
process.on('SIGINT', () => {
|
||||
console.log('\n🛑 Shutting down development server...');
|
||||
nodemon.kill('SIGINT');
|
||||
});
|
||||
|
||||
process.on('SIGTERM', () => {
|
||||
console.log('\n🛑 Shutting down development server...');
|
||||
nodemon.kill('SIGTERM');
|
||||
});
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
startDevelopmentServer();
|
||||
}
|
||||
|
||||
module.exports = { startDevelopmentServer };
|
||||
498
.history/scripts/init-db_20251019161840.js
Normal file
498
.history/scripts/init-db_20251019161840.js
Normal file
@@ -0,0 +1,498 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Database initialization script for SmartSolTech
|
||||
* Creates initial admin user and sample data
|
||||
*/
|
||||
|
||||
const mongoose = require('mongoose');
|
||||
const bcrypt = require('bcryptjs');
|
||||
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');
|
||||
|
||||
// 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');
|
||||
|
||||
// Create admin user
|
||||
await createAdminUser();
|
||||
|
||||
// Create sample services
|
||||
await createSampleServices();
|
||||
|
||||
// Create sample portfolio items
|
||||
await createSamplePortfolio();
|
||||
|
||||
// Create site settings
|
||||
await createSiteSettings();
|
||||
|
||||
console.log('🎉 Database initialization completed successfully!');
|
||||
console.log(`📧 Admin login: ${ADMIN_EMAIL}`);
|
||||
console.log(`🔑 Admin password: ${ADMIN_PASSWORD}`);
|
||||
console.log('⚠️ Please change the admin password after first login!');
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Database initialization failed:', error);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
await mongoose.connection.close();
|
||||
console.log('🔌 Database connection closed');
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
async function createAdminUser() {
|
||||
try {
|
||||
const existingAdmin = await User.findOne({ email: ADMIN_EMAIL });
|
||||
|
||||
if (existingAdmin) {
|
||||
console.log('👤 Admin user already exists, skipping...');
|
||||
return;
|
||||
}
|
||||
|
||||
const adminUser = new User({
|
||||
name: 'Administrator',
|
||||
email: ADMIN_EMAIL,
|
||||
password: ADMIN_PASSWORD,
|
||||
role: 'admin',
|
||||
isActive: true
|
||||
});
|
||||
|
||||
await adminUser.save();
|
||||
console.log('✅ Admin user created successfully');
|
||||
} catch (error) {
|
||||
console.error('❌ Error creating admin user:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function createSampleServices() {
|
||||
try {
|
||||
const existingServices = await Service.countDocuments();
|
||||
|
||||
if (existingServices > 0) {
|
||||
console.log('🛠️ Services already exist, skipping...');
|
||||
return;
|
||||
}
|
||||
|
||||
const services = [
|
||||
{
|
||||
name: '웹 개발',
|
||||
description: '현대적이고 반응형인 웹사이트와 웹 애플리케이션을 개발합니다. React, Node.js, MongoDB 등 최신 기술 스택을 활용하여 성능과 사용자 경험을 최적화합니다.',
|
||||
shortDescription: '현대적이고 반응형 웹사이트 및 웹 애플리케이션 개발',
|
||||
icon: 'fas fa-code',
|
||||
category: 'development',
|
||||
features: [
|
||||
{ name: '반응형 디자인', description: '모든 디바이스에서 완벽하게 작동', included: true },
|
||||
{ name: 'SEO 최적화', description: '검색엔진 최적화로 더 많은 방문자 유치', included: true },
|
||||
{ name: '성능 최적화', description: '빠른 로딩 속도와 원활한 사용자 경험', included: true },
|
||||
{ name: '보안 강화', description: '최신 보안 기술로 안전한 웹사이트', included: true },
|
||||
{ name: '유지보수', description: '6개월 무료 유지보수 지원', included: true }
|
||||
],
|
||||
pricing: {
|
||||
basePrice: 500000,
|
||||
currency: 'KRW',
|
||||
priceType: 'project',
|
||||
priceRange: { min: 500000, max: 5000000 }
|
||||
},
|
||||
estimatedTime: { min: 2, max: 8, unit: 'weeks' },
|
||||
featured: true,
|
||||
isActive: true,
|
||||
order: 1,
|
||||
tags: ['React', 'Node.js', 'MongoDB', 'Express', 'JavaScript', 'HTML', 'CSS']
|
||||
},
|
||||
{
|
||||
name: '모바일 앱 개발',
|
||||
description: 'iOS와 Android 플랫폼을 위한 네이티브 및 크로스플랫폼 모바일 애플리케이션을 개발합니다. React Native, Flutter 등을 활용하여 효율적인 개발을 진행합니다.',
|
||||
shortDescription: 'iOS/Android 네이티브 및 크로스플랫폼 앱 개발',
|
||||
icon: 'fas fa-mobile-alt',
|
||||
category: 'development',
|
||||
features: [
|
||||
{ name: '크로스플랫폼 개발', description: 'iOS와 Android 동시 지원', included: true },
|
||||
{ name: '네이티브 성능', description: '최적화된 성능과 사용자 경험', included: true },
|
||||
{ name: '푸시 알림', description: '실시간 푸시 알림 시스템', included: true },
|
||||
{ name: '오프라인 지원', description: '인터넷 연결 없이도 기본 기능 사용 가능', included: false },
|
||||
{ name: '앱스토어 등록', description: '앱스토어 및 플레이스토어 등록 지원', included: true }
|
||||
],
|
||||
pricing: {
|
||||
basePrice: 800000,
|
||||
currency: 'KRW',
|
||||
priceType: 'project',
|
||||
priceRange: { min: 800000, max: 8000000 }
|
||||
},
|
||||
estimatedTime: { min: 4, max: 16, unit: 'weeks' },
|
||||
featured: true,
|
||||
isActive: true,
|
||||
order: 2,
|
||||
tags: ['React Native', 'Flutter', 'iOS', 'Android', 'Swift', 'Kotlin', 'JavaScript']
|
||||
},
|
||||
{
|
||||
name: 'UI/UX 디자인',
|
||||
description: '사용자 중심의 직관적이고 아름다운 인터페이스를 디자인합니다. 사용자 경험 연구와 프로토타이핑을 통해 최적의 디자인 솔루션을 제공합니다.',
|
||||
shortDescription: '사용자 중심의 직관적이고 아름다운 인터페이스 디자인',
|
||||
icon: 'fas fa-palette',
|
||||
category: 'design',
|
||||
features: [
|
||||
{ name: '사용자 경험 연구', description: '타겟 사용자 분석 및 페르소나 설정', included: true },
|
||||
{ name: '와이어프레임', description: '정보 구조와 레이아웃 설계', included: true },
|
||||
{ name: '프로토타이핑', description: '인터랙티브 프로토타입 제작', included: true },
|
||||
{ name: '비주얼 디자인', description: '브랜드에 맞는 시각적 디자인', included: true },
|
||||
{ name: '디자인 시스템', description: '일관된 디자인을 위한 가이드라인', included: false }
|
||||
],
|
||||
pricing: {
|
||||
basePrice: 300000,
|
||||
currency: 'KRW',
|
||||
priceType: 'project',
|
||||
priceRange: { min: 300000, max: 2000000 }
|
||||
},
|
||||
estimatedTime: { min: 1, max: 6, unit: 'weeks' },
|
||||
featured: true,
|
||||
isActive: true,
|
||||
order: 3,
|
||||
tags: ['Figma', 'Sketch', 'Adobe XD', 'Photoshop', 'Illustrator', 'Prototyping']
|
||||
},
|
||||
{
|
||||
name: '디지털 마케팅',
|
||||
description: 'SEO, 소셜미디어 마케팅, 온라인 광고를 통해 디지털 마케팅 전략을 수립하고 실행합니다. 데이터 분석을 통한 지속적인 최적화를 제공합니다.',
|
||||
shortDescription: 'SEO, 소셜미디어, 온라인 광고를 통한 디지털 마케팅',
|
||||
icon: 'fas fa-chart-line',
|
||||
category: 'marketing',
|
||||
features: [
|
||||
{ name: 'SEO 최적화', description: '검색엔진 상위 노출을 위한 최적화', included: true },
|
||||
{ name: '소셜미디어 관리', description: '페이스북, 인스타그램, 유튜브 관리', included: true },
|
||||
{ name: '구글 광고', description: '구글 애즈를 통한 타겟 광고', included: true },
|
||||
{ name: '콘텐츠 마케팅', description: '블로그 및 콘텐츠 제작', included: false },
|
||||
{ name: '분석 및 리포팅', description: '마케팅 성과 분석 및 보고서', included: true }
|
||||
],
|
||||
pricing: {
|
||||
basePrice: 200000,
|
||||
currency: 'KRW',
|
||||
priceType: 'project',
|
||||
priceRange: { min: 200000, max: 1500000 }
|
||||
},
|
||||
estimatedTime: { min: 2, max: 12, unit: 'weeks' },
|
||||
featured: true,
|
||||
isActive: true,
|
||||
order: 4,
|
||||
tags: ['SEO', 'Google Ads', 'Facebook Ads', 'Analytics', 'Social Media', 'Content Marketing']
|
||||
},
|
||||
{
|
||||
name: '브랜딩',
|
||||
description: '기업의 정체성을 반영하는 로고, 브랜드 가이드라인, 마케팅 자료를 디자인합니다. 일관된 브랜드 이미지로 브랜드 가치를 높입니다.',
|
||||
shortDescription: '로고, 브랜드 가이드라인, 마케팅 자료 디자인',
|
||||
icon: 'fas fa-copyright',
|
||||
category: 'design',
|
||||
features: [
|
||||
{ name: '로고 디자인', description: '기업 정체성을 반영한 로고 제작', included: true },
|
||||
{ name: '브랜드 가이드라인', description: '색상, 폰트, 사용법 가이드', included: true },
|
||||
{ name: '명함 디자인', description: '브랜드에 맞는 명함 디자인', included: true },
|
||||
{ name: '브로슈어 디자인', description: '회사 소개 브로슈어 제작', included: false },
|
||||
{ name: '웹 브랜딩', description: '웹사이트 브랜딩 요소 적용', included: false }
|
||||
],
|
||||
pricing: {
|
||||
basePrice: 400000,
|
||||
currency: 'KRW',
|
||||
priceType: 'project',
|
||||
priceRange: { min: 400000, max: 2500000 }
|
||||
},
|
||||
estimatedTime: { min: 2, max: 8, unit: 'weeks' },
|
||||
featured: false,
|
||||
isActive: true,
|
||||
order: 5,
|
||||
tags: ['Logo Design', 'Brand Identity', 'Graphic Design', 'Corporate Design']
|
||||
},
|
||||
{
|
||||
name: '기술 컨설팅',
|
||||
description: '기업의 디지털 전환과 기술 도입을 위한 전문적인 컨설팅을 제공합니다. 기술 아키텍처 설계부터 구현 전략까지 포괄적으로 지원합니다.',
|
||||
shortDescription: '디지털 전환 및 기술 도입을 위한 전문 컨설팅',
|
||||
icon: 'fas fa-lightbulb',
|
||||
category: 'consulting',
|
||||
features: [
|
||||
{ name: '기술 분석', description: '현재 기술 스택 분석 및 개선안 제시', included: true },
|
||||
{ name: '아키텍처 설계', description: '확장 가능한 시스템 아키텍처 설계', included: true },
|
||||
{ name: '개발 프로세스 개선', description: '효율적인 개발 워크플로우 구축', included: true },
|
||||
{ name: '팀 교육', description: '개발팀 대상 기술 교육', included: false },
|
||||
{ name: '지속적인 지원', description: '프로젝트 완료 후 지속적인 기술 지원', included: false }
|
||||
],
|
||||
pricing: {
|
||||
basePrice: 150000,
|
||||
currency: 'KRW',
|
||||
priceType: 'hourly',
|
||||
priceRange: { min: 150000, max: 500000 }
|
||||
},
|
||||
estimatedTime: { min: 1, max: 4, unit: 'weeks' },
|
||||
featured: false,
|
||||
isActive: true,
|
||||
order: 6,
|
||||
tags: ['Architecture', 'Strategy', 'Process', 'Training', 'Consultation']
|
||||
}
|
||||
];
|
||||
|
||||
await Service.insertMany(services);
|
||||
console.log('✅ Sample services created successfully');
|
||||
} catch (error) {
|
||||
console.error('❌ Error creating sample services:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function createSamplePortfolio() {
|
||||
try {
|
||||
const existingPortfolio = await Portfolio.countDocuments();
|
||||
|
||||
if (existingPortfolio > 0) {
|
||||
console.log('🎨 Portfolio items already exist, skipping...');
|
||||
return;
|
||||
}
|
||||
|
||||
const portfolioItems = [
|
||||
{
|
||||
title: 'E-commerce 플랫폼',
|
||||
description: '현대적인 온라인 쇼핑몰 플랫폼을 개발했습니다. React와 Node.js를 활용하여 높은 성능과 사용자 경험을 제공하며, 결제 시스템과 재고 관리 기능을 포함합니다.',
|
||||
shortDescription: '반응형 온라인 쇼핑몰 플랫폼 개발',
|
||||
category: 'web-development',
|
||||
technologies: ['React', 'Node.js', 'MongoDB', 'Express', 'Stripe', 'AWS'],
|
||||
images: [
|
||||
{ url: '/images/portfolio/ecommerce-1.jpg', alt: 'E-commerce 메인페이지', isPrimary: true },
|
||||
{ url: '/images/portfolio/ecommerce-2.jpg', alt: '상품 상세페이지', isPrimary: false },
|
||||
{ url: '/images/portfolio/ecommerce-3.jpg', alt: '장바구니 페이지', isPrimary: false }
|
||||
],
|
||||
clientName: '패션 브랜드 ABC',
|
||||
projectUrl: 'https://example-ecommerce.com',
|
||||
status: 'completed',
|
||||
featured: true,
|
||||
publishedAt: new Date('2024-01-15'),
|
||||
completedAt: new Date('2024-01-10'),
|
||||
isPublished: true,
|
||||
viewCount: 150,
|
||||
likes: 25,
|
||||
order: 1,
|
||||
seo: {
|
||||
metaTitle: 'E-commerce 플랫폼 개발 프로젝트',
|
||||
metaDescription: '현대적인 온라인 쇼핑몰 플랫폼 개발 사례',
|
||||
keywords: ['E-commerce', 'React', 'Node.js', '온라인쇼핑몰']
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '모바일 피트니스 앱',
|
||||
description: 'React Native를 사용하여 크로스플랫폼 피트니스 애플리케이션을 개발했습니다. 운동 계획, 칼로리 추적, 소셜 기능을 포함하여 사용자들의 건강한 라이프스타일을 지원합니다.',
|
||||
shortDescription: '건강 관리를 위한 크로스플랫폼 모바일 앱',
|
||||
category: 'mobile-app',
|
||||
technologies: ['React Native', 'Redux', 'Firebase', 'Node.js', 'PostgreSQL'],
|
||||
images: [
|
||||
{ url: '/images/portfolio/fitness-1.jpg', alt: '피트니스 앱 메인화면', isPrimary: true },
|
||||
{ url: '/images/portfolio/fitness-2.jpg', alt: '운동 계획 화면', isPrimary: false },
|
||||
{ url: '/images/portfolio/fitness-3.jpg', alt: '통계 화면', isPrimary: false }
|
||||
],
|
||||
clientName: '헬스케어 스타트업 FIT',
|
||||
status: 'completed',
|
||||
featured: true,
|
||||
publishedAt: new Date('2024-02-20'),
|
||||
completedAt: new Date('2024-02-15'),
|
||||
isPublished: true,
|
||||
viewCount: 200,
|
||||
likes: 35,
|
||||
order: 2,
|
||||
seo: {
|
||||
metaTitle: '모바일 피트니스 앱 개발 프로젝트',
|
||||
metaDescription: 'React Native로 개발한 크로스플랫폼 피트니스 앱',
|
||||
keywords: ['Mobile App', 'React Native', 'Fitness', '헬스케어']
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '기업 웹사이트 리뉴얼',
|
||||
description: '기업의 브랜드 아이덴티티를 반영한 웹사이트 리뉴얼 프로젝트입니다. 사용자 경험을 개선하고 모던한 디자인을 적용하여 브랜드 가치를 높였습니다.',
|
||||
shortDescription: '브랜드 아이덴티티를 반영한 기업 웹사이트 리뉴얼',
|
||||
category: 'ui-ux-design',
|
||||
technologies: ['Figma', 'React', 'Sass', 'Framer Motion', 'Contentful'],
|
||||
images: [
|
||||
{ url: '/images/portfolio/corporate-1.jpg', alt: '기업 웹사이트 메인페이지', isPrimary: true },
|
||||
{ url: '/images/portfolio/corporate-2.jpg', alt: '회사소개 페이지', isPrimary: false },
|
||||
{ url: '/images/portfolio/corporate-3.jpg', alt: '서비스 페이지', isPrimary: false }
|
||||
],
|
||||
clientName: '기술 기업 TechCorp',
|
||||
projectUrl: 'https://example-corp.com',
|
||||
status: 'completed',
|
||||
featured: true,
|
||||
publishedAt: new Date('2024-03-10'),
|
||||
completedAt: new Date('2024-03-05'),
|
||||
isPublished: true,
|
||||
viewCount: 120,
|
||||
likes: 18,
|
||||
order: 3,
|
||||
seo: {
|
||||
metaTitle: '기업 웹사이트 리뉴얼 프로젝트',
|
||||
metaDescription: '모던한 디자인과 향상된 UX의 기업 웹사이트',
|
||||
keywords: ['Web Design', 'Corporate', 'UI/UX', '웹사이트리뉴얼']
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '레스토랑 예약 시스템',
|
||||
description: '레스토랑을 위한 온라인 예약 시스템을 개발했습니다. 실시간 테이블 현황, 예약 관리, 고객 관리 기능을 포함하여 레스토랑 운영 효율성을 높였습니다.',
|
||||
shortDescription: '실시간 테이블 예약 및 관리 시스템',
|
||||
category: 'web-development',
|
||||
technologies: ['Vue.js', 'Laravel', 'MySQL', 'Socket.io', 'Bootstrap'],
|
||||
images: [
|
||||
{ url: '/images/portfolio/restaurant-1.jpg', alt: '예약 시스템 메인', isPrimary: true },
|
||||
{ url: '/images/portfolio/restaurant-2.jpg', alt: '예약 현황 관리', isPrimary: false }
|
||||
],
|
||||
clientName: '레스토랑 델리셔스',
|
||||
status: 'completed',
|
||||
featured: false,
|
||||
publishedAt: new Date('2024-04-05'),
|
||||
completedAt: new Date('2024-04-01'),
|
||||
isPublished: true,
|
||||
viewCount: 85,
|
||||
likes: 12,
|
||||
order: 4,
|
||||
seo: {
|
||||
metaTitle: '레스토랑 예약 시스템 개발',
|
||||
metaDescription: '실시간 테이블 예약 및 관리 웹 시스템',
|
||||
keywords: ['Reservation System', 'Vue.js', 'Laravel', '예약시스템']
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '교육 플랫폼 앱',
|
||||
description: '온라인 교육을 위한 모바일 애플리케이션입니다. 동영상 강의, 퀴즈, 진도 관리 기능을 포함하여 효과적인 학습 환경을 제공합니다.',
|
||||
shortDescription: '온라인 학습을 위한 교육 플랫폼 모바일 앱',
|
||||
category: 'mobile-app',
|
||||
technologies: ['Flutter', 'Dart', 'Firebase', 'FFmpeg', 'AWS S3'],
|
||||
images: [
|
||||
{ url: '/images/portfolio/education-1.jpg', alt: '교육 앱 메인화면', isPrimary: true },
|
||||
{ url: '/images/portfolio/education-2.jpg', alt: '강의 재생 화면', isPrimary: false }
|
||||
],
|
||||
clientName: '온라인 교육 기업 EduTech',
|
||||
status: 'completed',
|
||||
featured: false,
|
||||
publishedAt: new Date('2024-05-12'),
|
||||
completedAt: new Date('2024-05-08'),
|
||||
isPublished: true,
|
||||
viewCount: 95,
|
||||
likes: 20,
|
||||
order: 5,
|
||||
seo: {
|
||||
metaTitle: '교육 플랫폼 모바일 앱 개발',
|
||||
metaDescription: 'Flutter로 개발한 온라인 교육 모바일 앱',
|
||||
keywords: ['Education App', 'Flutter', 'E-learning', '교육앱']
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'IoT 대시보드',
|
||||
description: 'IoT 디바이스들을 모니터링하고 제어할 수 있는 웹 대시보드를 개발했습니다. 실시간 데이터 시각화와 알림 기능을 포함합니다.',
|
||||
shortDescription: 'IoT 디바이스 모니터링 및 제어 웹 대시보드',
|
||||
category: 'web-development',
|
||||
technologies: ['React', 'D3.js', 'Node.js', 'MQTT', 'InfluxDB', 'Docker'],
|
||||
images: [
|
||||
{ url: '/images/portfolio/iot-1.jpg', alt: 'IoT 대시보드 메인', isPrimary: true }
|
||||
],
|
||||
clientName: 'IoT 솔루션 기업 SmartDevice',
|
||||
status: 'in-progress',
|
||||
featured: false,
|
||||
publishedAt: new Date('2024-06-01'),
|
||||
isPublished: true,
|
||||
viewCount: 45,
|
||||
likes: 8,
|
||||
order: 6,
|
||||
seo: {
|
||||
metaTitle: 'IoT 대시보드 개발 프로젝트',
|
||||
metaDescription: '실시간 IoT 디바이스 모니터링 웹 대시보드',
|
||||
keywords: ['IoT', 'Dashboard', 'Real-time', 'Monitoring']
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
await Portfolio.insertMany(portfolioItems);
|
||||
console.log('✅ Sample portfolio items created successfully');
|
||||
} catch (error) {
|
||||
console.error('❌ Error creating sample portfolio:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function createSiteSettings() {
|
||||
try {
|
||||
const existingSettings = await SiteSettings.findOne();
|
||||
|
||||
if (existingSettings) {
|
||||
console.log('⚙️ Site settings already exist, skipping...');
|
||||
return;
|
||||
}
|
||||
|
||||
const settings = new SiteSettings({
|
||||
siteName: 'SmartSolTech',
|
||||
siteDescription: '혁신적인 기술 솔루션으로 비즈니스의 성장을 지원합니다',
|
||||
logo: '/images/logo.png',
|
||||
favicon: '/images/favicon.ico',
|
||||
contact: {
|
||||
email: 'info@smartsoltech.kr',
|
||||
phone: '+82-10-1234-5678',
|
||||
address: 'Seoul, South Korea'
|
||||
},
|
||||
social: {
|
||||
facebook: 'https://facebook.com/smartsoltech',
|
||||
twitter: 'https://twitter.com/smartsoltech',
|
||||
linkedin: 'https://linkedin.com/company/smartsoltech',
|
||||
instagram: 'https://instagram.com/smartsoltech',
|
||||
github: 'https://github.com/smartsoltech'
|
||||
},
|
||||
telegram: {
|
||||
isEnabled: false
|
||||
},
|
||||
seo: {
|
||||
metaTitle: 'SmartSolTech - 혁신적인 기술 솔루션',
|
||||
metaDescription: '웹 개발, 모바일 앱, UI/UX 디자인, 디지털 마케팅 전문 기업. 한국에서 최고의 기술 솔루션을 제공합니다.',
|
||||
keywords: '웹 개발, 모바일 앱, UI/UX 디자인, 디지털 마케팅, 기술 솔루션, 한국, SmartSolTech'
|
||||
},
|
||||
hero: {
|
||||
title: 'Smart Technology Solutions',
|
||||
subtitle: '혁신적인 웹 개발, 모바일 앱, UI/UX 디자인으로 비즈니스의 디지털 전환을 이끌어갑니다',
|
||||
backgroundImage: '/images/hero-bg.jpg',
|
||||
ctaText: '프로젝트 시작하기',
|
||||
ctaLink: '/contact'
|
||||
},
|
||||
about: {
|
||||
title: 'SmartSolTech 소개',
|
||||
description: '우리는 최신 기술과 창의적인 아이디어로 고객의 비즈니스 성장을 지원하는 전문 개발팀입니다. 웹 개발부터 모바일 앱, UI/UX 디자인까지 포괄적인 디지털 솔루션을 제공합니다.',
|
||||
image: '/images/about.jpg'
|
||||
},
|
||||
maintenance: {
|
||||
isEnabled: false,
|
||||
message: '현재 시스템 점검 중입니다. 잠시 후 다시 접속해 주세요.'
|
||||
}
|
||||
});
|
||||
|
||||
await settings.save();
|
||||
console.log('✅ Site settings created successfully');
|
||||
} catch (error) {
|
||||
console.error('❌ Error creating site settings:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Run the initialization
|
||||
if (require.main === module) {
|
||||
initializeDatabase();
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
initializeDatabase,
|
||||
createAdminUser,
|
||||
createSampleServices,
|
||||
createSamplePortfolio,
|
||||
createSiteSettings
|
||||
};
|
||||
498
.history/scripts/init-db_20251019162545.js
Normal file
498
.history/scripts/init-db_20251019162545.js
Normal file
@@ -0,0 +1,498 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Database initialization script for SmartSolTech
|
||||
* Creates initial admin user and sample data
|
||||
*/
|
||||
|
||||
const mongoose = require('mongoose');
|
||||
const bcrypt = require('bcryptjs');
|
||||
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');
|
||||
|
||||
// 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');
|
||||
|
||||
// Create admin user
|
||||
await createAdminUser();
|
||||
|
||||
// Create sample services
|
||||
await createSampleServices();
|
||||
|
||||
// Create sample portfolio items
|
||||
await createSamplePortfolio();
|
||||
|
||||
// Create site settings
|
||||
await createSiteSettings();
|
||||
|
||||
console.log('🎉 Database initialization completed successfully!');
|
||||
console.log(`📧 Admin login: ${ADMIN_EMAIL}`);
|
||||
console.log(`🔑 Admin password: ${ADMIN_PASSWORD}`);
|
||||
console.log('⚠️ Please change the admin password after first login!');
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Database initialization failed:', error);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
await mongoose.connection.close();
|
||||
console.log('🔌 Database connection closed');
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
async function createAdminUser() {
|
||||
try {
|
||||
const existingAdmin = await User.findOne({ email: ADMIN_EMAIL });
|
||||
|
||||
if (existingAdmin) {
|
||||
console.log('👤 Admin user already exists, skipping...');
|
||||
return;
|
||||
}
|
||||
|
||||
const adminUser = new User({
|
||||
name: 'Administrator',
|
||||
email: ADMIN_EMAIL,
|
||||
password: ADMIN_PASSWORD,
|
||||
role: 'admin',
|
||||
isActive: true
|
||||
});
|
||||
|
||||
await adminUser.save();
|
||||
console.log('✅ Admin user created successfully');
|
||||
} catch (error) {
|
||||
console.error('❌ Error creating admin user:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function createSampleServices() {
|
||||
try {
|
||||
const existingServices = await Service.countDocuments();
|
||||
|
||||
if (existingServices > 0) {
|
||||
console.log('🛠️ Services already exist, skipping...');
|
||||
return;
|
||||
}
|
||||
|
||||
const services = [
|
||||
{
|
||||
name: '웹 개발',
|
||||
description: '현대적이고 반응형인 웹사이트와 웹 애플리케이션을 개발합니다. React, Node.js, MongoDB 등 최신 기술 스택을 활용하여 성능과 사용자 경험을 최적화합니다.',
|
||||
shortDescription: '현대적이고 반응형 웹사이트 및 웹 애플리케이션 개발',
|
||||
icon: 'fas fa-code',
|
||||
category: 'development',
|
||||
features: [
|
||||
{ name: '반응형 디자인', description: '모든 디바이스에서 완벽하게 작동', included: true },
|
||||
{ name: 'SEO 최적화', description: '검색엔진 최적화로 더 많은 방문자 유치', included: true },
|
||||
{ name: '성능 최적화', description: '빠른 로딩 속도와 원활한 사용자 경험', included: true },
|
||||
{ name: '보안 강화', description: '최신 보안 기술로 안전한 웹사이트', included: true },
|
||||
{ name: '유지보수', description: '6개월 무료 유지보수 지원', included: true }
|
||||
],
|
||||
pricing: {
|
||||
basePrice: 500000,
|
||||
currency: 'KRW',
|
||||
priceType: 'project',
|
||||
priceRange: { min: 500000, max: 5000000 }
|
||||
},
|
||||
estimatedTime: { min: 2, max: 8, unit: 'weeks' },
|
||||
featured: true,
|
||||
isActive: true,
|
||||
order: 1,
|
||||
tags: ['React', 'Node.js', 'MongoDB', 'Express', 'JavaScript', 'HTML', 'CSS']
|
||||
},
|
||||
{
|
||||
name: '모바일 앱 개발',
|
||||
description: 'iOS와 Android 플랫폼을 위한 네이티브 및 크로스플랫폼 모바일 애플리케이션을 개발합니다. React Native, Flutter 등을 활용하여 효율적인 개발을 진행합니다.',
|
||||
shortDescription: 'iOS/Android 네이티브 및 크로스플랫폼 앱 개발',
|
||||
icon: 'fas fa-mobile-alt',
|
||||
category: 'development',
|
||||
features: [
|
||||
{ name: '크로스플랫폼 개발', description: 'iOS와 Android 동시 지원', included: true },
|
||||
{ name: '네이티브 성능', description: '최적화된 성능과 사용자 경험', included: true },
|
||||
{ name: '푸시 알림', description: '실시간 푸시 알림 시스템', included: true },
|
||||
{ name: '오프라인 지원', description: '인터넷 연결 없이도 기본 기능 사용 가능', included: false },
|
||||
{ name: '앱스토어 등록', description: '앱스토어 및 플레이스토어 등록 지원', included: true }
|
||||
],
|
||||
pricing: {
|
||||
basePrice: 800000,
|
||||
currency: 'KRW',
|
||||
priceType: 'project',
|
||||
priceRange: { min: 800000, max: 8000000 }
|
||||
},
|
||||
estimatedTime: { min: 4, max: 16, unit: 'weeks' },
|
||||
featured: true,
|
||||
isActive: true,
|
||||
order: 2,
|
||||
tags: ['React Native', 'Flutter', 'iOS', 'Android', 'Swift', 'Kotlin', 'JavaScript']
|
||||
},
|
||||
{
|
||||
name: 'UI/UX 디자인',
|
||||
description: '사용자 중심의 직관적이고 아름다운 인터페이스를 디자인합니다. 사용자 경험 연구와 프로토타이핑을 통해 최적의 디자인 솔루션을 제공합니다.',
|
||||
shortDescription: '사용자 중심의 직관적이고 아름다운 인터페이스 디자인',
|
||||
icon: 'fas fa-palette',
|
||||
category: 'design',
|
||||
features: [
|
||||
{ name: '사용자 경험 연구', description: '타겟 사용자 분석 및 페르소나 설정', included: true },
|
||||
{ name: '와이어프레임', description: '정보 구조와 레이아웃 설계', included: true },
|
||||
{ name: '프로토타이핑', description: '인터랙티브 프로토타입 제작', included: true },
|
||||
{ name: '비주얼 디자인', description: '브랜드에 맞는 시각적 디자인', included: true },
|
||||
{ name: '디자인 시스템', description: '일관된 디자인을 위한 가이드라인', included: false }
|
||||
],
|
||||
pricing: {
|
||||
basePrice: 300000,
|
||||
currency: 'KRW',
|
||||
priceType: 'project',
|
||||
priceRange: { min: 300000, max: 2000000 }
|
||||
},
|
||||
estimatedTime: { min: 1, max: 6, unit: 'weeks' },
|
||||
featured: true,
|
||||
isActive: true,
|
||||
order: 3,
|
||||
tags: ['Figma', 'Sketch', 'Adobe XD', 'Photoshop', 'Illustrator', 'Prototyping']
|
||||
},
|
||||
{
|
||||
name: '디지털 마케팅',
|
||||
description: 'SEO, 소셜미디어 마케팅, 온라인 광고를 통해 디지털 마케팅 전략을 수립하고 실행합니다. 데이터 분석을 통한 지속적인 최적화를 제공합니다.',
|
||||
shortDescription: 'SEO, 소셜미디어, 온라인 광고를 통한 디지털 마케팅',
|
||||
icon: 'fas fa-chart-line',
|
||||
category: 'marketing',
|
||||
features: [
|
||||
{ name: 'SEO 최적화', description: '검색엔진 상위 노출을 위한 최적화', included: true },
|
||||
{ name: '소셜미디어 관리', description: '페이스북, 인스타그램, 유튜브 관리', included: true },
|
||||
{ name: '구글 광고', description: '구글 애즈를 통한 타겟 광고', included: true },
|
||||
{ name: '콘텐츠 마케팅', description: '블로그 및 콘텐츠 제작', included: false },
|
||||
{ name: '분석 및 리포팅', description: '마케팅 성과 분석 및 보고서', included: true }
|
||||
],
|
||||
pricing: {
|
||||
basePrice: 200000,
|
||||
currency: 'KRW',
|
||||
priceType: 'project',
|
||||
priceRange: { min: 200000, max: 1500000 }
|
||||
},
|
||||
estimatedTime: { min: 2, max: 12, unit: 'weeks' },
|
||||
featured: true,
|
||||
isActive: true,
|
||||
order: 4,
|
||||
tags: ['SEO', 'Google Ads', 'Facebook Ads', 'Analytics', 'Social Media', 'Content Marketing']
|
||||
},
|
||||
{
|
||||
name: '브랜딩',
|
||||
description: '기업의 정체성을 반영하는 로고, 브랜드 가이드라인, 마케팅 자료를 디자인합니다. 일관된 브랜드 이미지로 브랜드 가치를 높입니다.',
|
||||
shortDescription: '로고, 브랜드 가이드라인, 마케팅 자료 디자인',
|
||||
icon: 'fas fa-copyright',
|
||||
category: 'design',
|
||||
features: [
|
||||
{ name: '로고 디자인', description: '기업 정체성을 반영한 로고 제작', included: true },
|
||||
{ name: '브랜드 가이드라인', description: '색상, 폰트, 사용법 가이드', included: true },
|
||||
{ name: '명함 디자인', description: '브랜드에 맞는 명함 디자인', included: true },
|
||||
{ name: '브로슈어 디자인', description: '회사 소개 브로슈어 제작', included: false },
|
||||
{ name: '웹 브랜딩', description: '웹사이트 브랜딩 요소 적용', included: false }
|
||||
],
|
||||
pricing: {
|
||||
basePrice: 400000,
|
||||
currency: 'KRW',
|
||||
priceType: 'project',
|
||||
priceRange: { min: 400000, max: 2500000 }
|
||||
},
|
||||
estimatedTime: { min: 2, max: 8, unit: 'weeks' },
|
||||
featured: false,
|
||||
isActive: true,
|
||||
order: 5,
|
||||
tags: ['Logo Design', 'Brand Identity', 'Graphic Design', 'Corporate Design']
|
||||
},
|
||||
{
|
||||
name: '기술 컨설팅',
|
||||
description: '기업의 디지털 전환과 기술 도입을 위한 전문적인 컨설팅을 제공합니다. 기술 아키텍처 설계부터 구현 전략까지 포괄적으로 지원합니다.',
|
||||
shortDescription: '디지털 전환 및 기술 도입을 위한 전문 컨설팅',
|
||||
icon: 'fas fa-lightbulb',
|
||||
category: 'consulting',
|
||||
features: [
|
||||
{ name: '기술 분석', description: '현재 기술 스택 분석 및 개선안 제시', included: true },
|
||||
{ name: '아키텍처 설계', description: '확장 가능한 시스템 아키텍처 설계', included: true },
|
||||
{ name: '개발 프로세스 개선', description: '효율적인 개발 워크플로우 구축', included: true },
|
||||
{ name: '팀 교육', description: '개발팀 대상 기술 교육', included: false },
|
||||
{ name: '지속적인 지원', description: '프로젝트 완료 후 지속적인 기술 지원', included: false }
|
||||
],
|
||||
pricing: {
|
||||
basePrice: 150000,
|
||||
currency: 'KRW',
|
||||
priceType: 'hourly',
|
||||
priceRange: { min: 150000, max: 500000 }
|
||||
},
|
||||
estimatedTime: { min: 1, max: 4, unit: 'weeks' },
|
||||
featured: false,
|
||||
isActive: true,
|
||||
order: 6,
|
||||
tags: ['Architecture', 'Strategy', 'Process', 'Training', 'Consultation']
|
||||
}
|
||||
];
|
||||
|
||||
await Service.insertMany(services);
|
||||
console.log('✅ Sample services created successfully');
|
||||
} catch (error) {
|
||||
console.error('❌ Error creating sample services:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function createSamplePortfolio() {
|
||||
try {
|
||||
const existingPortfolio = await Portfolio.countDocuments();
|
||||
|
||||
if (existingPortfolio > 0) {
|
||||
console.log('🎨 Portfolio items already exist, skipping...');
|
||||
return;
|
||||
}
|
||||
|
||||
const portfolioItems = [
|
||||
{
|
||||
title: 'E-commerce 플랫폼',
|
||||
description: '현대적인 온라인 쇼핑몰 플랫폼을 개발했습니다. React와 Node.js를 활용하여 높은 성능과 사용자 경험을 제공하며, 결제 시스템과 재고 관리 기능을 포함합니다.',
|
||||
shortDescription: '반응형 온라인 쇼핑몰 플랫폼 개발',
|
||||
category: 'web-development',
|
||||
technologies: ['React', 'Node.js', 'MongoDB', 'Express', 'Stripe', 'AWS'],
|
||||
images: [
|
||||
{ url: '/images/portfolio/ecommerce-1.jpg', alt: 'E-commerce 메인페이지', isPrimary: true },
|
||||
{ url: '/images/portfolio/ecommerce-2.jpg', alt: '상품 상세페이지', isPrimary: false },
|
||||
{ url: '/images/portfolio/ecommerce-3.jpg', alt: '장바구니 페이지', isPrimary: false }
|
||||
],
|
||||
clientName: '패션 브랜드 ABC',
|
||||
projectUrl: 'https://example-ecommerce.com',
|
||||
status: 'completed',
|
||||
featured: true,
|
||||
publishedAt: new Date('2024-01-15'),
|
||||
completedAt: new Date('2024-01-10'),
|
||||
isPublished: true,
|
||||
viewCount: 150,
|
||||
likes: 25,
|
||||
order: 1,
|
||||
seo: {
|
||||
metaTitle: 'E-commerce 플랫폼 개발 프로젝트',
|
||||
metaDescription: '현대적인 온라인 쇼핑몰 플랫폼 개발 사례',
|
||||
keywords: ['E-commerce', 'React', 'Node.js', '온라인쇼핑몰']
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '모바일 피트니스 앱',
|
||||
description: 'React Native를 사용하여 크로스플랫폼 피트니스 애플리케이션을 개발했습니다. 운동 계획, 칼로리 추적, 소셜 기능을 포함하여 사용자들의 건강한 라이프스타일을 지원합니다.',
|
||||
shortDescription: '건강 관리를 위한 크로스플랫폼 모바일 앱',
|
||||
category: 'mobile-app',
|
||||
technologies: ['React Native', 'Redux', 'Firebase', 'Node.js', 'PostgreSQL'],
|
||||
images: [
|
||||
{ url: '/images/portfolio/fitness-1.jpg', alt: '피트니스 앱 메인화면', isPrimary: true },
|
||||
{ url: '/images/portfolio/fitness-2.jpg', alt: '운동 계획 화면', isPrimary: false },
|
||||
{ url: '/images/portfolio/fitness-3.jpg', alt: '통계 화면', isPrimary: false }
|
||||
],
|
||||
clientName: '헬스케어 스타트업 FIT',
|
||||
status: 'completed',
|
||||
featured: true,
|
||||
publishedAt: new Date('2024-02-20'),
|
||||
completedAt: new Date('2024-02-15'),
|
||||
isPublished: true,
|
||||
viewCount: 200,
|
||||
likes: 35,
|
||||
order: 2,
|
||||
seo: {
|
||||
metaTitle: '모바일 피트니스 앱 개발 프로젝트',
|
||||
metaDescription: 'React Native로 개발한 크로스플랫폼 피트니스 앱',
|
||||
keywords: ['Mobile App', 'React Native', 'Fitness', '헬스케어']
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '기업 웹사이트 리뉴얼',
|
||||
description: '기업의 브랜드 아이덴티티를 반영한 웹사이트 리뉴얼 프로젝트입니다. 사용자 경험을 개선하고 모던한 디자인을 적용하여 브랜드 가치를 높였습니다.',
|
||||
shortDescription: '브랜드 아이덴티티를 반영한 기업 웹사이트 리뉴얼',
|
||||
category: 'ui-ux-design',
|
||||
technologies: ['Figma', 'React', 'Sass', 'Framer Motion', 'Contentful'],
|
||||
images: [
|
||||
{ url: '/images/portfolio/corporate-1.jpg', alt: '기업 웹사이트 메인페이지', isPrimary: true },
|
||||
{ url: '/images/portfolio/corporate-2.jpg', alt: '회사소개 페이지', isPrimary: false },
|
||||
{ url: '/images/portfolio/corporate-3.jpg', alt: '서비스 페이지', isPrimary: false }
|
||||
],
|
||||
clientName: '기술 기업 TechCorp',
|
||||
projectUrl: 'https://example-corp.com',
|
||||
status: 'completed',
|
||||
featured: true,
|
||||
publishedAt: new Date('2024-03-10'),
|
||||
completedAt: new Date('2024-03-05'),
|
||||
isPublished: true,
|
||||
viewCount: 120,
|
||||
likes: 18,
|
||||
order: 3,
|
||||
seo: {
|
||||
metaTitle: '기업 웹사이트 리뉴얼 프로젝트',
|
||||
metaDescription: '모던한 디자인과 향상된 UX의 기업 웹사이트',
|
||||
keywords: ['Web Design', 'Corporate', 'UI/UX', '웹사이트리뉴얼']
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '레스토랑 예약 시스템',
|
||||
description: '레스토랑을 위한 온라인 예약 시스템을 개발했습니다. 실시간 테이블 현황, 예약 관리, 고객 관리 기능을 포함하여 레스토랑 운영 효율성을 높였습니다.',
|
||||
shortDescription: '실시간 테이블 예약 및 관리 시스템',
|
||||
category: 'web-development',
|
||||
technologies: ['Vue.js', 'Laravel', 'MySQL', 'Socket.io', 'Bootstrap'],
|
||||
images: [
|
||||
{ url: '/images/portfolio/restaurant-1.jpg', alt: '예약 시스템 메인', isPrimary: true },
|
||||
{ url: '/images/portfolio/restaurant-2.jpg', alt: '예약 현황 관리', isPrimary: false }
|
||||
],
|
||||
clientName: '레스토랑 델리셔스',
|
||||
status: 'completed',
|
||||
featured: false,
|
||||
publishedAt: new Date('2024-04-05'),
|
||||
completedAt: new Date('2024-04-01'),
|
||||
isPublished: true,
|
||||
viewCount: 85,
|
||||
likes: 12,
|
||||
order: 4,
|
||||
seo: {
|
||||
metaTitle: '레스토랑 예약 시스템 개발',
|
||||
metaDescription: '실시간 테이블 예약 및 관리 웹 시스템',
|
||||
keywords: ['Reservation System', 'Vue.js', 'Laravel', '예약시스템']
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '교육 플랫폼 앱',
|
||||
description: '온라인 교육을 위한 모바일 애플리케이션입니다. 동영상 강의, 퀴즈, 진도 관리 기능을 포함하여 효과적인 학습 환경을 제공합니다.',
|
||||
shortDescription: '온라인 학습을 위한 교육 플랫폼 모바일 앱',
|
||||
category: 'mobile-app',
|
||||
technologies: ['Flutter', 'Dart', 'Firebase', 'FFmpeg', 'AWS S3'],
|
||||
images: [
|
||||
{ url: '/images/portfolio/education-1.jpg', alt: '교육 앱 메인화면', isPrimary: true },
|
||||
{ url: '/images/portfolio/education-2.jpg', alt: '강의 재생 화면', isPrimary: false }
|
||||
],
|
||||
clientName: '온라인 교육 기업 EduTech',
|
||||
status: 'completed',
|
||||
featured: false,
|
||||
publishedAt: new Date('2024-05-12'),
|
||||
completedAt: new Date('2024-05-08'),
|
||||
isPublished: true,
|
||||
viewCount: 95,
|
||||
likes: 20,
|
||||
order: 5,
|
||||
seo: {
|
||||
metaTitle: '교육 플랫폼 모바일 앱 개발',
|
||||
metaDescription: 'Flutter로 개발한 온라인 교육 모바일 앱',
|
||||
keywords: ['Education App', 'Flutter', 'E-learning', '교육앱']
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'IoT 대시보드',
|
||||
description: 'IoT 디바이스들을 모니터링하고 제어할 수 있는 웹 대시보드를 개발했습니다. 실시간 데이터 시각화와 알림 기능을 포함합니다.',
|
||||
shortDescription: 'IoT 디바이스 모니터링 및 제어 웹 대시보드',
|
||||
category: 'web-development',
|
||||
technologies: ['React', 'D3.js', 'Node.js', 'MQTT', 'InfluxDB', 'Docker'],
|
||||
images: [
|
||||
{ url: '/images/portfolio/iot-1.jpg', alt: 'IoT 대시보드 메인', isPrimary: true }
|
||||
],
|
||||
clientName: 'IoT 솔루션 기업 SmartDevice',
|
||||
status: 'in-progress',
|
||||
featured: false,
|
||||
publishedAt: new Date('2024-06-01'),
|
||||
isPublished: true,
|
||||
viewCount: 45,
|
||||
likes: 8,
|
||||
order: 6,
|
||||
seo: {
|
||||
metaTitle: 'IoT 대시보드 개발 프로젝트',
|
||||
metaDescription: '실시간 IoT 디바이스 모니터링 웹 대시보드',
|
||||
keywords: ['IoT', 'Dashboard', 'Real-time', 'Monitoring']
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
await Portfolio.insertMany(portfolioItems);
|
||||
console.log('✅ Sample portfolio items created successfully');
|
||||
} catch (error) {
|
||||
console.error('❌ Error creating sample portfolio:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function createSiteSettings() {
|
||||
try {
|
||||
const existingSettings = await SiteSettings.findOne();
|
||||
|
||||
if (existingSettings) {
|
||||
console.log('⚙️ Site settings already exist, skipping...');
|
||||
return;
|
||||
}
|
||||
|
||||
const settings = new SiteSettings({
|
||||
siteName: 'SmartSolTech',
|
||||
siteDescription: '혁신적인 기술 솔루션으로 비즈니스의 성장을 지원합니다',
|
||||
logo: '/images/logo.png',
|
||||
favicon: '/images/favicon.ico',
|
||||
contact: {
|
||||
email: 'info@smartsoltech.kr',
|
||||
phone: '+82-10-1234-5678',
|
||||
address: 'Seoul, South Korea'
|
||||
},
|
||||
social: {
|
||||
facebook: 'https://facebook.com/smartsoltech',
|
||||
twitter: 'https://twitter.com/smartsoltech',
|
||||
linkedin: 'https://linkedin.com/company/smartsoltech',
|
||||
instagram: 'https://instagram.com/smartsoltech',
|
||||
github: 'https://github.com/smartsoltech'
|
||||
},
|
||||
telegram: {
|
||||
isEnabled: false
|
||||
},
|
||||
seo: {
|
||||
metaTitle: 'SmartSolTech - 혁신적인 기술 솔루션',
|
||||
metaDescription: '웹 개발, 모바일 앱, UI/UX 디자인, 디지털 마케팅 전문 기업. 한국에서 최고의 기술 솔루션을 제공합니다.',
|
||||
keywords: '웹 개발, 모바일 앱, UI/UX 디자인, 디지털 마케팅, 기술 솔루션, 한국, SmartSolTech'
|
||||
},
|
||||
hero: {
|
||||
title: 'Smart Technology Solutions',
|
||||
subtitle: '혁신적인 웹 개발, 모바일 앱, UI/UX 디자인으로 비즈니스의 디지털 전환을 이끌어갑니다',
|
||||
backgroundImage: '/images/hero-bg.jpg',
|
||||
ctaText: '프로젝트 시작하기',
|
||||
ctaLink: '/contact'
|
||||
},
|
||||
about: {
|
||||
title: 'SmartSolTech 소개',
|
||||
description: '우리는 최신 기술과 창의적인 아이디어로 고객의 비즈니스 성장을 지원하는 전문 개발팀입니다. 웹 개발부터 모바일 앱, UI/UX 디자인까지 포괄적인 디지털 솔루션을 제공합니다.',
|
||||
image: '/images/about.jpg'
|
||||
},
|
||||
maintenance: {
|
||||
isEnabled: false,
|
||||
message: '현재 시스템 점검 중입니다. 잠시 후 다시 접속해 주세요.'
|
||||
}
|
||||
});
|
||||
|
||||
await settings.save();
|
||||
console.log('✅ Site settings created successfully');
|
||||
} catch (error) {
|
||||
console.error('❌ Error creating site settings:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Run the initialization
|
||||
if (require.main === module) {
|
||||
initializeDatabase();
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
initializeDatabase,
|
||||
createAdminUser,
|
||||
createSampleServices,
|
||||
createSamplePortfolio,
|
||||
createSiteSettings
|
||||
};
|
||||
416
.history/server-demo_20251019163550.js
Normal file
416
.history/server-demo_20251019163550.js
Normal file
@@ -0,0 +1,416 @@
|
||||
/**
|
||||
* Demo server for SmartSolTech website
|
||||
* Uses mock data instead of MongoDB for demonstration
|
||||
*/
|
||||
|
||||
const express = require('express');
|
||||
const path = require('path');
|
||||
const cookieParser = require('cookie-parser');
|
||||
const session = require('express-session');
|
||||
const flash = require('connect-flash');
|
||||
const helmet = require('helmet');
|
||||
const compression = require('compression');
|
||||
const rateLimit = require('express-rate-limit');
|
||||
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 3000;
|
||||
|
||||
// Security middleware
|
||||
app.use(helmet({
|
||||
contentSecurityPolicy: {
|
||||
directives: {
|
||||
defaultSrc: ["'self'"],
|
||||
styleSrc: ["'self'", "'unsafe-inline'", "https://cdn.jsdelivr.net", "https://cdnjs.cloudflare.com", "https://unpkg.com"],
|
||||
scriptSrc: ["'self'", "'unsafe-inline'", "https://cdn.jsdelivr.net", "https://cdnjs.cloudflare.com", "https://unpkg.com"],
|
||||
imgSrc: ["'self'", "data:", "https:", "blob:"],
|
||||
fontSrc: ["'self'", "https://cdnjs.cloudflare.com"],
|
||||
connectSrc: ["'self'", "https:"],
|
||||
mediaSrc: ["'self'"],
|
||||
frameSrc: ["'none'"]
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
// Rate limiting
|
||||
const limiter = rateLimit({
|
||||
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||
max: 100, // limit each IP to 100 requests per windowMs
|
||||
message: 'Too many requests from this IP, please try again later.'
|
||||
});
|
||||
app.use(limiter);
|
||||
|
||||
// Compression
|
||||
app.use(compression());
|
||||
|
||||
// View engine setup
|
||||
app.set('views', path.join(__dirname, 'views'));
|
||||
app.set('view engine', 'ejs');
|
||||
|
||||
// Middleware
|
||||
app.use(express.json({ limit: '10mb' }));
|
||||
app.use(express.urlencoded({ extended: true, limit: '10mb' }));
|
||||
app.use(cookieParser());
|
||||
app.use(express.static(path.join(__dirname, 'public')));
|
||||
|
||||
// Session configuration
|
||||
app.use(session({
|
||||
secret: 'demo-secret-key',
|
||||
resave: false,
|
||||
saveUninitialized: false,
|
||||
cookie: {
|
||||
secure: false, // Set to true in production with HTTPS
|
||||
maxAge: 24 * 60 * 60 * 1000 // 24 hours
|
||||
}
|
||||
}));
|
||||
|
||||
// Flash messages
|
||||
app.use(flash());
|
||||
|
||||
// Global variables for templates
|
||||
app.use((req, res, next) => {
|
||||
res.locals.success_msg = req.flash('success');
|
||||
res.locals.error_msg = req.flash('error');
|
||||
res.locals.error = req.flash('error');
|
||||
res.locals.currentYear = new Date().getFullYear();
|
||||
next();
|
||||
});
|
||||
|
||||
// Mock data
|
||||
const mockPortfolio = [
|
||||
{
|
||||
_id: '1',
|
||||
title: 'E-commerce 플랫폼',
|
||||
description: '현대적인 온라인 쇼핑몰 플랫폼을 개발했습니다. React와 Node.js를 활용하여 높은 성능과 사용자 경험을 제공하며, 결제 시스템과 재고 관리 기능을 포함합니다.',
|
||||
shortDescription: '반응형 온라인 쇼핑몰 플랫폼 개발',
|
||||
category: 'web-development',
|
||||
technologies: ['React', 'Node.js', 'MongoDB', 'Express', 'Stripe', 'AWS'],
|
||||
images: [
|
||||
{ url: '/images/portfolio/ecommerce-1.jpg', alt: 'E-commerce 메인페이지', isPrimary: true }
|
||||
],
|
||||
clientName: '패션 브랜드 ABC',
|
||||
projectUrl: 'https://example-ecommerce.demo',
|
||||
status: 'completed',
|
||||
featured: true,
|
||||
publishedAt: new Date('2024-01-15'),
|
||||
completedAt: new Date('2024-01-10'),
|
||||
isPublished: true,
|
||||
viewCount: 150,
|
||||
likes: 25
|
||||
},
|
||||
{
|
||||
_id: '2',
|
||||
title: '모바일 피트니스 앱',
|
||||
description: 'React Native를 사용하여 크로스플랫폼 피트니스 애플리케이션을 개발했습니다. 운동 계획, 칼로리 추적, 소셜 기능을 포함하여 사용자들의 건강한 라이프스타일을 지원합니다.',
|
||||
shortDescription: '건강 관리를 위한 크로스플랫폼 모바일 앱',
|
||||
category: 'mobile-app',
|
||||
technologies: ['React Native', 'Redux', 'Firebase', 'Node.js', 'PostgreSQL'],
|
||||
images: [
|
||||
{ url: '/images/portfolio/fitness-1.jpg', alt: '피트니스 앱 메인화면', isPrimary: true }
|
||||
],
|
||||
clientName: '헬스케어 스타트업 FIT',
|
||||
status: 'completed',
|
||||
featured: true,
|
||||
publishedAt: new Date('2024-02-20'),
|
||||
completedAt: new Date('2024-02-15'),
|
||||
isPublished: true,
|
||||
viewCount: 200,
|
||||
likes: 35
|
||||
},
|
||||
{
|
||||
_id: '3',
|
||||
title: '기업 웹사이트 리뉴얼',
|
||||
description: '기업의 브랜드 아이덴티티를 반영한 웹사이트 리뉴얼 프로젝트입니다. 사용자 경험을 개선하고 모던한 디자인을 적용하여 브랜드 가치를 높였습니다.',
|
||||
shortDescription: '브랜드 아이덴티티를 반영한 기업 웹사이트 리뉴얼',
|
||||
category: 'ui-ux-design',
|
||||
technologies: ['Figma', 'React', 'Sass', 'Framer Motion', 'Contentful'],
|
||||
images: [
|
||||
{ url: '/images/portfolio/corporate-1.jpg', alt: '기업 웹사이트 메인페이지', isPrimary: true }
|
||||
],
|
||||
clientName: '기술 기업 TechCorp',
|
||||
projectUrl: 'https://example-corp.demo',
|
||||
status: 'completed',
|
||||
featured: true,
|
||||
publishedAt: new Date('2024-03-10'),
|
||||
completedAt: new Date('2024-03-05'),
|
||||
isPublished: true,
|
||||
viewCount: 120,
|
||||
likes: 18
|
||||
}
|
||||
];
|
||||
|
||||
const mockServices = [
|
||||
{
|
||||
_id: '1',
|
||||
name: '웹 개발',
|
||||
description: '현대적이고 반응형인 웹사이트와 웹 애플리케이션을 개발합니다. React, Node.js, MongoDB 등 최신 기술 스택을 활용하여 성능과 사용자 경험을 최적화합니다.',
|
||||
shortDescription: '현대적이고 반응형 웹사이트 및 웹 애플리케이션 개발',
|
||||
icon: 'fas fa-code',
|
||||
category: 'development',
|
||||
pricing: {
|
||||
basePrice: 500000,
|
||||
currency: 'KRW',
|
||||
priceType: 'project',
|
||||
priceRange: { min: 500000, max: 5000000 }
|
||||
},
|
||||
featured: true,
|
||||
isActive: true
|
||||
},
|
||||
{
|
||||
_id: '2',
|
||||
name: '모바일 앱 개발',
|
||||
description: 'iOS와 Android 플랫폼을 위한 네이티브 및 크로스플랫폼 모바일 애플리케이션을 개발합니다. React Native, Flutter 등을 활용하여 효율적인 개발을 진행합니다.',
|
||||
shortDescription: 'iOS/Android 네이티브 및 크로스플랫폼 앱 개발',
|
||||
icon: 'fas fa-mobile-alt',
|
||||
category: 'development',
|
||||
pricing: {
|
||||
basePrice: 800000,
|
||||
currency: 'KRW',
|
||||
priceType: 'project',
|
||||
priceRange: { min: 800000, max: 8000000 }
|
||||
},
|
||||
featured: true,
|
||||
isActive: true
|
||||
},
|
||||
{
|
||||
_id: '3',
|
||||
name: 'UI/UX 디자인',
|
||||
description: '사용자 중심의 직관적이고 아름다운 인터페이스를 디자인합니다. 사용자 경험 연구와 프로토타이핑을 통해 최적의 디자인 솔루션을 제공합니다.',
|
||||
shortDescription: '사용자 중심의 직관적이고 아름다운 인터페이스 디자인',
|
||||
icon: 'fas fa-palette',
|
||||
category: 'design',
|
||||
pricing: {
|
||||
basePrice: 300000,
|
||||
currency: 'KRW',
|
||||
priceType: 'project',
|
||||
priceRange: { min: 300000, max: 2000000 }
|
||||
},
|
||||
featured: true,
|
||||
isActive: true
|
||||
},
|
||||
{
|
||||
_id: '4',
|
||||
name: '디지털 마케팅',
|
||||
description: 'SEO, 소셜미디어 마케팅, 온라인 광고를 통해 디지털 마케팅 전략을 수립하고 실행합니다. 데이터 분석을 통한 지속적인 최적화를 제공합니다.',
|
||||
shortDescription: 'SEO, 소셜미디어, 온라인 광고를 통한 디지털 마케팅',
|
||||
icon: 'fas fa-chart-line',
|
||||
category: 'marketing',
|
||||
pricing: {
|
||||
basePrice: 200000,
|
||||
currency: 'KRW',
|
||||
priceType: 'project',
|
||||
priceRange: { min: 200000, max: 1500000 }
|
||||
},
|
||||
featured: true,
|
||||
isActive: true
|
||||
}
|
||||
];
|
||||
|
||||
const mockSettings = {
|
||||
siteName: 'SmartSolTech',
|
||||
siteDescription: '혁신적인 기술 솔루션으로 비즈니스의 성장을 지원합니다',
|
||||
contact: {
|
||||
email: 'info@smartsoltech.kr',
|
||||
phone: '+82-10-1234-5678',
|
||||
address: 'Seoul, South Korea'
|
||||
},
|
||||
social: {
|
||||
facebook: 'https://facebook.com/smartsoltech',
|
||||
twitter: 'https://twitter.com/smartsoltech',
|
||||
linkedin: 'https://linkedin.com/company/smartsoltech',
|
||||
instagram: 'https://instagram.com/smartsoltech'
|
||||
}
|
||||
};
|
||||
|
||||
// Helper function for category names
|
||||
function getCategoryName(category) {
|
||||
const categoryNames = {
|
||||
'web-development': '웹 개발',
|
||||
'mobile-app': '모바일 앱',
|
||||
'ui-ux-design': 'UI/UX 디자인',
|
||||
'branding': '브랜딩',
|
||||
'marketing': '디지털 마케팅'
|
||||
};
|
||||
return categoryNames[category] || category;
|
||||
}
|
||||
|
||||
// Routes
|
||||
app.get('/', (req, res) => {
|
||||
res.render('index', {
|
||||
title: 'SmartSolTech - Innovative Technology Solutions',
|
||||
settings: mockSettings,
|
||||
featuredPortfolio: mockPortfolio.filter(p => p.featured),
|
||||
featuredServices: mockServices.filter(s => s.featured),
|
||||
currentPage: 'home'
|
||||
});
|
||||
});
|
||||
|
||||
app.get('/portfolio', (req, res) => {
|
||||
const category = req.query.category;
|
||||
let filteredPortfolio = mockPortfolio;
|
||||
|
||||
if (category && category !== 'all') {
|
||||
filteredPortfolio = mockPortfolio.filter(p => p.category === category);
|
||||
}
|
||||
|
||||
res.render('portfolio', {
|
||||
title: '포트폴리오 - SmartSolTech',
|
||||
portfolioItems: filteredPortfolio,
|
||||
currentPage: 'portfolio'
|
||||
});
|
||||
});
|
||||
|
||||
app.get('/portfolio/:id', (req, res) => {
|
||||
const portfolio = mockPortfolio.find(p => p._id === req.params.id);
|
||||
|
||||
if (!portfolio) {
|
||||
return res.status(404).render('error', {
|
||||
title: '404 - 페이지를 찾을 수 없습니다',
|
||||
message: '요청하신 포트폴리오 항목을 찾을 수 없습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
const relatedProjects = mockPortfolio.filter(p =>
|
||||
p._id !== portfolio._id && p.category === portfolio.category
|
||||
).slice(0, 3);
|
||||
|
||||
res.render('portfolio-detail', {
|
||||
title: `${portfolio.title} - 포트폴리오`,
|
||||
portfolio,
|
||||
relatedProjects
|
||||
});
|
||||
});
|
||||
|
||||
app.get('/services', (req, res) => {
|
||||
res.render('services', {
|
||||
title: '서비스 - SmartSolTech',
|
||||
services: mockServices,
|
||||
currentPage: 'services'
|
||||
});
|
||||
});
|
||||
|
||||
app.get('/contact', (req, res) => {
|
||||
res.render('contact', {
|
||||
title: '연락처 - SmartSolTech',
|
||||
settings: mockSettings,
|
||||
currentPage: 'contact'
|
||||
});
|
||||
});
|
||||
|
||||
app.post('/contact', (req, res) => {
|
||||
// Simulate contact form processing
|
||||
console.log('Contact form submission:', req.body);
|
||||
|
||||
req.flash('success', '문의가 성공적으로 접수되었습니다. 빠른 시일 내에 답변드리겠습니다.');
|
||||
res.redirect('/contact');
|
||||
});
|
||||
|
||||
app.get('/calculator', (req, res) => {
|
||||
res.render('calculator', {
|
||||
title: '비용 계산기 - SmartSolTech',
|
||||
services: mockServices,
|
||||
currentPage: 'calculator'
|
||||
});
|
||||
});
|
||||
|
||||
// API Routes for calculator
|
||||
app.post('/api/calculator/estimate', (req, res) => {
|
||||
const { service, projectType, timeline, features } = req.body;
|
||||
|
||||
// Simple calculation logic
|
||||
let basePrice = 500000;
|
||||
const selectedService = mockServices.find(s => s._id === service);
|
||||
|
||||
if (selectedService) {
|
||||
basePrice = selectedService.pricing.basePrice;
|
||||
}
|
||||
|
||||
// Apply multipliers based on project complexity
|
||||
const typeMultipliers = {
|
||||
'simple': 1,
|
||||
'medium': 1.5,
|
||||
'complex': 2.5,
|
||||
'enterprise': 4
|
||||
};
|
||||
|
||||
const timelineMultipliers = {
|
||||
'urgent': 1.5,
|
||||
'normal': 1,
|
||||
'flexible': 0.9
|
||||
};
|
||||
|
||||
const typeMultiplier = typeMultipliers[projectType] || 1;
|
||||
const timelineMultiplier = timelineMultipliers[timeline] || 1;
|
||||
const featuresMultiplier = 1 + (features?.length || 0) * 0.2;
|
||||
|
||||
const estimatedPrice = Math.round(basePrice * typeMultiplier * timelineMultiplier * featuresMultiplier);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
estimate: {
|
||||
basePrice,
|
||||
estimatedPrice,
|
||||
breakdown: {
|
||||
basePrice,
|
||||
projectType: projectType,
|
||||
typeMultiplier,
|
||||
timeline,
|
||||
timelineMultiplier,
|
||||
features: features || [],
|
||||
featuresMultiplier
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// API Routes for portfolio interactions
|
||||
app.post('/api/portfolio/:id/like', (req, res) => {
|
||||
const portfolio = mockPortfolio.find(p => p._id === req.params.id);
|
||||
if (portfolio) {
|
||||
portfolio.likes = (portfolio.likes || 0) + 1;
|
||||
res.json({ success: true, likes: portfolio.likes });
|
||||
} else {
|
||||
res.status(404).json({ success: false, message: 'Portfolio not found' });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/portfolio/:id/view', (req, res) => {
|
||||
const portfolio = mockPortfolio.find(p => p._id === req.params.id);
|
||||
if (portfolio) {
|
||||
portfolio.viewCount = (portfolio.viewCount || 0) + 1;
|
||||
res.json({ success: true, viewCount: portfolio.viewCount });
|
||||
} else {
|
||||
res.status(404).json({ success: false, message: 'Portfolio not found' });
|
||||
}
|
||||
});
|
||||
|
||||
// Error handling
|
||||
app.use((req, res) => {
|
||||
res.status(404).render('error', {
|
||||
title: '404 - 페이지를 찾을 수 없습니다',
|
||||
message: '요청하신 페이지를 찾을 수 없습니다.'
|
||||
});
|
||||
});
|
||||
|
||||
app.use((err, req, res, next) => {
|
||||
console.error(err.stack);
|
||||
res.status(500).render('error', {
|
||||
title: '500 - 서버 오류',
|
||||
message: '서버에서 오류가 발생했습니다.'
|
||||
});
|
||||
});
|
||||
|
||||
// Start server
|
||||
app.listen(PORT, () => {
|
||||
console.log(`🚀 SmartSolTech Demo Server running on http://localhost:${PORT}`);
|
||||
console.log('📋 Available pages:');
|
||||
console.log(' • Home: http://localhost:3000/');
|
||||
console.log(' • Portfolio: http://localhost:3000/portfolio');
|
||||
console.log(' • Services: http://localhost:3000/services');
|
||||
console.log(' • Contact: http://localhost:3000/contact');
|
||||
console.log(' • Calculator: http://localhost:3000/calculator');
|
||||
console.log('');
|
||||
console.log('💡 This is a demo version using mock data');
|
||||
console.log('💾 To use with MongoDB, run: npm start');
|
||||
});
|
||||
|
||||
module.exports = app;
|
||||
416
.history/server-demo_20251019163807.js
Normal file
416
.history/server-demo_20251019163807.js
Normal file
@@ -0,0 +1,416 @@
|
||||
/**
|
||||
* Demo server for SmartSolTech website
|
||||
* Uses mock data instead of MongoDB for demonstration
|
||||
*/
|
||||
|
||||
const express = require('express');
|
||||
const path = require('path');
|
||||
const cookieParser = require('cookie-parser');
|
||||
const session = require('express-session');
|
||||
const flash = require('connect-flash');
|
||||
const helmet = require('helmet');
|
||||
const compression = require('compression');
|
||||
const rateLimit = require('express-rate-limit');
|
||||
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 3000;
|
||||
|
||||
// Security middleware
|
||||
app.use(helmet({
|
||||
contentSecurityPolicy: {
|
||||
directives: {
|
||||
defaultSrc: ["'self'"],
|
||||
styleSrc: ["'self'", "'unsafe-inline'", "https://cdn.jsdelivr.net", "https://cdnjs.cloudflare.com", "https://unpkg.com"],
|
||||
scriptSrc: ["'self'", "'unsafe-inline'", "https://cdn.jsdelivr.net", "https://cdnjs.cloudflare.com", "https://unpkg.com"],
|
||||
imgSrc: ["'self'", "data:", "https:", "blob:"],
|
||||
fontSrc: ["'self'", "https://cdnjs.cloudflare.com"],
|
||||
connectSrc: ["'self'", "https:"],
|
||||
mediaSrc: ["'self'"],
|
||||
frameSrc: ["'none'"]
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
// Rate limiting
|
||||
const limiter = rateLimit({
|
||||
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||
max: 100, // limit each IP to 100 requests per windowMs
|
||||
message: 'Too many requests from this IP, please try again later.'
|
||||
});
|
||||
app.use(limiter);
|
||||
|
||||
// Compression
|
||||
app.use(compression());
|
||||
|
||||
// View engine setup
|
||||
app.set('views', path.join(__dirname, 'views'));
|
||||
app.set('view engine', 'ejs');
|
||||
|
||||
// Middleware
|
||||
app.use(express.json({ limit: '10mb' }));
|
||||
app.use(express.urlencoded({ extended: true, limit: '10mb' }));
|
||||
app.use(cookieParser());
|
||||
app.use(express.static(path.join(__dirname, 'public')));
|
||||
|
||||
// Session configuration
|
||||
app.use(session({
|
||||
secret: 'demo-secret-key',
|
||||
resave: false,
|
||||
saveUninitialized: false,
|
||||
cookie: {
|
||||
secure: false, // Set to true in production with HTTPS
|
||||
maxAge: 24 * 60 * 60 * 1000 // 24 hours
|
||||
}
|
||||
}));
|
||||
|
||||
// Flash messages
|
||||
app.use(flash());
|
||||
|
||||
// Global variables for templates
|
||||
app.use((req, res, next) => {
|
||||
res.locals.success_msg = req.flash('success');
|
||||
res.locals.error_msg = req.flash('error');
|
||||
res.locals.error = req.flash('error');
|
||||
res.locals.currentYear = new Date().getFullYear();
|
||||
next();
|
||||
});
|
||||
|
||||
// Mock data
|
||||
const mockPortfolio = [
|
||||
{
|
||||
_id: '1',
|
||||
title: 'E-commerce 플랫폼',
|
||||
description: '현대적인 온라인 쇼핑몰 플랫폼을 개발했습니다. React와 Node.js를 활용하여 높은 성능과 사용자 경험을 제공하며, 결제 시스템과 재고 관리 기능을 포함합니다.',
|
||||
shortDescription: '반응형 온라인 쇼핑몰 플랫폼 개발',
|
||||
category: 'web-development',
|
||||
technologies: ['React', 'Node.js', 'MongoDB', 'Express', 'Stripe', 'AWS'],
|
||||
images: [
|
||||
{ url: '/images/portfolio/ecommerce-1.jpg', alt: 'E-commerce 메인페이지', isPrimary: true }
|
||||
],
|
||||
clientName: '패션 브랜드 ABC',
|
||||
projectUrl: 'https://example-ecommerce.demo',
|
||||
status: 'completed',
|
||||
featured: true,
|
||||
publishedAt: new Date('2024-01-15'),
|
||||
completedAt: new Date('2024-01-10'),
|
||||
isPublished: true,
|
||||
viewCount: 150,
|
||||
likes: 25
|
||||
},
|
||||
{
|
||||
_id: '2',
|
||||
title: '모바일 피트니스 앱',
|
||||
description: 'React Native를 사용하여 크로스플랫폼 피트니스 애플리케이션을 개발했습니다. 운동 계획, 칼로리 추적, 소셜 기능을 포함하여 사용자들의 건강한 라이프스타일을 지원합니다.',
|
||||
shortDescription: '건강 관리를 위한 크로스플랫폼 모바일 앱',
|
||||
category: 'mobile-app',
|
||||
technologies: ['React Native', 'Redux', 'Firebase', 'Node.js', 'PostgreSQL'],
|
||||
images: [
|
||||
{ url: '/images/portfolio/fitness-1.jpg', alt: '피트니스 앱 메인화면', isPrimary: true }
|
||||
],
|
||||
clientName: '헬스케어 스타트업 FIT',
|
||||
status: 'completed',
|
||||
featured: true,
|
||||
publishedAt: new Date('2024-02-20'),
|
||||
completedAt: new Date('2024-02-15'),
|
||||
isPublished: true,
|
||||
viewCount: 200,
|
||||
likes: 35
|
||||
},
|
||||
{
|
||||
_id: '3',
|
||||
title: '기업 웹사이트 리뉴얼',
|
||||
description: '기업의 브랜드 아이덴티티를 반영한 웹사이트 리뉴얼 프로젝트입니다. 사용자 경험을 개선하고 모던한 디자인을 적용하여 브랜드 가치를 높였습니다.',
|
||||
shortDescription: '브랜드 아이덴티티를 반영한 기업 웹사이트 리뉴얼',
|
||||
category: 'ui-ux-design',
|
||||
technologies: ['Figma', 'React', 'Sass', 'Framer Motion', 'Contentful'],
|
||||
images: [
|
||||
{ url: '/images/portfolio/corporate-1.jpg', alt: '기업 웹사이트 메인페이지', isPrimary: true }
|
||||
],
|
||||
clientName: '기술 기업 TechCorp',
|
||||
projectUrl: 'https://example-corp.demo',
|
||||
status: 'completed',
|
||||
featured: true,
|
||||
publishedAt: new Date('2024-03-10'),
|
||||
completedAt: new Date('2024-03-05'),
|
||||
isPublished: true,
|
||||
viewCount: 120,
|
||||
likes: 18
|
||||
}
|
||||
];
|
||||
|
||||
const mockServices = [
|
||||
{
|
||||
_id: '1',
|
||||
name: '웹 개발',
|
||||
description: '현대적이고 반응형인 웹사이트와 웹 애플리케이션을 개발합니다. React, Node.js, MongoDB 등 최신 기술 스택을 활용하여 성능과 사용자 경험을 최적화합니다.',
|
||||
shortDescription: '현대적이고 반응형 웹사이트 및 웹 애플리케이션 개발',
|
||||
icon: 'fas fa-code',
|
||||
category: 'development',
|
||||
pricing: {
|
||||
basePrice: 500000,
|
||||
currency: 'KRW',
|
||||
priceType: 'project',
|
||||
priceRange: { min: 500000, max: 5000000 }
|
||||
},
|
||||
featured: true,
|
||||
isActive: true
|
||||
},
|
||||
{
|
||||
_id: '2',
|
||||
name: '모바일 앱 개발',
|
||||
description: 'iOS와 Android 플랫폼을 위한 네이티브 및 크로스플랫폼 모바일 애플리케이션을 개발합니다. React Native, Flutter 등을 활용하여 효율적인 개발을 진행합니다.',
|
||||
shortDescription: 'iOS/Android 네이티브 및 크로스플랫폼 앱 개발',
|
||||
icon: 'fas fa-mobile-alt',
|
||||
category: 'development',
|
||||
pricing: {
|
||||
basePrice: 800000,
|
||||
currency: 'KRW',
|
||||
priceType: 'project',
|
||||
priceRange: { min: 800000, max: 8000000 }
|
||||
},
|
||||
featured: true,
|
||||
isActive: true
|
||||
},
|
||||
{
|
||||
_id: '3',
|
||||
name: 'UI/UX 디자인',
|
||||
description: '사용자 중심의 직관적이고 아름다운 인터페이스를 디자인합니다. 사용자 경험 연구와 프로토타이핑을 통해 최적의 디자인 솔루션을 제공합니다.',
|
||||
shortDescription: '사용자 중심의 직관적이고 아름다운 인터페이스 디자인',
|
||||
icon: 'fas fa-palette',
|
||||
category: 'design',
|
||||
pricing: {
|
||||
basePrice: 300000,
|
||||
currency: 'KRW',
|
||||
priceType: 'project',
|
||||
priceRange: { min: 300000, max: 2000000 }
|
||||
},
|
||||
featured: true,
|
||||
isActive: true
|
||||
},
|
||||
{
|
||||
_id: '4',
|
||||
name: '디지털 마케팅',
|
||||
description: 'SEO, 소셜미디어 마케팅, 온라인 광고를 통해 디지털 마케팅 전략을 수립하고 실행합니다. 데이터 분석을 통한 지속적인 최적화를 제공합니다.',
|
||||
shortDescription: 'SEO, 소셜미디어, 온라인 광고를 통한 디지털 마케팅',
|
||||
icon: 'fas fa-chart-line',
|
||||
category: 'marketing',
|
||||
pricing: {
|
||||
basePrice: 200000,
|
||||
currency: 'KRW',
|
||||
priceType: 'project',
|
||||
priceRange: { min: 200000, max: 1500000 }
|
||||
},
|
||||
featured: true,
|
||||
isActive: true
|
||||
}
|
||||
];
|
||||
|
||||
const mockSettings = {
|
||||
siteName: 'SmartSolTech',
|
||||
siteDescription: '혁신적인 기술 솔루션으로 비즈니스의 성장을 지원합니다',
|
||||
contact: {
|
||||
email: 'info@smartsoltech.kr',
|
||||
phone: '+82-10-1234-5678',
|
||||
address: 'Seoul, South Korea'
|
||||
},
|
||||
social: {
|
||||
facebook: 'https://facebook.com/smartsoltech',
|
||||
twitter: 'https://twitter.com/smartsoltech',
|
||||
linkedin: 'https://linkedin.com/company/smartsoltech',
|
||||
instagram: 'https://instagram.com/smartsoltech'
|
||||
}
|
||||
};
|
||||
|
||||
// Helper function for category names
|
||||
function getCategoryName(category) {
|
||||
const categoryNames = {
|
||||
'web-development': '웹 개발',
|
||||
'mobile-app': '모바일 앱',
|
||||
'ui-ux-design': 'UI/UX 디자인',
|
||||
'branding': '브랜딩',
|
||||
'marketing': '디지털 마케팅'
|
||||
};
|
||||
return categoryNames[category] || category;
|
||||
}
|
||||
|
||||
// Routes
|
||||
app.get('/', (req, res) => {
|
||||
res.render('index', {
|
||||
title: 'SmartSolTech - Innovative Technology Solutions',
|
||||
settings: mockSettings,
|
||||
featuredPortfolio: mockPortfolio.filter(p => p.featured),
|
||||
featuredServices: mockServices.filter(s => s.featured),
|
||||
currentPage: 'home'
|
||||
});
|
||||
});
|
||||
|
||||
app.get('/portfolio', (req, res) => {
|
||||
const category = req.query.category;
|
||||
let filteredPortfolio = mockPortfolio;
|
||||
|
||||
if (category && category !== 'all') {
|
||||
filteredPortfolio = mockPortfolio.filter(p => p.category === category);
|
||||
}
|
||||
|
||||
res.render('portfolio', {
|
||||
title: '포트폴리오 - SmartSolTech',
|
||||
portfolioItems: filteredPortfolio,
|
||||
currentPage: 'portfolio'
|
||||
});
|
||||
});
|
||||
|
||||
app.get('/portfolio/:id', (req, res) => {
|
||||
const portfolio = mockPortfolio.find(p => p._id === req.params.id);
|
||||
|
||||
if (!portfolio) {
|
||||
return res.status(404).render('error', {
|
||||
title: '404 - 페이지를 찾을 수 없습니다',
|
||||
message: '요청하신 포트폴리오 항목을 찾을 수 없습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
const relatedProjects = mockPortfolio.filter(p =>
|
||||
p._id !== portfolio._id && p.category === portfolio.category
|
||||
).slice(0, 3);
|
||||
|
||||
res.render('portfolio-detail', {
|
||||
title: `${portfolio.title} - 포트폴리오`,
|
||||
portfolio,
|
||||
relatedProjects
|
||||
});
|
||||
});
|
||||
|
||||
app.get('/services', (req, res) => {
|
||||
res.render('services', {
|
||||
title: '서비스 - SmartSolTech',
|
||||
services: mockServices,
|
||||
currentPage: 'services'
|
||||
});
|
||||
});
|
||||
|
||||
app.get('/contact', (req, res) => {
|
||||
res.render('contact', {
|
||||
title: '연락처 - SmartSolTech',
|
||||
settings: mockSettings,
|
||||
currentPage: 'contact'
|
||||
});
|
||||
});
|
||||
|
||||
app.post('/contact', (req, res) => {
|
||||
// Simulate contact form processing
|
||||
console.log('Contact form submission:', req.body);
|
||||
|
||||
req.flash('success', '문의가 성공적으로 접수되었습니다. 빠른 시일 내에 답변드리겠습니다.');
|
||||
res.redirect('/contact');
|
||||
});
|
||||
|
||||
app.get('/calculator', (req, res) => {
|
||||
res.render('calculator', {
|
||||
title: '비용 계산기 - SmartSolTech',
|
||||
services: mockServices,
|
||||
currentPage: 'calculator'
|
||||
});
|
||||
});
|
||||
|
||||
// API Routes for calculator
|
||||
app.post('/api/calculator/estimate', (req, res) => {
|
||||
const { service, projectType, timeline, features } = req.body;
|
||||
|
||||
// Simple calculation logic
|
||||
let basePrice = 500000;
|
||||
const selectedService = mockServices.find(s => s._id === service);
|
||||
|
||||
if (selectedService) {
|
||||
basePrice = selectedService.pricing.basePrice;
|
||||
}
|
||||
|
||||
// Apply multipliers based on project complexity
|
||||
const typeMultipliers = {
|
||||
'simple': 1,
|
||||
'medium': 1.5,
|
||||
'complex': 2.5,
|
||||
'enterprise': 4
|
||||
};
|
||||
|
||||
const timelineMultipliers = {
|
||||
'urgent': 1.5,
|
||||
'normal': 1,
|
||||
'flexible': 0.9
|
||||
};
|
||||
|
||||
const typeMultiplier = typeMultipliers[projectType] || 1;
|
||||
const timelineMultiplier = timelineMultipliers[timeline] || 1;
|
||||
const featuresMultiplier = 1 + (features?.length || 0) * 0.2;
|
||||
|
||||
const estimatedPrice = Math.round(basePrice * typeMultiplier * timelineMultiplier * featuresMultiplier);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
estimate: {
|
||||
basePrice,
|
||||
estimatedPrice,
|
||||
breakdown: {
|
||||
basePrice,
|
||||
projectType: projectType,
|
||||
typeMultiplier,
|
||||
timeline,
|
||||
timelineMultiplier,
|
||||
features: features || [],
|
||||
featuresMultiplier
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// API Routes for portfolio interactions
|
||||
app.post('/api/portfolio/:id/like', (req, res) => {
|
||||
const portfolio = mockPortfolio.find(p => p._id === req.params.id);
|
||||
if (portfolio) {
|
||||
portfolio.likes = (portfolio.likes || 0) + 1;
|
||||
res.json({ success: true, likes: portfolio.likes });
|
||||
} else {
|
||||
res.status(404).json({ success: false, message: 'Portfolio not found' });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/portfolio/:id/view', (req, res) => {
|
||||
const portfolio = mockPortfolio.find(p => p._id === req.params.id);
|
||||
if (portfolio) {
|
||||
portfolio.viewCount = (portfolio.viewCount || 0) + 1;
|
||||
res.json({ success: true, viewCount: portfolio.viewCount });
|
||||
} else {
|
||||
res.status(404).json({ success: false, message: 'Portfolio not found' });
|
||||
}
|
||||
});
|
||||
|
||||
// Error handling
|
||||
app.use((req, res) => {
|
||||
res.status(404).render('error', {
|
||||
title: '404 - 페이지를 찾을 수 없습니다',
|
||||
message: '요청하신 페이지를 찾을 수 없습니다.'
|
||||
});
|
||||
});
|
||||
|
||||
app.use((err, req, res, next) => {
|
||||
console.error(err.stack);
|
||||
res.status(500).render('error', {
|
||||
title: '500 - 서버 오류',
|
||||
message: '서버에서 오류가 발생했습니다.'
|
||||
});
|
||||
});
|
||||
|
||||
// Start server
|
||||
app.listen(PORT, () => {
|
||||
console.log(`🚀 SmartSolTech Demo Server running on http://localhost:${PORT}`);
|
||||
console.log('📋 Available pages:');
|
||||
console.log(' • Home: http://localhost:3000/');
|
||||
console.log(' • Portfolio: http://localhost:3000/portfolio');
|
||||
console.log(' • Services: http://localhost:3000/services');
|
||||
console.log(' • Contact: http://localhost:3000/contact');
|
||||
console.log(' • Calculator: http://localhost:3000/calculator');
|
||||
console.log('');
|
||||
console.log('💡 This is a demo version using mock data');
|
||||
console.log('💾 To use with MongoDB, run: npm start');
|
||||
});
|
||||
|
||||
module.exports = app;
|
||||
418
.history/server-demo_20251019164141.js
Normal file
418
.history/server-demo_20251019164141.js
Normal file
@@ -0,0 +1,418 @@
|
||||
/**
|
||||
* Demo server for SmartSolTech website
|
||||
* Uses mock data instead of MongoDB for demonstration
|
||||
*/
|
||||
|
||||
const express = require('express');
|
||||
const path = require('path');
|
||||
const cookieParser = require('cookie-parser');
|
||||
const session = require('express-session');
|
||||
const flash = require('connect-flash');
|
||||
const helmet = require('helmet');
|
||||
const compression = require('compression');
|
||||
const rateLimit = require('express-rate-limit');
|
||||
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 3000;
|
||||
|
||||
// Security middleware
|
||||
app.use(helmet({
|
||||
contentSecurityPolicy: {
|
||||
directives: {
|
||||
defaultSrc: ["'self'"],
|
||||
styleSrc: ["'self'", "'unsafe-inline'", "https://cdn.jsdelivr.net", "https://cdnjs.cloudflare.com", "https://unpkg.com"],
|
||||
scriptSrc: ["'self'", "'unsafe-inline'", "https://cdn.jsdelivr.net", "https://cdnjs.cloudflare.com", "https://unpkg.com"],
|
||||
imgSrc: ["'self'", "data:", "https:", "blob:"],
|
||||
fontSrc: ["'self'", "https://cdnjs.cloudflare.com"],
|
||||
connectSrc: ["'self'", "https:"],
|
||||
mediaSrc: ["'self'"],
|
||||
frameSrc: ["'none'"]
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
// Rate limiting
|
||||
const limiter = rateLimit({
|
||||
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||
max: 100, // limit each IP to 100 requests per windowMs
|
||||
message: 'Too many requests from this IP, please try again later.'
|
||||
});
|
||||
app.use(limiter);
|
||||
|
||||
// Compression
|
||||
app.use(compression());
|
||||
|
||||
// View engine setup
|
||||
app.set('views', path.join(__dirname, 'views'));
|
||||
app.set('view engine', 'ejs');
|
||||
|
||||
// Middleware
|
||||
app.use(express.json({ limit: '10mb' }));
|
||||
app.use(express.urlencoded({ extended: true, limit: '10mb' }));
|
||||
app.use(cookieParser());
|
||||
app.use(express.static(path.join(__dirname, 'public')));
|
||||
|
||||
// Session configuration
|
||||
app.use(session({
|
||||
secret: 'demo-secret-key',
|
||||
resave: false,
|
||||
saveUninitialized: false,
|
||||
cookie: {
|
||||
secure: false, // Set to true in production with HTTPS
|
||||
maxAge: 24 * 60 * 60 * 1000 // 24 hours
|
||||
}
|
||||
}));
|
||||
|
||||
// Flash messages
|
||||
app.use(flash());
|
||||
|
||||
// Global variables for templates
|
||||
app.use((req, res, next) => {
|
||||
res.locals.success_msg = req.flash('success');
|
||||
res.locals.error_msg = req.flash('error');
|
||||
res.locals.error = req.flash('error');
|
||||
res.locals.currentYear = new Date().getFullYear();
|
||||
next();
|
||||
});
|
||||
|
||||
// Mock data
|
||||
const mockPortfolio = [
|
||||
{
|
||||
_id: '1',
|
||||
title: 'E-commerce 플랫폼',
|
||||
description: '현대적인 온라인 쇼핑몰 플랫폼을 개발했습니다. React와 Node.js를 활용하여 높은 성능과 사용자 경험을 제공하며, 결제 시스템과 재고 관리 기능을 포함합니다.',
|
||||
shortDescription: '반응형 온라인 쇼핑몰 플랫폼 개발',
|
||||
category: 'web-development',
|
||||
technologies: ['React', 'Node.js', 'MongoDB', 'Express', 'Stripe', 'AWS'],
|
||||
images: [
|
||||
{ url: '/images/portfolio/ecommerce-1.jpg', alt: 'E-commerce 메인페이지', isPrimary: true }
|
||||
],
|
||||
clientName: '패션 브랜드 ABC',
|
||||
projectUrl: 'https://example-ecommerce.demo',
|
||||
status: 'completed',
|
||||
featured: true,
|
||||
publishedAt: new Date('2024-01-15'),
|
||||
completedAt: new Date('2024-01-10'),
|
||||
isPublished: true,
|
||||
viewCount: 150,
|
||||
likes: 25
|
||||
},
|
||||
{
|
||||
_id: '2',
|
||||
title: '모바일 피트니스 앱',
|
||||
description: 'React Native를 사용하여 크로스플랫폼 피트니스 애플리케이션을 개발했습니다. 운동 계획, 칼로리 추적, 소셜 기능을 포함하여 사용자들의 건강한 라이프스타일을 지원합니다.',
|
||||
shortDescription: '건강 관리를 위한 크로스플랫폼 모바일 앱',
|
||||
category: 'mobile-app',
|
||||
technologies: ['React Native', 'Redux', 'Firebase', 'Node.js', 'PostgreSQL'],
|
||||
images: [
|
||||
{ url: '/images/portfolio/fitness-1.jpg', alt: '피트니스 앱 메인화면', isPrimary: true }
|
||||
],
|
||||
clientName: '헬스케어 스타트업 FIT',
|
||||
status: 'completed',
|
||||
featured: true,
|
||||
publishedAt: new Date('2024-02-20'),
|
||||
completedAt: new Date('2024-02-15'),
|
||||
isPublished: true,
|
||||
viewCount: 200,
|
||||
likes: 35
|
||||
},
|
||||
{
|
||||
_id: '3',
|
||||
title: '기업 웹사이트 리뉴얼',
|
||||
description: '기업의 브랜드 아이덴티티를 반영한 웹사이트 리뉴얼 프로젝트입니다. 사용자 경험을 개선하고 모던한 디자인을 적용하여 브랜드 가치를 높였습니다.',
|
||||
shortDescription: '브랜드 아이덴티티를 반영한 기업 웹사이트 리뉴얼',
|
||||
category: 'ui-ux-design',
|
||||
technologies: ['Figma', 'React', 'Sass', 'Framer Motion', 'Contentful'],
|
||||
images: [
|
||||
{ url: '/images/portfolio/corporate-1.jpg', alt: '기업 웹사이트 메인페이지', isPrimary: true }
|
||||
],
|
||||
clientName: '기술 기업 TechCorp',
|
||||
projectUrl: 'https://example-corp.demo',
|
||||
status: 'completed',
|
||||
featured: true,
|
||||
publishedAt: new Date('2024-03-10'),
|
||||
completedAt: new Date('2024-03-05'),
|
||||
isPublished: true,
|
||||
viewCount: 120,
|
||||
likes: 18
|
||||
}
|
||||
];
|
||||
|
||||
const mockServices = [
|
||||
{
|
||||
_id: '1',
|
||||
name: '웹 개발',
|
||||
description: '현대적이고 반응형인 웹사이트와 웹 애플리케이션을 개발합니다. React, Node.js, MongoDB 등 최신 기술 스택을 활용하여 성능과 사용자 경험을 최적화합니다.',
|
||||
shortDescription: '현대적이고 반응형 웹사이트 및 웹 애플리케이션 개발',
|
||||
icon: 'fas fa-code',
|
||||
category: 'development',
|
||||
pricing: {
|
||||
basePrice: 500000,
|
||||
currency: 'KRW',
|
||||
priceType: 'project',
|
||||
priceRange: { min: 500000, max: 5000000 }
|
||||
},
|
||||
featured: true,
|
||||
isActive: true
|
||||
},
|
||||
{
|
||||
_id: '2',
|
||||
name: '모바일 앱 개발',
|
||||
description: 'iOS와 Android 플랫폼을 위한 네이티브 및 크로스플랫폼 모바일 애플리케이션을 개발합니다. React Native, Flutter 등을 활용하여 효율적인 개발을 진행합니다.',
|
||||
shortDescription: 'iOS/Android 네이티브 및 크로스플랫폼 앱 개발',
|
||||
icon: 'fas fa-mobile-alt',
|
||||
category: 'development',
|
||||
pricing: {
|
||||
basePrice: 800000,
|
||||
currency: 'KRW',
|
||||
priceType: 'project',
|
||||
priceRange: { min: 800000, max: 8000000 }
|
||||
},
|
||||
featured: true,
|
||||
isActive: true
|
||||
},
|
||||
{
|
||||
_id: '3',
|
||||
name: 'UI/UX 디자인',
|
||||
description: '사용자 중심의 직관적이고 아름다운 인터페이스를 디자인합니다. 사용자 경험 연구와 프로토타이핑을 통해 최적의 디자인 솔루션을 제공합니다.',
|
||||
shortDescription: '사용자 중심의 직관적이고 아름다운 인터페이스 디자인',
|
||||
icon: 'fas fa-palette',
|
||||
category: 'design',
|
||||
pricing: {
|
||||
basePrice: 300000,
|
||||
currency: 'KRW',
|
||||
priceType: 'project',
|
||||
priceRange: { min: 300000, max: 2000000 }
|
||||
},
|
||||
featured: true,
|
||||
isActive: true
|
||||
},
|
||||
{
|
||||
_id: '4',
|
||||
name: '디지털 마케팅',
|
||||
description: 'SEO, 소셜미디어 마케팅, 온라인 광고를 통해 디지털 마케팅 전략을 수립하고 실행합니다. 데이터 분석을 통한 지속적인 최적화를 제공합니다.',
|
||||
shortDescription: 'SEO, 소셜미디어, 온라인 광고를 통한 디지털 마케팅',
|
||||
icon: 'fas fa-chart-line',
|
||||
category: 'marketing',
|
||||
pricing: {
|
||||
basePrice: 200000,
|
||||
currency: 'KRW',
|
||||
priceType: 'project',
|
||||
priceRange: { min: 200000, max: 1500000 }
|
||||
},
|
||||
featured: true,
|
||||
isActive: true
|
||||
}
|
||||
];
|
||||
|
||||
const mockSettings = {
|
||||
siteName: 'SmartSolTech',
|
||||
siteDescription: '혁신적인 기술 솔루션으로 비즈니스의 성장을 지원합니다',
|
||||
contact: {
|
||||
email: 'info@smartsoltech.kr',
|
||||
phone: '+82-10-1234-5678',
|
||||
address: 'Seoul, South Korea'
|
||||
},
|
||||
social: {
|
||||
facebook: 'https://facebook.com/smartsoltech',
|
||||
twitter: 'https://twitter.com/smartsoltech',
|
||||
linkedin: 'https://linkedin.com/company/smartsoltech',
|
||||
instagram: 'https://instagram.com/smartsoltech'
|
||||
}
|
||||
};
|
||||
|
||||
// Helper function for category names
|
||||
function getCategoryName(category) {
|
||||
const categoryNames = {
|
||||
'web-development': '웹 개발',
|
||||
'mobile-app': '모바일 앱',
|
||||
'ui-ux-design': 'UI/UX 디자인',
|
||||
'branding': '브랜딩',
|
||||
'marketing': '디지털 마케팅'
|
||||
};
|
||||
return categoryNames[category] || category;
|
||||
}
|
||||
|
||||
// Routes
|
||||
app.get('/', (req, res) => {
|
||||
res.render('index', {
|
||||
title: 'SmartSolTech - Innovative Technology Solutions',
|
||||
settings: mockSettings,
|
||||
featuredPortfolio: mockPortfolio.filter(p => p.featured),
|
||||
featuredServices: mockServices.filter(s => s.featured),
|
||||
currentPage: 'home'
|
||||
});
|
||||
});
|
||||
|
||||
app.get('/portfolio', (req, res) => {
|
||||
const category = req.query.category;
|
||||
let filteredPortfolio = mockPortfolio;
|
||||
|
||||
if (category && category !== 'all') {
|
||||
filteredPortfolio = mockPortfolio.filter(p => p.category === category);
|
||||
}
|
||||
|
||||
res.render('portfolio', {
|
||||
title: '포트폴리오 - SmartSolTech',
|
||||
portfolioItems: filteredPortfolio,
|
||||
currentPage: 'portfolio'
|
||||
});
|
||||
});
|
||||
|
||||
app.get('/portfolio/:id', (req, res) => {
|
||||
const portfolio = mockPortfolio.find(p => p._id === req.params.id);
|
||||
|
||||
if (!portfolio) {
|
||||
return res.status(404).render('error', {
|
||||
title: '404 - 페이지를 찾을 수 없습니다',
|
||||
message: '요청하신 포트폴리오 항목을 찾을 수 없습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
const relatedProjects = mockPortfolio.filter(p =>
|
||||
p._id !== portfolio._id && p.category === portfolio.category
|
||||
).slice(0, 3);
|
||||
|
||||
res.render('portfolio-detail', {
|
||||
title: `${portfolio.title} - 포트폴리오`,
|
||||
portfolio,
|
||||
relatedProjects
|
||||
});
|
||||
});
|
||||
|
||||
app.get('/services', (req, res) => {
|
||||
res.render('services', {
|
||||
title: '서비스 - SmartSolTech',
|
||||
services: mockServices,
|
||||
currentPage: 'services'
|
||||
});
|
||||
});
|
||||
|
||||
app.get('/contact', (req, res) => {
|
||||
res.render('contact', {
|
||||
title: '연락처 - SmartSolTech',
|
||||
settings: mockSettings,
|
||||
currentPage: 'contact'
|
||||
});
|
||||
});
|
||||
|
||||
app.post('/contact', (req, res) => {
|
||||
// Simulate contact form processing
|
||||
console.log('Contact form submission:', req.body);
|
||||
|
||||
req.flash('success', '문의가 성공적으로 접수되었습니다. 빠른 시일 내에 답변드리겠습니다.');
|
||||
res.redirect('/contact');
|
||||
});
|
||||
|
||||
app.get('/calculator', (req, res) => {
|
||||
res.render('calculator', {
|
||||
title: '비용 계산기 - SmartSolTech',
|
||||
services: mockServices,
|
||||
currentPage: 'calculator'
|
||||
});
|
||||
});
|
||||
|
||||
// API Routes for calculator
|
||||
app.post('/api/calculator/estimate', (req, res) => {
|
||||
const { service, projectType, timeline, features } = req.body;
|
||||
|
||||
// Simple calculation logic
|
||||
let basePrice = 500000;
|
||||
const selectedService = mockServices.find(s => s._id === service);
|
||||
|
||||
if (selectedService) {
|
||||
basePrice = selectedService.pricing.basePrice;
|
||||
}
|
||||
|
||||
// Apply multipliers based on project complexity
|
||||
const typeMultipliers = {
|
||||
'simple': 1,
|
||||
'medium': 1.5,
|
||||
'complex': 2.5,
|
||||
'enterprise': 4
|
||||
};
|
||||
|
||||
const timelineMultipliers = {
|
||||
'urgent': 1.5,
|
||||
'normal': 1,
|
||||
'flexible': 0.9
|
||||
};
|
||||
|
||||
const typeMultiplier = typeMultipliers[projectType] || 1;
|
||||
const timelineMultiplier = timelineMultipliers[timeline] || 1;
|
||||
const featuresMultiplier = 1 + (features?.length || 0) * 0.2;
|
||||
|
||||
const estimatedPrice = Math.round(basePrice * typeMultiplier * timelineMultiplier * featuresMultiplier);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
estimate: {
|
||||
basePrice,
|
||||
estimatedPrice,
|
||||
breakdown: {
|
||||
basePrice,
|
||||
projectType: projectType,
|
||||
typeMultiplier,
|
||||
timeline,
|
||||
timelineMultiplier,
|
||||
features: features || [],
|
||||
featuresMultiplier
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// API Routes for portfolio interactions
|
||||
app.post('/api/portfolio/:id/like', (req, res) => {
|
||||
const portfolio = mockPortfolio.find(p => p._id === req.params.id);
|
||||
if (portfolio) {
|
||||
portfolio.likes = (portfolio.likes || 0) + 1;
|
||||
res.json({ success: true, likes: portfolio.likes });
|
||||
} else {
|
||||
res.status(404).json({ success: false, message: 'Portfolio not found' });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/portfolio/:id/view', (req, res) => {
|
||||
const portfolio = mockPortfolio.find(p => p._id === req.params.id);
|
||||
if (portfolio) {
|
||||
portfolio.viewCount = (portfolio.viewCount || 0) + 1;
|
||||
res.json({ success: true, viewCount: portfolio.viewCount });
|
||||
} else {
|
||||
res.status(404).json({ success: false, message: 'Portfolio not found' });
|
||||
}
|
||||
});
|
||||
|
||||
// Error handling
|
||||
app.use((req, res) => {
|
||||
res.status(404).render('error', {
|
||||
title: '404 - 페이지를 찾을 수 없습니다',
|
||||
message: '요청하신 페이지를 찾을 수 없습니다.',
|
||||
currentPage: '404'
|
||||
});
|
||||
});
|
||||
|
||||
app.use((err, req, res, next) => {
|
||||
console.error(err.stack);
|
||||
res.status(500).render('error', {
|
||||
title: '500 - 서버 오류',
|
||||
message: '서버에서 오류가 발생했습니다.',
|
||||
currentPage: 'error'
|
||||
});
|
||||
});
|
||||
|
||||
// Start server
|
||||
app.listen(PORT, () => {
|
||||
console.log(`🚀 SmartSolTech Demo Server running on http://localhost:${PORT}`);
|
||||
console.log('📋 Available pages:');
|
||||
console.log(' • Home: http://localhost:3000/');
|
||||
console.log(' • Portfolio: http://localhost:3000/portfolio');
|
||||
console.log(' • Services: http://localhost:3000/services');
|
||||
console.log(' • Contact: http://localhost:3000/contact');
|
||||
console.log(' • Calculator: http://localhost:3000/calculator');
|
||||
console.log('');
|
||||
console.log('💡 This is a demo version using mock data');
|
||||
console.log('💾 To use with MongoDB, run: npm start');
|
||||
});
|
||||
|
||||
module.exports = app;
|
||||
418
.history/server-demo_20251019165556.js
Normal file
418
.history/server-demo_20251019165556.js
Normal file
@@ -0,0 +1,418 @@
|
||||
/**
|
||||
* Demo server for SmartSolTech website
|
||||
* Uses mock data instead of MongoDB for demonstration
|
||||
*/
|
||||
|
||||
const express = require('express');
|
||||
const path = require('path');
|
||||
const cookieParser = require('cookie-parser');
|
||||
const session = require('express-session');
|
||||
const flash = require('connect-flash');
|
||||
const helmet = require('helmet');
|
||||
const compression = require('compression');
|
||||
const rateLimit = require('express-rate-limit');
|
||||
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 3000;
|
||||
|
||||
// Security middleware
|
||||
app.use(helmet({
|
||||
contentSecurityPolicy: {
|
||||
directives: {
|
||||
defaultSrc: ["'self'"],
|
||||
styleSrc: ["'self'", "'unsafe-inline'", "https://cdn.jsdelivr.net", "https://cdnjs.cloudflare.com", "https://unpkg.com"],
|
||||
scriptSrc: ["'self'", "'unsafe-inline'", "https://cdn.jsdelivr.net", "https://cdnjs.cloudflare.com", "https://unpkg.com"],
|
||||
imgSrc: ["'self'", "data:", "https:", "blob:"],
|
||||
fontSrc: ["'self'", "https://cdnjs.cloudflare.com"],
|
||||
connectSrc: ["'self'", "https:"],
|
||||
mediaSrc: ["'self'"],
|
||||
frameSrc: ["'none'"]
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
// Rate limiting
|
||||
const limiter = rateLimit({
|
||||
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||
max: 100, // limit each IP to 100 requests per windowMs
|
||||
message: 'Too many requests from this IP, please try again later.'
|
||||
});
|
||||
app.use(limiter);
|
||||
|
||||
// Compression
|
||||
app.use(compression());
|
||||
|
||||
// View engine setup
|
||||
app.set('views', path.join(__dirname, 'views'));
|
||||
app.set('view engine', 'ejs');
|
||||
|
||||
// Middleware
|
||||
app.use(express.json({ limit: '10mb' }));
|
||||
app.use(express.urlencoded({ extended: true, limit: '10mb' }));
|
||||
app.use(cookieParser());
|
||||
app.use(express.static(path.join(__dirname, 'public')));
|
||||
|
||||
// Session configuration
|
||||
app.use(session({
|
||||
secret: 'demo-secret-key',
|
||||
resave: false,
|
||||
saveUninitialized: false,
|
||||
cookie: {
|
||||
secure: false, // Set to true in production with HTTPS
|
||||
maxAge: 24 * 60 * 60 * 1000 // 24 hours
|
||||
}
|
||||
}));
|
||||
|
||||
// Flash messages
|
||||
app.use(flash());
|
||||
|
||||
// Global variables for templates
|
||||
app.use((req, res, next) => {
|
||||
res.locals.success_msg = req.flash('success');
|
||||
res.locals.error_msg = req.flash('error');
|
||||
res.locals.error = req.flash('error');
|
||||
res.locals.currentYear = new Date().getFullYear();
|
||||
next();
|
||||
});
|
||||
|
||||
// Mock data
|
||||
const mockPortfolio = [
|
||||
{
|
||||
_id: '1',
|
||||
title: 'E-commerce 플랫폼',
|
||||
description: '현대적인 온라인 쇼핑몰 플랫폼을 개발했습니다. React와 Node.js를 활용하여 높은 성능과 사용자 경험을 제공하며, 결제 시스템과 재고 관리 기능을 포함합니다.',
|
||||
shortDescription: '반응형 온라인 쇼핑몰 플랫폼 개발',
|
||||
category: 'web-development',
|
||||
technologies: ['React', 'Node.js', 'MongoDB', 'Express', 'Stripe', 'AWS'],
|
||||
images: [
|
||||
{ url: '/images/portfolio/ecommerce-1.jpg', alt: 'E-commerce 메인페이지', isPrimary: true }
|
||||
],
|
||||
clientName: '패션 브랜드 ABC',
|
||||
projectUrl: 'https://example-ecommerce.demo',
|
||||
status: 'completed',
|
||||
featured: true,
|
||||
publishedAt: new Date('2024-01-15'),
|
||||
completedAt: new Date('2024-01-10'),
|
||||
isPublished: true,
|
||||
viewCount: 150,
|
||||
likes: 25
|
||||
},
|
||||
{
|
||||
_id: '2',
|
||||
title: '모바일 피트니스 앱',
|
||||
description: 'React Native를 사용하여 크로스플랫폼 피트니스 애플리케이션을 개발했습니다. 운동 계획, 칼로리 추적, 소셜 기능을 포함하여 사용자들의 건강한 라이프스타일을 지원합니다.',
|
||||
shortDescription: '건강 관리를 위한 크로스플랫폼 모바일 앱',
|
||||
category: 'mobile-app',
|
||||
technologies: ['React Native', 'Redux', 'Firebase', 'Node.js', 'PostgreSQL'],
|
||||
images: [
|
||||
{ url: '/images/portfolio/fitness-1.jpg', alt: '피트니스 앱 메인화면', isPrimary: true }
|
||||
],
|
||||
clientName: '헬스케어 스타트업 FIT',
|
||||
status: 'completed',
|
||||
featured: true,
|
||||
publishedAt: new Date('2024-02-20'),
|
||||
completedAt: new Date('2024-02-15'),
|
||||
isPublished: true,
|
||||
viewCount: 200,
|
||||
likes: 35
|
||||
},
|
||||
{
|
||||
_id: '3',
|
||||
title: '기업 웹사이트 리뉴얼',
|
||||
description: '기업의 브랜드 아이덴티티를 반영한 웹사이트 리뉴얼 프로젝트입니다. 사용자 경험을 개선하고 모던한 디자인을 적용하여 브랜드 가치를 높였습니다.',
|
||||
shortDescription: '브랜드 아이덴티티를 반영한 기업 웹사이트 리뉴얼',
|
||||
category: 'ui-ux-design',
|
||||
technologies: ['Figma', 'React', 'Sass', 'Framer Motion', 'Contentful'],
|
||||
images: [
|
||||
{ url: '/images/portfolio/corporate-1.jpg', alt: '기업 웹사이트 메인페이지', isPrimary: true }
|
||||
],
|
||||
clientName: '기술 기업 TechCorp',
|
||||
projectUrl: 'https://example-corp.demo',
|
||||
status: 'completed',
|
||||
featured: true,
|
||||
publishedAt: new Date('2024-03-10'),
|
||||
completedAt: new Date('2024-03-05'),
|
||||
isPublished: true,
|
||||
viewCount: 120,
|
||||
likes: 18
|
||||
}
|
||||
];
|
||||
|
||||
const mockServices = [
|
||||
{
|
||||
_id: '1',
|
||||
name: '웹 개발',
|
||||
description: '현대적이고 반응형인 웹사이트와 웹 애플리케이션을 개발합니다. React, Node.js, MongoDB 등 최신 기술 스택을 활용하여 성능과 사용자 경험을 최적화합니다.',
|
||||
shortDescription: '현대적이고 반응형 웹사이트 및 웹 애플리케이션 개발',
|
||||
icon: 'fas fa-code',
|
||||
category: 'development',
|
||||
pricing: {
|
||||
basePrice: 500000,
|
||||
currency: 'KRW',
|
||||
priceType: 'project',
|
||||
priceRange: { min: 500000, max: 5000000 }
|
||||
},
|
||||
featured: true,
|
||||
isActive: true
|
||||
},
|
||||
{
|
||||
_id: '2',
|
||||
name: '모바일 앱 개발',
|
||||
description: 'iOS와 Android 플랫폼을 위한 네이티브 및 크로스플랫폼 모바일 애플리케이션을 개발합니다. React Native, Flutter 등을 활용하여 효율적인 개발을 진행합니다.',
|
||||
shortDescription: 'iOS/Android 네이티브 및 크로스플랫폼 앱 개발',
|
||||
icon: 'fas fa-mobile-alt',
|
||||
category: 'development',
|
||||
pricing: {
|
||||
basePrice: 800000,
|
||||
currency: 'KRW',
|
||||
priceType: 'project',
|
||||
priceRange: { min: 800000, max: 8000000 }
|
||||
},
|
||||
featured: true,
|
||||
isActive: true
|
||||
},
|
||||
{
|
||||
_id: '3',
|
||||
name: 'UI/UX 디자인',
|
||||
description: '사용자 중심의 직관적이고 아름다운 인터페이스를 디자인합니다. 사용자 경험 연구와 프로토타이핑을 통해 최적의 디자인 솔루션을 제공합니다.',
|
||||
shortDescription: '사용자 중심의 직관적이고 아름다운 인터페이스 디자인',
|
||||
icon: 'fas fa-palette',
|
||||
category: 'design',
|
||||
pricing: {
|
||||
basePrice: 300000,
|
||||
currency: 'KRW',
|
||||
priceType: 'project',
|
||||
priceRange: { min: 300000, max: 2000000 }
|
||||
},
|
||||
featured: true,
|
||||
isActive: true
|
||||
},
|
||||
{
|
||||
_id: '4',
|
||||
name: '디지털 마케팅',
|
||||
description: 'SEO, 소셜미디어 마케팅, 온라인 광고를 통해 디지털 마케팅 전략을 수립하고 실행합니다. 데이터 분석을 통한 지속적인 최적화를 제공합니다.',
|
||||
shortDescription: 'SEO, 소셜미디어, 온라인 광고를 통한 디지털 마케팅',
|
||||
icon: 'fas fa-chart-line',
|
||||
category: 'marketing',
|
||||
pricing: {
|
||||
basePrice: 200000,
|
||||
currency: 'KRW',
|
||||
priceType: 'project',
|
||||
priceRange: { min: 200000, max: 1500000 }
|
||||
},
|
||||
featured: true,
|
||||
isActive: true
|
||||
}
|
||||
];
|
||||
|
||||
const mockSettings = {
|
||||
siteName: 'SmartSolTech',
|
||||
siteDescription: '혁신적인 기술 솔루션으로 비즈니스의 성장을 지원합니다',
|
||||
contact: {
|
||||
email: 'info@smartsoltech.kr',
|
||||
phone: '+82-10-1234-5678',
|
||||
address: 'Seoul, South Korea'
|
||||
},
|
||||
social: {
|
||||
facebook: 'https://facebook.com/smartsoltech',
|
||||
twitter: 'https://twitter.com/smartsoltech',
|
||||
linkedin: 'https://linkedin.com/company/smartsoltech',
|
||||
instagram: 'https://instagram.com/smartsoltech'
|
||||
}
|
||||
};
|
||||
|
||||
// Helper function for category names
|
||||
function getCategoryName(category) {
|
||||
const categoryNames = {
|
||||
'web-development': '웹 개발',
|
||||
'mobile-app': '모바일 앱',
|
||||
'ui-ux-design': 'UI/UX 디자인',
|
||||
'branding': '브랜딩',
|
||||
'marketing': '디지털 마케팅'
|
||||
};
|
||||
return categoryNames[category] || category;
|
||||
}
|
||||
|
||||
// Routes
|
||||
app.get('/', (req, res) => {
|
||||
res.render('index', {
|
||||
title: 'SmartSolTech - Innovative Technology Solutions',
|
||||
settings: mockSettings,
|
||||
featuredPortfolio: mockPortfolio.filter(p => p.featured),
|
||||
featuredServices: mockServices.filter(s => s.featured),
|
||||
currentPage: 'home'
|
||||
});
|
||||
});
|
||||
|
||||
app.get('/portfolio', (req, res) => {
|
||||
const category = req.query.category;
|
||||
let filteredPortfolio = mockPortfolio;
|
||||
|
||||
if (category && category !== 'all') {
|
||||
filteredPortfolio = mockPortfolio.filter(p => p.category === category);
|
||||
}
|
||||
|
||||
res.render('portfolio', {
|
||||
title: '포트폴리오 - SmartSolTech',
|
||||
portfolioItems: filteredPortfolio,
|
||||
currentPage: 'portfolio'
|
||||
});
|
||||
});
|
||||
|
||||
app.get('/portfolio/:id', (req, res) => {
|
||||
const portfolio = mockPortfolio.find(p => p._id === req.params.id);
|
||||
|
||||
if (!portfolio) {
|
||||
return res.status(404).render('error', {
|
||||
title: '404 - 페이지를 찾을 수 없습니다',
|
||||
message: '요청하신 포트폴리오 항목을 찾을 수 없습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
const relatedProjects = mockPortfolio.filter(p =>
|
||||
p._id !== portfolio._id && p.category === portfolio.category
|
||||
).slice(0, 3);
|
||||
|
||||
res.render('portfolio-detail', {
|
||||
title: `${portfolio.title} - 포트폴리오`,
|
||||
portfolio,
|
||||
relatedProjects
|
||||
});
|
||||
});
|
||||
|
||||
app.get('/services', (req, res) => {
|
||||
res.render('services', {
|
||||
title: '서비스 - SmartSolTech',
|
||||
services: mockServices,
|
||||
currentPage: 'services'
|
||||
});
|
||||
});
|
||||
|
||||
app.get('/contact', (req, res) => {
|
||||
res.render('contact', {
|
||||
title: '연락처 - SmartSolTech',
|
||||
settings: mockSettings,
|
||||
currentPage: 'contact'
|
||||
});
|
||||
});
|
||||
|
||||
app.post('/contact', (req, res) => {
|
||||
// Simulate contact form processing
|
||||
console.log('Contact form submission:', req.body);
|
||||
|
||||
req.flash('success', '문의가 성공적으로 접수되었습니다. 빠른 시일 내에 답변드리겠습니다.');
|
||||
res.redirect('/contact');
|
||||
});
|
||||
|
||||
app.get('/calculator', (req, res) => {
|
||||
res.render('calculator', {
|
||||
title: '비용 계산기 - SmartSolTech',
|
||||
services: mockServices,
|
||||
currentPage: 'calculator'
|
||||
});
|
||||
});
|
||||
|
||||
// API Routes for calculator
|
||||
app.post('/api/calculator/estimate', (req, res) => {
|
||||
const { service, projectType, timeline, features } = req.body;
|
||||
|
||||
// Simple calculation logic
|
||||
let basePrice = 500000;
|
||||
const selectedService = mockServices.find(s => s._id === service);
|
||||
|
||||
if (selectedService) {
|
||||
basePrice = selectedService.pricing.basePrice;
|
||||
}
|
||||
|
||||
// Apply multipliers based on project complexity
|
||||
const typeMultipliers = {
|
||||
'simple': 1,
|
||||
'medium': 1.5,
|
||||
'complex': 2.5,
|
||||
'enterprise': 4
|
||||
};
|
||||
|
||||
const timelineMultipliers = {
|
||||
'urgent': 1.5,
|
||||
'normal': 1,
|
||||
'flexible': 0.9
|
||||
};
|
||||
|
||||
const typeMultiplier = typeMultipliers[projectType] || 1;
|
||||
const timelineMultiplier = timelineMultipliers[timeline] || 1;
|
||||
const featuresMultiplier = 1 + (features?.length || 0) * 0.2;
|
||||
|
||||
const estimatedPrice = Math.round(basePrice * typeMultiplier * timelineMultiplier * featuresMultiplier);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
estimate: {
|
||||
basePrice,
|
||||
estimatedPrice,
|
||||
breakdown: {
|
||||
basePrice,
|
||||
projectType: projectType,
|
||||
typeMultiplier,
|
||||
timeline,
|
||||
timelineMultiplier,
|
||||
features: features || [],
|
||||
featuresMultiplier
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// API Routes for portfolio interactions
|
||||
app.post('/api/portfolio/:id/like', (req, res) => {
|
||||
const portfolio = mockPortfolio.find(p => p._id === req.params.id);
|
||||
if (portfolio) {
|
||||
portfolio.likes = (portfolio.likes || 0) + 1;
|
||||
res.json({ success: true, likes: portfolio.likes });
|
||||
} else {
|
||||
res.status(404).json({ success: false, message: 'Portfolio not found' });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/portfolio/:id/view', (req, res) => {
|
||||
const portfolio = mockPortfolio.find(p => p._id === req.params.id);
|
||||
if (portfolio) {
|
||||
portfolio.viewCount = (portfolio.viewCount || 0) + 1;
|
||||
res.json({ success: true, viewCount: portfolio.viewCount });
|
||||
} else {
|
||||
res.status(404).json({ success: false, message: 'Portfolio not found' });
|
||||
}
|
||||
});
|
||||
|
||||
// Error handling
|
||||
app.use((req, res) => {
|
||||
res.status(404).render('error', {
|
||||
title: '404 - 페이지를 찾을 수 없습니다',
|
||||
message: '요청하신 페이지를 찾을 수 없습니다.',
|
||||
currentPage: '404'
|
||||
});
|
||||
});
|
||||
|
||||
app.use((err, req, res, next) => {
|
||||
console.error(err.stack);
|
||||
res.status(500).render('error', {
|
||||
title: '500 - 서버 오류',
|
||||
message: '서버에서 오류가 발생했습니다.',
|
||||
currentPage: 'error'
|
||||
});
|
||||
});
|
||||
|
||||
// Start server
|
||||
app.listen(PORT, () => {
|
||||
console.log(`🚀 SmartSolTech Demo Server running on http://localhost:${PORT}`);
|
||||
console.log('📋 Available pages:');
|
||||
console.log(' • Home: http://localhost:3000/');
|
||||
console.log(' • Portfolio: http://localhost:3000/portfolio');
|
||||
console.log(' • Services: http://localhost:3000/services');
|
||||
console.log(' • Contact: http://localhost:3000/contact');
|
||||
console.log(' • Calculator: http://localhost:3000/calculator');
|
||||
console.log('');
|
||||
console.log('💡 This is a demo version using mock data');
|
||||
console.log('💾 To use with MongoDB, run: npm start');
|
||||
});
|
||||
|
||||
module.exports = app;
|
||||
419
.history/server-demo_20251019165723.js
Normal file
419
.history/server-demo_20251019165723.js
Normal file
@@ -0,0 +1,419 @@
|
||||
/**
|
||||
* Demo server for SmartSolTech website
|
||||
* Uses mock data instead of MongoDB for demonstration
|
||||
*/
|
||||
|
||||
const express = require('express');
|
||||
const path = require('path');
|
||||
const cookieParser = require('cookie-parser');
|
||||
const session = require('express-session');
|
||||
const flash = require('connect-flash');
|
||||
const helmet = require('helmet');
|
||||
const compression = require('compression');
|
||||
const rateLimit = require('express-rate-limit');
|
||||
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 3000;
|
||||
|
||||
// Security middleware
|
||||
app.use(helmet({
|
||||
contentSecurityPolicy: {
|
||||
directives: {
|
||||
defaultSrc: ["'self'"],
|
||||
styleSrc: ["'self'", "'unsafe-inline'", "https://cdn.jsdelivr.net", "https://cdnjs.cloudflare.com", "https://unpkg.com"],
|
||||
scriptSrc: ["'self'", "'unsafe-inline'", "https://cdn.jsdelivr.net", "https://cdnjs.cloudflare.com", "https://unpkg.com"],
|
||||
imgSrc: ["'self'", "data:", "https:", "blob:"],
|
||||
fontSrc: ["'self'", "https://cdnjs.cloudflare.com"],
|
||||
connectSrc: ["'self'", "https:"],
|
||||
mediaSrc: ["'self'"],
|
||||
frameSrc: ["'none'"]
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
// Rate limiting
|
||||
const limiter = rateLimit({
|
||||
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||
max: 100, // limit each IP to 100 requests per windowMs
|
||||
message: 'Too many requests from this IP, please try again later.'
|
||||
});
|
||||
app.use(limiter);
|
||||
|
||||
// Compression
|
||||
app.use(compression());
|
||||
|
||||
// View engine setup
|
||||
app.set('views', path.join(__dirname, 'views'));
|
||||
app.set('view engine', 'ejs');
|
||||
|
||||
// Middleware
|
||||
app.use(express.json({ limit: '10mb' }));
|
||||
app.use(express.urlencoded({ extended: true, limit: '10mb' }));
|
||||
app.use(cookieParser());
|
||||
app.use(express.static(path.join(__dirname, 'public')));
|
||||
|
||||
// Session configuration
|
||||
app.use(session({
|
||||
secret: 'demo-secret-key',
|
||||
resave: false,
|
||||
saveUninitialized: false,
|
||||
cookie: {
|
||||
secure: false, // Set to true in production with HTTPS
|
||||
maxAge: 24 * 60 * 60 * 1000 // 24 hours
|
||||
}
|
||||
}));
|
||||
|
||||
// Flash messages
|
||||
app.use(flash());
|
||||
|
||||
// Global variables for templates
|
||||
app.use((req, res, next) => {
|
||||
res.locals.success_msg = req.flash('success');
|
||||
res.locals.error_msg = req.flash('error');
|
||||
res.locals.error = req.flash('error');
|
||||
res.locals.currentYear = new Date().getFullYear();
|
||||
next();
|
||||
});
|
||||
|
||||
// Mock data
|
||||
const mockPortfolio = [
|
||||
{
|
||||
_id: '1',
|
||||
title: 'E-commerce 플랫폼',
|
||||
description: '현대적인 온라인 쇼핑몰 플랫폼을 개발했습니다. React와 Node.js를 활용하여 높은 성능과 사용자 경험을 제공하며, 결제 시스템과 재고 관리 기능을 포함합니다.',
|
||||
shortDescription: '반응형 온라인 쇼핑몰 플랫폼 개발',
|
||||
category: 'web-development',
|
||||
technologies: ['React', 'Node.js', 'MongoDB', 'Express', 'Stripe', 'AWS'],
|
||||
images: [
|
||||
{ url: '/images/portfolio/ecommerce-1.jpg', alt: 'E-commerce 메인페이지', isPrimary: true }
|
||||
],
|
||||
clientName: '패션 브랜드 ABC',
|
||||
projectUrl: 'https://example-ecommerce.demo',
|
||||
status: 'completed',
|
||||
featured: true,
|
||||
publishedAt: new Date('2024-01-15'),
|
||||
completedAt: new Date('2024-01-10'),
|
||||
isPublished: true,
|
||||
viewCount: 150,
|
||||
likes: 25
|
||||
},
|
||||
{
|
||||
_id: '2',
|
||||
title: '모바일 피트니스 앱',
|
||||
description: 'React Native를 사용하여 크로스플랫폼 피트니스 애플리케이션을 개발했습니다. 운동 계획, 칼로리 추적, 소셜 기능을 포함하여 사용자들의 건강한 라이프스타일을 지원합니다.',
|
||||
shortDescription: '건강 관리를 위한 크로스플랫폼 모바일 앱',
|
||||
category: 'mobile-app',
|
||||
technologies: ['React Native', 'Redux', 'Firebase', 'Node.js', 'PostgreSQL'],
|
||||
images: [
|
||||
{ url: '/images/portfolio/fitness-1.jpg', alt: '피트니스 앱 메인화면', isPrimary: true }
|
||||
],
|
||||
clientName: '헬스케어 스타트업 FIT',
|
||||
status: 'completed',
|
||||
featured: true,
|
||||
publishedAt: new Date('2024-02-20'),
|
||||
completedAt: new Date('2024-02-15'),
|
||||
isPublished: true,
|
||||
viewCount: 200,
|
||||
likes: 35
|
||||
},
|
||||
{
|
||||
_id: '3',
|
||||
title: '기업 웹사이트 리뉴얼',
|
||||
description: '기업의 브랜드 아이덴티티를 반영한 웹사이트 리뉴얼 프로젝트입니다. 사용자 경험을 개선하고 모던한 디자인을 적용하여 브랜드 가치를 높였습니다.',
|
||||
shortDescription: '브랜드 아이덴티티를 반영한 기업 웹사이트 리뉴얼',
|
||||
category: 'ui-ux-design',
|
||||
technologies: ['Figma', 'React', 'Sass', 'Framer Motion', 'Contentful'],
|
||||
images: [
|
||||
{ url: '/images/portfolio/corporate-1.jpg', alt: '기업 웹사이트 메인페이지', isPrimary: true }
|
||||
],
|
||||
clientName: '기술 기업 TechCorp',
|
||||
projectUrl: 'https://example-corp.demo',
|
||||
status: 'completed',
|
||||
featured: true,
|
||||
publishedAt: new Date('2024-03-10'),
|
||||
completedAt: new Date('2024-03-05'),
|
||||
isPublished: true,
|
||||
viewCount: 120,
|
||||
likes: 18
|
||||
}
|
||||
];
|
||||
|
||||
const mockServices = [
|
||||
{
|
||||
_id: '1',
|
||||
name: '웹 개발',
|
||||
description: '현대적이고 반응형인 웹사이트와 웹 애플리케이션을 개발합니다. React, Node.js, MongoDB 등 최신 기술 스택을 활용하여 성능과 사용자 경험을 최적화합니다.',
|
||||
shortDescription: '현대적이고 반응형 웹사이트 및 웹 애플리케이션 개발',
|
||||
icon: 'fas fa-code',
|
||||
category: 'development',
|
||||
pricing: {
|
||||
basePrice: 500000,
|
||||
currency: 'KRW',
|
||||
priceType: 'project',
|
||||
priceRange: { min: 500000, max: 5000000 }
|
||||
},
|
||||
featured: true,
|
||||
isActive: true
|
||||
},
|
||||
{
|
||||
_id: '2',
|
||||
name: '모바일 앱 개발',
|
||||
description: 'iOS와 Android 플랫폼을 위한 네이티브 및 크로스플랫폼 모바일 애플리케이션을 개발합니다. React Native, Flutter 등을 활용하여 효율적인 개발을 진행합니다.',
|
||||
shortDescription: 'iOS/Android 네이티브 및 크로스플랫폼 앱 개발',
|
||||
icon: 'fas fa-mobile-alt',
|
||||
category: 'development',
|
||||
pricing: {
|
||||
basePrice: 800000,
|
||||
currency: 'KRW',
|
||||
priceType: 'project',
|
||||
priceRange: { min: 800000, max: 8000000 }
|
||||
},
|
||||
featured: true,
|
||||
isActive: true
|
||||
},
|
||||
{
|
||||
_id: '3',
|
||||
name: 'UI/UX 디자인',
|
||||
description: '사용자 중심의 직관적이고 아름다운 인터페이스를 디자인합니다. 사용자 경험 연구와 프로토타이핑을 통해 최적의 디자인 솔루션을 제공합니다.',
|
||||
shortDescription: '사용자 중심의 직관적이고 아름다운 인터페이스 디자인',
|
||||
icon: 'fas fa-palette',
|
||||
category: 'design',
|
||||
pricing: {
|
||||
basePrice: 300000,
|
||||
currency: 'KRW',
|
||||
priceType: 'project',
|
||||
priceRange: { min: 300000, max: 2000000 }
|
||||
},
|
||||
featured: true,
|
||||
isActive: true
|
||||
},
|
||||
{
|
||||
_id: '4',
|
||||
name: '디지털 마케팅',
|
||||
description: 'SEO, 소셜미디어 마케팅, 온라인 광고를 통해 디지털 마케팅 전략을 수립하고 실행합니다. 데이터 분석을 통한 지속적인 최적화를 제공합니다.',
|
||||
shortDescription: 'SEO, 소셜미디어, 온라인 광고를 통한 디지털 마케팅',
|
||||
icon: 'fas fa-chart-line',
|
||||
category: 'marketing',
|
||||
pricing: {
|
||||
basePrice: 200000,
|
||||
currency: 'KRW',
|
||||
priceType: 'project',
|
||||
priceRange: { min: 200000, max: 1500000 }
|
||||
},
|
||||
featured: true,
|
||||
isActive: true
|
||||
}
|
||||
];
|
||||
|
||||
const mockSettings = {
|
||||
siteName: 'SmartSolTech',
|
||||
siteDescription: '혁신적인 기술 솔루션으로 비즈니스의 성장을 지원합니다',
|
||||
contact: {
|
||||
email: 'info@smartsoltech.kr',
|
||||
phone: '+82-10-1234-5678',
|
||||
address: 'Seoul, South Korea'
|
||||
},
|
||||
social: {
|
||||
facebook: 'https://facebook.com/smartsoltech',
|
||||
twitter: 'https://twitter.com/smartsoltech',
|
||||
linkedin: 'https://linkedin.com/company/smartsoltech',
|
||||
instagram: 'https://instagram.com/smartsoltech'
|
||||
}
|
||||
};
|
||||
|
||||
// Helper function for category names
|
||||
function getCategoryName(category) {
|
||||
const categoryNames = {
|
||||
'web-development': '웹 개발',
|
||||
'mobile-app': '모바일 앱',
|
||||
'ui-ux-design': 'UI/UX 디자인',
|
||||
'branding': '브랜딩',
|
||||
'marketing': '디지털 마케팅'
|
||||
};
|
||||
return categoryNames[category] || category;
|
||||
}
|
||||
|
||||
// Routes
|
||||
app.get('/', (req, res) => {
|
||||
res.render('index', {
|
||||
title: 'SmartSolTech - Innovative Technology Solutions',
|
||||
settings: mockSettings,
|
||||
featuredPortfolio: mockPortfolio.filter(p => p.featured),
|
||||
featuredServices: mockServices.filter(s => s.featured),
|
||||
currentPage: 'home'
|
||||
});
|
||||
});
|
||||
|
||||
app.get('/portfolio', (req, res) => {
|
||||
const category = req.query.category;
|
||||
let filteredPortfolio = mockPortfolio;
|
||||
|
||||
if (category && category !== 'all') {
|
||||
filteredPortfolio = mockPortfolio.filter(p => p.category === category);
|
||||
}
|
||||
|
||||
res.render('portfolio', {
|
||||
title: '포트폴리오 - SmartSolTech',
|
||||
portfolioItems: filteredPortfolio,
|
||||
currentPage: 'portfolio'
|
||||
});
|
||||
});
|
||||
|
||||
app.get('/portfolio/:id', (req, res) => {
|
||||
const portfolio = mockPortfolio.find(p => p._id === req.params.id);
|
||||
|
||||
if (!portfolio) {
|
||||
return res.status(404).render('error', {
|
||||
title: '404 - 페이지를 찾을 수 없습니다',
|
||||
message: '요청하신 포트폴리오 항목을 찾을 수 없습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
const relatedProjects = mockPortfolio.filter(p =>
|
||||
p._id !== portfolio._id && p.category === portfolio.category
|
||||
).slice(0, 3);
|
||||
|
||||
res.render('portfolio-detail', {
|
||||
title: `${portfolio.title} - 포트폴리오`,
|
||||
portfolio,
|
||||
relatedProjects,
|
||||
currentPage: 'portfolio'
|
||||
});
|
||||
});
|
||||
|
||||
app.get('/services', (req, res) => {
|
||||
res.render('services', {
|
||||
title: '서비스 - SmartSolTech',
|
||||
services: mockServices,
|
||||
currentPage: 'services'
|
||||
});
|
||||
});
|
||||
|
||||
app.get('/contact', (req, res) => {
|
||||
res.render('contact', {
|
||||
title: '연락처 - SmartSolTech',
|
||||
settings: mockSettings,
|
||||
currentPage: 'contact'
|
||||
});
|
||||
});
|
||||
|
||||
app.post('/contact', (req, res) => {
|
||||
// Simulate contact form processing
|
||||
console.log('Contact form submission:', req.body);
|
||||
|
||||
req.flash('success', '문의가 성공적으로 접수되었습니다. 빠른 시일 내에 답변드리겠습니다.');
|
||||
res.redirect('/contact');
|
||||
});
|
||||
|
||||
app.get('/calculator', (req, res) => {
|
||||
res.render('calculator', {
|
||||
title: '비용 계산기 - SmartSolTech',
|
||||
services: mockServices,
|
||||
currentPage: 'calculator'
|
||||
});
|
||||
});
|
||||
|
||||
// API Routes for calculator
|
||||
app.post('/api/calculator/estimate', (req, res) => {
|
||||
const { service, projectType, timeline, features } = req.body;
|
||||
|
||||
// Simple calculation logic
|
||||
let basePrice = 500000;
|
||||
const selectedService = mockServices.find(s => s._id === service);
|
||||
|
||||
if (selectedService) {
|
||||
basePrice = selectedService.pricing.basePrice;
|
||||
}
|
||||
|
||||
// Apply multipliers based on project complexity
|
||||
const typeMultipliers = {
|
||||
'simple': 1,
|
||||
'medium': 1.5,
|
||||
'complex': 2.5,
|
||||
'enterprise': 4
|
||||
};
|
||||
|
||||
const timelineMultipliers = {
|
||||
'urgent': 1.5,
|
||||
'normal': 1,
|
||||
'flexible': 0.9
|
||||
};
|
||||
|
||||
const typeMultiplier = typeMultipliers[projectType] || 1;
|
||||
const timelineMultiplier = timelineMultipliers[timeline] || 1;
|
||||
const featuresMultiplier = 1 + (features?.length || 0) * 0.2;
|
||||
|
||||
const estimatedPrice = Math.round(basePrice * typeMultiplier * timelineMultiplier * featuresMultiplier);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
estimate: {
|
||||
basePrice,
|
||||
estimatedPrice,
|
||||
breakdown: {
|
||||
basePrice,
|
||||
projectType: projectType,
|
||||
typeMultiplier,
|
||||
timeline,
|
||||
timelineMultiplier,
|
||||
features: features || [],
|
||||
featuresMultiplier
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// API Routes for portfolio interactions
|
||||
app.post('/api/portfolio/:id/like', (req, res) => {
|
||||
const portfolio = mockPortfolio.find(p => p._id === req.params.id);
|
||||
if (portfolio) {
|
||||
portfolio.likes = (portfolio.likes || 0) + 1;
|
||||
res.json({ success: true, likes: portfolio.likes });
|
||||
} else {
|
||||
res.status(404).json({ success: false, message: 'Portfolio not found' });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/portfolio/:id/view', (req, res) => {
|
||||
const portfolio = mockPortfolio.find(p => p._id === req.params.id);
|
||||
if (portfolio) {
|
||||
portfolio.viewCount = (portfolio.viewCount || 0) + 1;
|
||||
res.json({ success: true, viewCount: portfolio.viewCount });
|
||||
} else {
|
||||
res.status(404).json({ success: false, message: 'Portfolio not found' });
|
||||
}
|
||||
});
|
||||
|
||||
// Error handling
|
||||
app.use((req, res) => {
|
||||
res.status(404).render('error', {
|
||||
title: '404 - 페이지를 찾을 수 없습니다',
|
||||
message: '요청하신 페이지를 찾을 수 없습니다.',
|
||||
currentPage: '404'
|
||||
});
|
||||
});
|
||||
|
||||
app.use((err, req, res, next) => {
|
||||
console.error(err.stack);
|
||||
res.status(500).render('error', {
|
||||
title: '500 - 서버 오류',
|
||||
message: '서버에서 오류가 발생했습니다.',
|
||||
currentPage: 'error'
|
||||
});
|
||||
});
|
||||
|
||||
// Start server
|
||||
app.listen(PORT, () => {
|
||||
console.log(`🚀 SmartSolTech Demo Server running on http://localhost:${PORT}`);
|
||||
console.log('📋 Available pages:');
|
||||
console.log(' • Home: http://localhost:3000/');
|
||||
console.log(' • Portfolio: http://localhost:3000/portfolio');
|
||||
console.log(' • Services: http://localhost:3000/services');
|
||||
console.log(' • Contact: http://localhost:3000/contact');
|
||||
console.log(' • Calculator: http://localhost:3000/calculator');
|
||||
console.log('');
|
||||
console.log('💡 This is a demo version using mock data');
|
||||
console.log('💾 To use with MongoDB, run: npm start');
|
||||
});
|
||||
|
||||
module.exports = app;
|
||||
419
.history/server-demo_20251019170013.js
Normal file
419
.history/server-demo_20251019170013.js
Normal file
@@ -0,0 +1,419 @@
|
||||
/**
|
||||
* Demo server for SmartSolTech website
|
||||
* Uses mock data instead of MongoDB for demonstration
|
||||
*/
|
||||
|
||||
const express = require('express');
|
||||
const path = require('path');
|
||||
const cookieParser = require('cookie-parser');
|
||||
const session = require('express-session');
|
||||
const flash = require('connect-flash');
|
||||
const helmet = require('helmet');
|
||||
const compression = require('compression');
|
||||
const rateLimit = require('express-rate-limit');
|
||||
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 3000;
|
||||
|
||||
// Security middleware
|
||||
app.use(helmet({
|
||||
contentSecurityPolicy: {
|
||||
directives: {
|
||||
defaultSrc: ["'self'"],
|
||||
styleSrc: ["'self'", "'unsafe-inline'", "https://cdn.jsdelivr.net", "https://cdnjs.cloudflare.com", "https://unpkg.com"],
|
||||
scriptSrc: ["'self'", "'unsafe-inline'", "https://cdn.jsdelivr.net", "https://cdnjs.cloudflare.com", "https://unpkg.com"],
|
||||
imgSrc: ["'self'", "data:", "https:", "blob:"],
|
||||
fontSrc: ["'self'", "https://cdnjs.cloudflare.com"],
|
||||
connectSrc: ["'self'", "https:"],
|
||||
mediaSrc: ["'self'"],
|
||||
frameSrc: ["'none'"]
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
// Rate limiting
|
||||
const limiter = rateLimit({
|
||||
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||
max: 100, // limit each IP to 100 requests per windowMs
|
||||
message: 'Too many requests from this IP, please try again later.'
|
||||
});
|
||||
app.use(limiter);
|
||||
|
||||
// Compression
|
||||
app.use(compression());
|
||||
|
||||
// View engine setup
|
||||
app.set('views', path.join(__dirname, 'views'));
|
||||
app.set('view engine', 'ejs');
|
||||
|
||||
// Middleware
|
||||
app.use(express.json({ limit: '10mb' }));
|
||||
app.use(express.urlencoded({ extended: true, limit: '10mb' }));
|
||||
app.use(cookieParser());
|
||||
app.use(express.static(path.join(__dirname, 'public')));
|
||||
|
||||
// Session configuration
|
||||
app.use(session({
|
||||
secret: 'demo-secret-key',
|
||||
resave: false,
|
||||
saveUninitialized: false,
|
||||
cookie: {
|
||||
secure: false, // Set to true in production with HTTPS
|
||||
maxAge: 24 * 60 * 60 * 1000 // 24 hours
|
||||
}
|
||||
}));
|
||||
|
||||
// Flash messages
|
||||
app.use(flash());
|
||||
|
||||
// Global variables for templates
|
||||
app.use((req, res, next) => {
|
||||
res.locals.success_msg = req.flash('success');
|
||||
res.locals.error_msg = req.flash('error');
|
||||
res.locals.error = req.flash('error');
|
||||
res.locals.currentYear = new Date().getFullYear();
|
||||
next();
|
||||
});
|
||||
|
||||
// Mock data
|
||||
const mockPortfolio = [
|
||||
{
|
||||
_id: '1',
|
||||
title: 'E-commerce 플랫폼',
|
||||
description: '현대적인 온라인 쇼핑몰 플랫폼을 개발했습니다. React와 Node.js를 활용하여 높은 성능과 사용자 경험을 제공하며, 결제 시스템과 재고 관리 기능을 포함합니다.',
|
||||
shortDescription: '반응형 온라인 쇼핑몰 플랫폼 개발',
|
||||
category: 'web-development',
|
||||
technologies: ['React', 'Node.js', 'MongoDB', 'Express', 'Stripe', 'AWS'],
|
||||
images: [
|
||||
{ url: '/images/portfolio/ecommerce-1.jpg', alt: 'E-commerce 메인페이지', isPrimary: true }
|
||||
],
|
||||
clientName: '패션 브랜드 ABC',
|
||||
projectUrl: 'https://example-ecommerce.demo',
|
||||
status: 'completed',
|
||||
featured: true,
|
||||
publishedAt: new Date('2024-01-15'),
|
||||
completedAt: new Date('2024-01-10'),
|
||||
isPublished: true,
|
||||
viewCount: 150,
|
||||
likes: 25
|
||||
},
|
||||
{
|
||||
_id: '2',
|
||||
title: '모바일 피트니스 앱',
|
||||
description: 'React Native를 사용하여 크로스플랫폼 피트니스 애플리케이션을 개발했습니다. 운동 계획, 칼로리 추적, 소셜 기능을 포함하여 사용자들의 건강한 라이프스타일을 지원합니다.',
|
||||
shortDescription: '건강 관리를 위한 크로스플랫폼 모바일 앱',
|
||||
category: 'mobile-app',
|
||||
technologies: ['React Native', 'Redux', 'Firebase', 'Node.js', 'PostgreSQL'],
|
||||
images: [
|
||||
{ url: '/images/portfolio/fitness-1.jpg', alt: '피트니스 앱 메인화면', isPrimary: true }
|
||||
],
|
||||
clientName: '헬스케어 스타트업 FIT',
|
||||
status: 'completed',
|
||||
featured: true,
|
||||
publishedAt: new Date('2024-02-20'),
|
||||
completedAt: new Date('2024-02-15'),
|
||||
isPublished: true,
|
||||
viewCount: 200,
|
||||
likes: 35
|
||||
},
|
||||
{
|
||||
_id: '3',
|
||||
title: '기업 웹사이트 리뉴얼',
|
||||
description: '기업의 브랜드 아이덴티티를 반영한 웹사이트 리뉴얼 프로젝트입니다. 사용자 경험을 개선하고 모던한 디자인을 적용하여 브랜드 가치를 높였습니다.',
|
||||
shortDescription: '브랜드 아이덴티티를 반영한 기업 웹사이트 리뉴얼',
|
||||
category: 'ui-ux-design',
|
||||
technologies: ['Figma', 'React', 'Sass', 'Framer Motion', 'Contentful'],
|
||||
images: [
|
||||
{ url: '/images/portfolio/corporate-1.jpg', alt: '기업 웹사이트 메인페이지', isPrimary: true }
|
||||
],
|
||||
clientName: '기술 기업 TechCorp',
|
||||
projectUrl: 'https://example-corp.demo',
|
||||
status: 'completed',
|
||||
featured: true,
|
||||
publishedAt: new Date('2024-03-10'),
|
||||
completedAt: new Date('2024-03-05'),
|
||||
isPublished: true,
|
||||
viewCount: 120,
|
||||
likes: 18
|
||||
}
|
||||
];
|
||||
|
||||
const mockServices = [
|
||||
{
|
||||
_id: '1',
|
||||
name: '웹 개발',
|
||||
description: '현대적이고 반응형인 웹사이트와 웹 애플리케이션을 개발합니다. React, Node.js, MongoDB 등 최신 기술 스택을 활용하여 성능과 사용자 경험을 최적화합니다.',
|
||||
shortDescription: '현대적이고 반응형 웹사이트 및 웹 애플리케이션 개발',
|
||||
icon: 'fas fa-code',
|
||||
category: 'development',
|
||||
pricing: {
|
||||
basePrice: 500000,
|
||||
currency: 'KRW',
|
||||
priceType: 'project',
|
||||
priceRange: { min: 500000, max: 5000000 }
|
||||
},
|
||||
featured: true,
|
||||
isActive: true
|
||||
},
|
||||
{
|
||||
_id: '2',
|
||||
name: '모바일 앱 개발',
|
||||
description: 'iOS와 Android 플랫폼을 위한 네이티브 및 크로스플랫폼 모바일 애플리케이션을 개발합니다. React Native, Flutter 등을 활용하여 효율적인 개발을 진행합니다.',
|
||||
shortDescription: 'iOS/Android 네이티브 및 크로스플랫폼 앱 개발',
|
||||
icon: 'fas fa-mobile-alt',
|
||||
category: 'development',
|
||||
pricing: {
|
||||
basePrice: 800000,
|
||||
currency: 'KRW',
|
||||
priceType: 'project',
|
||||
priceRange: { min: 800000, max: 8000000 }
|
||||
},
|
||||
featured: true,
|
||||
isActive: true
|
||||
},
|
||||
{
|
||||
_id: '3',
|
||||
name: 'UI/UX 디자인',
|
||||
description: '사용자 중심의 직관적이고 아름다운 인터페이스를 디자인합니다. 사용자 경험 연구와 프로토타이핑을 통해 최적의 디자인 솔루션을 제공합니다.',
|
||||
shortDescription: '사용자 중심의 직관적이고 아름다운 인터페이스 디자인',
|
||||
icon: 'fas fa-palette',
|
||||
category: 'design',
|
||||
pricing: {
|
||||
basePrice: 300000,
|
||||
currency: 'KRW',
|
||||
priceType: 'project',
|
||||
priceRange: { min: 300000, max: 2000000 }
|
||||
},
|
||||
featured: true,
|
||||
isActive: true
|
||||
},
|
||||
{
|
||||
_id: '4',
|
||||
name: '디지털 마케팅',
|
||||
description: 'SEO, 소셜미디어 마케팅, 온라인 광고를 통해 디지털 마케팅 전략을 수립하고 실행합니다. 데이터 분석을 통한 지속적인 최적화를 제공합니다.',
|
||||
shortDescription: 'SEO, 소셜미디어, 온라인 광고를 통한 디지털 마케팅',
|
||||
icon: 'fas fa-chart-line',
|
||||
category: 'marketing',
|
||||
pricing: {
|
||||
basePrice: 200000,
|
||||
currency: 'KRW',
|
||||
priceType: 'project',
|
||||
priceRange: { min: 200000, max: 1500000 }
|
||||
},
|
||||
featured: true,
|
||||
isActive: true
|
||||
}
|
||||
];
|
||||
|
||||
const mockSettings = {
|
||||
siteName: 'SmartSolTech',
|
||||
siteDescription: '혁신적인 기술 솔루션으로 비즈니스의 성장을 지원합니다',
|
||||
contact: {
|
||||
email: 'info@smartsoltech.kr',
|
||||
phone: '+82-10-1234-5678',
|
||||
address: 'Seoul, South Korea'
|
||||
},
|
||||
social: {
|
||||
facebook: 'https://facebook.com/smartsoltech',
|
||||
twitter: 'https://twitter.com/smartsoltech',
|
||||
linkedin: 'https://linkedin.com/company/smartsoltech',
|
||||
instagram: 'https://instagram.com/smartsoltech'
|
||||
}
|
||||
};
|
||||
|
||||
// Helper function for category names
|
||||
function getCategoryName(category) {
|
||||
const categoryNames = {
|
||||
'web-development': '웹 개발',
|
||||
'mobile-app': '모바일 앱',
|
||||
'ui-ux-design': 'UI/UX 디자인',
|
||||
'branding': '브랜딩',
|
||||
'marketing': '디지털 마케팅'
|
||||
};
|
||||
return categoryNames[category] || category;
|
||||
}
|
||||
|
||||
// Routes
|
||||
app.get('/', (req, res) => {
|
||||
res.render('index', {
|
||||
title: 'SmartSolTech - Innovative Technology Solutions',
|
||||
settings: mockSettings,
|
||||
featuredPortfolio: mockPortfolio.filter(p => p.featured),
|
||||
featuredServices: mockServices.filter(s => s.featured),
|
||||
currentPage: 'home'
|
||||
});
|
||||
});
|
||||
|
||||
app.get('/portfolio', (req, res) => {
|
||||
const category = req.query.category;
|
||||
let filteredPortfolio = mockPortfolio;
|
||||
|
||||
if (category && category !== 'all') {
|
||||
filteredPortfolio = mockPortfolio.filter(p => p.category === category);
|
||||
}
|
||||
|
||||
res.render('portfolio', {
|
||||
title: '포트폴리오 - SmartSolTech',
|
||||
portfolioItems: filteredPortfolio,
|
||||
currentPage: 'portfolio'
|
||||
});
|
||||
});
|
||||
|
||||
app.get('/portfolio/:id', (req, res) => {
|
||||
const portfolio = mockPortfolio.find(p => p._id === req.params.id);
|
||||
|
||||
if (!portfolio) {
|
||||
return res.status(404).render('error', {
|
||||
title: '404 - 페이지를 찾을 수 없습니다',
|
||||
message: '요청하신 포트폴리오 항목을 찾을 수 없습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
const relatedProjects = mockPortfolio.filter(p =>
|
||||
p._id !== portfolio._id && p.category === portfolio.category
|
||||
).slice(0, 3);
|
||||
|
||||
res.render('portfolio-detail', {
|
||||
title: `${portfolio.title} - 포트폴리오`,
|
||||
portfolio,
|
||||
relatedProjects,
|
||||
currentPage: 'portfolio'
|
||||
});
|
||||
});
|
||||
|
||||
app.get('/services', (req, res) => {
|
||||
res.render('services', {
|
||||
title: '서비스 - SmartSolTech',
|
||||
services: mockServices,
|
||||
currentPage: 'services'
|
||||
});
|
||||
});
|
||||
|
||||
app.get('/contact', (req, res) => {
|
||||
res.render('contact', {
|
||||
title: '연락처 - SmartSolTech',
|
||||
settings: mockSettings,
|
||||
currentPage: 'contact'
|
||||
});
|
||||
});
|
||||
|
||||
app.post('/contact', (req, res) => {
|
||||
// Simulate contact form processing
|
||||
console.log('Contact form submission:', req.body);
|
||||
|
||||
req.flash('success', '문의가 성공적으로 접수되었습니다. 빠른 시일 내에 답변드리겠습니다.');
|
||||
res.redirect('/contact');
|
||||
});
|
||||
|
||||
app.get('/calculator', (req, res) => {
|
||||
res.render('calculator', {
|
||||
title: '비용 계산기 - SmartSolTech',
|
||||
services: mockServices,
|
||||
currentPage: 'calculator'
|
||||
});
|
||||
});
|
||||
|
||||
// API Routes for calculator
|
||||
app.post('/api/calculator/estimate', (req, res) => {
|
||||
const { service, projectType, timeline, features } = req.body;
|
||||
|
||||
// Simple calculation logic
|
||||
let basePrice = 500000;
|
||||
const selectedService = mockServices.find(s => s._id === service);
|
||||
|
||||
if (selectedService) {
|
||||
basePrice = selectedService.pricing.basePrice;
|
||||
}
|
||||
|
||||
// Apply multipliers based on project complexity
|
||||
const typeMultipliers = {
|
||||
'simple': 1,
|
||||
'medium': 1.5,
|
||||
'complex': 2.5,
|
||||
'enterprise': 4
|
||||
};
|
||||
|
||||
const timelineMultipliers = {
|
||||
'urgent': 1.5,
|
||||
'normal': 1,
|
||||
'flexible': 0.9
|
||||
};
|
||||
|
||||
const typeMultiplier = typeMultipliers[projectType] || 1;
|
||||
const timelineMultiplier = timelineMultipliers[timeline] || 1;
|
||||
const featuresMultiplier = 1 + (features?.length || 0) * 0.2;
|
||||
|
||||
const estimatedPrice = Math.round(basePrice * typeMultiplier * timelineMultiplier * featuresMultiplier);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
estimate: {
|
||||
basePrice,
|
||||
estimatedPrice,
|
||||
breakdown: {
|
||||
basePrice,
|
||||
projectType: projectType,
|
||||
typeMultiplier,
|
||||
timeline,
|
||||
timelineMultiplier,
|
||||
features: features || [],
|
||||
featuresMultiplier
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// API Routes for portfolio interactions
|
||||
app.post('/api/portfolio/:id/like', (req, res) => {
|
||||
const portfolio = mockPortfolio.find(p => p._id === req.params.id);
|
||||
if (portfolio) {
|
||||
portfolio.likes = (portfolio.likes || 0) + 1;
|
||||
res.json({ success: true, likes: portfolio.likes });
|
||||
} else {
|
||||
res.status(404).json({ success: false, message: 'Portfolio not found' });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/portfolio/:id/view', (req, res) => {
|
||||
const portfolio = mockPortfolio.find(p => p._id === req.params.id);
|
||||
if (portfolio) {
|
||||
portfolio.viewCount = (portfolio.viewCount || 0) + 1;
|
||||
res.json({ success: true, viewCount: portfolio.viewCount });
|
||||
} else {
|
||||
res.status(404).json({ success: false, message: 'Portfolio not found' });
|
||||
}
|
||||
});
|
||||
|
||||
// Error handling
|
||||
app.use((req, res) => {
|
||||
res.status(404).render('error', {
|
||||
title: '404 - 페이지를 찾을 수 없습니다',
|
||||
message: '요청하신 페이지를 찾을 수 없습니다.',
|
||||
currentPage: '404'
|
||||
});
|
||||
});
|
||||
|
||||
app.use((err, req, res, next) => {
|
||||
console.error(err.stack);
|
||||
res.status(500).render('error', {
|
||||
title: '500 - 서버 오류',
|
||||
message: '서버에서 오류가 발생했습니다.',
|
||||
currentPage: 'error'
|
||||
});
|
||||
});
|
||||
|
||||
// Start server
|
||||
app.listen(PORT, () => {
|
||||
console.log(`🚀 SmartSolTech Demo Server running on http://localhost:${PORT}`);
|
||||
console.log('📋 Available pages:');
|
||||
console.log(' • Home: http://localhost:3000/');
|
||||
console.log(' • Portfolio: http://localhost:3000/portfolio');
|
||||
console.log(' • Services: http://localhost:3000/services');
|
||||
console.log(' • Contact: http://localhost:3000/contact');
|
||||
console.log(' • Calculator: http://localhost:3000/calculator');
|
||||
console.log('');
|
||||
console.log('💡 This is a demo version using mock data');
|
||||
console.log('💾 To use with MongoDB, run: npm start');
|
||||
});
|
||||
|
||||
module.exports = app;
|
||||
426
.history/server-demo_20251019170431.js
Normal file
426
.history/server-demo_20251019170431.js
Normal file
@@ -0,0 +1,426 @@
|
||||
/**
|
||||
* Demo server for SmartSolTech website
|
||||
* Uses mock data instead of MongoDB for demonstration
|
||||
*/
|
||||
|
||||
const express = require('express');
|
||||
const path = require('path');
|
||||
const cookieParser = require('cookie-parser');
|
||||
const session = require('express-session');
|
||||
const flash = require('connect-flash');
|
||||
const helmet = require('helmet');
|
||||
const compression = require('compression');
|
||||
const rateLimit = require('express-rate-limit');
|
||||
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 3000;
|
||||
|
||||
// Security middleware
|
||||
app.use(helmet({
|
||||
contentSecurityPolicy: {
|
||||
directives: {
|
||||
defaultSrc: ["'self'"],
|
||||
styleSrc: ["'self'", "'unsafe-inline'", "https://cdn.jsdelivr.net", "https://cdnjs.cloudflare.com", "https://unpkg.com"],
|
||||
scriptSrc: ["'self'", "'unsafe-inline'", "https://cdn.jsdelivr.net", "https://cdnjs.cloudflare.com", "https://unpkg.com"],
|
||||
imgSrc: ["'self'", "data:", "https:", "blob:"],
|
||||
fontSrc: ["'self'", "https://cdnjs.cloudflare.com"],
|
||||
connectSrc: ["'self'", "https:"],
|
||||
mediaSrc: ["'self'"],
|
||||
frameSrc: ["'none'"]
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
// Rate limiting
|
||||
const limiter = rateLimit({
|
||||
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||
max: 100, // limit each IP to 100 requests per windowMs
|
||||
message: 'Too many requests from this IP, please try again later.'
|
||||
});
|
||||
app.use(limiter);
|
||||
|
||||
// Compression
|
||||
app.use(compression());
|
||||
|
||||
// View engine setup
|
||||
app.set('views', path.join(__dirname, 'views'));
|
||||
app.set('view engine', 'ejs');
|
||||
|
||||
// Middleware
|
||||
app.use(express.json({ limit: '10mb' }));
|
||||
app.use(express.urlencoded({ extended: true, limit: '10mb' }));
|
||||
app.use(cookieParser());
|
||||
app.use(express.static(path.join(__dirname, 'public')));
|
||||
|
||||
// Session configuration
|
||||
app.use(session({
|
||||
secret: 'demo-secret-key',
|
||||
resave: false,
|
||||
saveUninitialized: false,
|
||||
cookie: {
|
||||
secure: false, // Set to true in production with HTTPS
|
||||
maxAge: 24 * 60 * 60 * 1000 // 24 hours
|
||||
}
|
||||
}));
|
||||
|
||||
// Flash messages
|
||||
app.use(flash());
|
||||
|
||||
// Global variables for templates
|
||||
app.use((req, res, next) => {
|
||||
res.locals.success_msg = req.flash('success');
|
||||
res.locals.error_msg = req.flash('error');
|
||||
res.locals.error = req.flash('error');
|
||||
res.locals.currentYear = new Date().getFullYear();
|
||||
next();
|
||||
});
|
||||
|
||||
// Mock data
|
||||
const mockPortfolio = [
|
||||
{
|
||||
_id: '1',
|
||||
title: 'E-commerce 플랫폼',
|
||||
description: '현대적인 온라인 쇼핑몰 플랫폼을 개발했습니다. React와 Node.js를 활용하여 높은 성능과 사용자 경험을 제공하며, 결제 시스템과 재고 관리 기능을 포함합니다.',
|
||||
shortDescription: '반응형 온라인 쇼핑몰 플랫폼 개발',
|
||||
category: 'web-development',
|
||||
technologies: ['React', 'Node.js', 'MongoDB', 'Express', 'Stripe', 'AWS'],
|
||||
images: [
|
||||
{ url: '/images/portfolio/ecommerce-1.jpg', alt: 'E-commerce 메인페이지', isPrimary: true }
|
||||
],
|
||||
clientName: '패션 브랜드 ABC',
|
||||
projectUrl: 'https://example-ecommerce.demo',
|
||||
status: 'completed',
|
||||
featured: true,
|
||||
publishedAt: new Date('2024-01-15'),
|
||||
completedAt: new Date('2024-01-10'),
|
||||
isPublished: true,
|
||||
viewCount: 150,
|
||||
likes: 25
|
||||
},
|
||||
{
|
||||
_id: '2',
|
||||
title: '모바일 피트니스 앱',
|
||||
description: 'React Native를 사용하여 크로스플랫폼 피트니스 애플리케이션을 개발했습니다. 운동 계획, 칼로리 추적, 소셜 기능을 포함하여 사용자들의 건강한 라이프스타일을 지원합니다.',
|
||||
shortDescription: '건강 관리를 위한 크로스플랫폼 모바일 앱',
|
||||
category: 'mobile-app',
|
||||
technologies: ['React Native', 'Redux', 'Firebase', 'Node.js', 'PostgreSQL'],
|
||||
images: [
|
||||
{ url: '/images/portfolio/fitness-1.jpg', alt: '피트니스 앱 메인화면', isPrimary: true }
|
||||
],
|
||||
clientName: '헬스케어 스타트업 FIT',
|
||||
status: 'completed',
|
||||
featured: true,
|
||||
publishedAt: new Date('2024-02-20'),
|
||||
completedAt: new Date('2024-02-15'),
|
||||
isPublished: true,
|
||||
viewCount: 200,
|
||||
likes: 35
|
||||
},
|
||||
{
|
||||
_id: '3',
|
||||
title: '기업 웹사이트 리뉴얼',
|
||||
description: '기업의 브랜드 아이덴티티를 반영한 웹사이트 리뉴얼 프로젝트입니다. 사용자 경험을 개선하고 모던한 디자인을 적용하여 브랜드 가치를 높였습니다.',
|
||||
shortDescription: '브랜드 아이덴티티를 반영한 기업 웹사이트 리뉴얼',
|
||||
category: 'ui-ux-design',
|
||||
technologies: ['Figma', 'React', 'Sass', 'Framer Motion', 'Contentful'],
|
||||
images: [
|
||||
{ url: '/images/portfolio/corporate-1.jpg', alt: '기업 웹사이트 메인페이지', isPrimary: true }
|
||||
],
|
||||
clientName: '기술 기업 TechCorp',
|
||||
projectUrl: 'https://example-corp.demo',
|
||||
status: 'completed',
|
||||
featured: true,
|
||||
publishedAt: new Date('2024-03-10'),
|
||||
completedAt: new Date('2024-03-05'),
|
||||
isPublished: true,
|
||||
viewCount: 120,
|
||||
likes: 18
|
||||
}
|
||||
];
|
||||
|
||||
const mockServices = [
|
||||
{
|
||||
_id: '1',
|
||||
name: '웹 개발',
|
||||
description: '현대적이고 반응형인 웹사이트와 웹 애플리케이션을 개발합니다. React, Node.js, MongoDB 등 최신 기술 스택을 활용하여 성능과 사용자 경험을 최적화합니다.',
|
||||
shortDescription: '현대적이고 반응형 웹사이트 및 웹 애플리케이션 개발',
|
||||
icon: 'fas fa-code',
|
||||
category: 'development',
|
||||
pricing: {
|
||||
basePrice: 500000,
|
||||
currency: 'KRW',
|
||||
priceType: 'project',
|
||||
priceRange: { min: 500000, max: 5000000 }
|
||||
},
|
||||
featured: true,
|
||||
isActive: true
|
||||
},
|
||||
{
|
||||
_id: '2',
|
||||
name: '모바일 앱 개발',
|
||||
description: 'iOS와 Android 플랫폼을 위한 네이티브 및 크로스플랫폼 모바일 애플리케이션을 개발합니다. React Native, Flutter 등을 활용하여 효율적인 개발을 진행합니다.',
|
||||
shortDescription: 'iOS/Android 네이티브 및 크로스플랫폼 앱 개발',
|
||||
icon: 'fas fa-mobile-alt',
|
||||
category: 'development',
|
||||
pricing: {
|
||||
basePrice: 800000,
|
||||
currency: 'KRW',
|
||||
priceType: 'project',
|
||||
priceRange: { min: 800000, max: 8000000 }
|
||||
},
|
||||
featured: true,
|
||||
isActive: true
|
||||
},
|
||||
{
|
||||
_id: '3',
|
||||
name: 'UI/UX 디자인',
|
||||
description: '사용자 중심의 직관적이고 아름다운 인터페이스를 디자인합니다. 사용자 경험 연구와 프로토타이핑을 통해 최적의 디자인 솔루션을 제공합니다.',
|
||||
shortDescription: '사용자 중심의 직관적이고 아름다운 인터페이스 디자인',
|
||||
icon: 'fas fa-palette',
|
||||
category: 'design',
|
||||
pricing: {
|
||||
basePrice: 300000,
|
||||
currency: 'KRW',
|
||||
priceType: 'project',
|
||||
priceRange: { min: 300000, max: 2000000 }
|
||||
},
|
||||
featured: true,
|
||||
isActive: true
|
||||
},
|
||||
{
|
||||
_id: '4',
|
||||
name: '디지털 마케팅',
|
||||
description: 'SEO, 소셜미디어 마케팅, 온라인 광고를 통해 디지털 마케팅 전략을 수립하고 실행합니다. 데이터 분석을 통한 지속적인 최적화를 제공합니다.',
|
||||
shortDescription: 'SEO, 소셜미디어, 온라인 광고를 통한 디지털 마케팅',
|
||||
icon: 'fas fa-chart-line',
|
||||
category: 'marketing',
|
||||
pricing: {
|
||||
basePrice: 200000,
|
||||
currency: 'KRW',
|
||||
priceType: 'project',
|
||||
priceRange: { min: 200000, max: 1500000 }
|
||||
},
|
||||
featured: true,
|
||||
isActive: true
|
||||
}
|
||||
];
|
||||
|
||||
const mockSettings = {
|
||||
siteName: 'SmartSolTech',
|
||||
siteDescription: '혁신적인 기술 솔루션으로 비즈니스의 성장을 지원합니다',
|
||||
contact: {
|
||||
email: 'info@smartsoltech.kr',
|
||||
phone: '+82-10-1234-5678',
|
||||
address: 'Seoul, South Korea'
|
||||
},
|
||||
social: {
|
||||
facebook: 'https://facebook.com/smartsoltech',
|
||||
twitter: 'https://twitter.com/smartsoltech',
|
||||
linkedin: 'https://linkedin.com/company/smartsoltech',
|
||||
instagram: 'https://instagram.com/smartsoltech'
|
||||
}
|
||||
};
|
||||
|
||||
// Helper function for category names
|
||||
function getCategoryName(category) {
|
||||
const categoryNames = {
|
||||
'web-development': '웹 개발',
|
||||
'mobile-app': '모바일 앱',
|
||||
'ui-ux-design': 'UI/UX 디자인',
|
||||
'branding': '브랜딩',
|
||||
'marketing': '디지털 마케팅'
|
||||
};
|
||||
return categoryNames[category] || category;
|
||||
}
|
||||
|
||||
// Routes
|
||||
app.get('/', (req, res) => {
|
||||
res.render('index', {
|
||||
title: 'SmartSolTech - Innovative Technology Solutions',
|
||||
settings: mockSettings,
|
||||
featuredPortfolio: mockPortfolio.filter(p => p.featured),
|
||||
featuredServices: mockServices.filter(s => s.featured),
|
||||
currentPage: 'home'
|
||||
});
|
||||
});
|
||||
|
||||
app.get('/portfolio', (req, res) => {
|
||||
const category = req.query.category;
|
||||
let filteredPortfolio = mockPortfolio;
|
||||
|
||||
if (category && category !== 'all') {
|
||||
filteredPortfolio = mockPortfolio.filter(p => p.category === category);
|
||||
}
|
||||
|
||||
res.render('portfolio', {
|
||||
title: '포트폴리오 - SmartSolTech',
|
||||
portfolioItems: filteredPortfolio,
|
||||
currentPage: 'portfolio'
|
||||
});
|
||||
});
|
||||
|
||||
app.get('/portfolio/:id', (req, res) => {
|
||||
const portfolio = mockPortfolio.find(p => p._id === req.params.id);
|
||||
|
||||
if (!portfolio) {
|
||||
return res.status(404).render('error', {
|
||||
title: '404 - 페이지를 찾을 수 없습니다',
|
||||
message: '요청하신 포트폴리오 항목을 찾을 수 없습니다.'
|
||||
});
|
||||
}
|
||||
|
||||
const relatedProjects = mockPortfolio.filter(p =>
|
||||
p._id !== portfolio._id && p.category === portfolio.category
|
||||
).slice(0, 3);
|
||||
|
||||
res.render('portfolio-detail', {
|
||||
title: `${portfolio.title} - 포트폴리오`,
|
||||
portfolio,
|
||||
relatedProjects,
|
||||
currentPage: 'portfolio'
|
||||
});
|
||||
});
|
||||
|
||||
app.get('/services', (req, res) => {
|
||||
res.render('services', {
|
||||
title: '서비스 - SmartSolTech',
|
||||
services: mockServices,
|
||||
currentPage: 'services'
|
||||
});
|
||||
});
|
||||
|
||||
app.get('/about', (req, res) => {
|
||||
res.render('about', {
|
||||
title: '회사 소개 - SmartSolTech',
|
||||
currentPage: 'about'
|
||||
});
|
||||
});
|
||||
|
||||
app.get('/contact', (req, res) => {
|
||||
res.render('contact', {
|
||||
title: '연락처 - SmartSolTech',
|
||||
settings: mockSettings,
|
||||
currentPage: 'contact'
|
||||
});
|
||||
});
|
||||
|
||||
app.post('/contact', (req, res) => {
|
||||
// Simulate contact form processing
|
||||
console.log('Contact form submission:', req.body);
|
||||
|
||||
req.flash('success', '문의가 성공적으로 접수되었습니다. 빠른 시일 내에 답변드리겠습니다.');
|
||||
res.redirect('/contact');
|
||||
});
|
||||
|
||||
app.get('/calculator', (req, res) => {
|
||||
res.render('calculator', {
|
||||
title: '비용 계산기 - SmartSolTech',
|
||||
services: mockServices,
|
||||
currentPage: 'calculator'
|
||||
});
|
||||
});
|
||||
|
||||
// API Routes for calculator
|
||||
app.post('/api/calculator/estimate', (req, res) => {
|
||||
const { service, projectType, timeline, features } = req.body;
|
||||
|
||||
// Simple calculation logic
|
||||
let basePrice = 500000;
|
||||
const selectedService = mockServices.find(s => s._id === service);
|
||||
|
||||
if (selectedService) {
|
||||
basePrice = selectedService.pricing.basePrice;
|
||||
}
|
||||
|
||||
// Apply multipliers based on project complexity
|
||||
const typeMultipliers = {
|
||||
'simple': 1,
|
||||
'medium': 1.5,
|
||||
'complex': 2.5,
|
||||
'enterprise': 4
|
||||
};
|
||||
|
||||
const timelineMultipliers = {
|
||||
'urgent': 1.5,
|
||||
'normal': 1,
|
||||
'flexible': 0.9
|
||||
};
|
||||
|
||||
const typeMultiplier = typeMultipliers[projectType] || 1;
|
||||
const timelineMultiplier = timelineMultipliers[timeline] || 1;
|
||||
const featuresMultiplier = 1 + (features?.length || 0) * 0.2;
|
||||
|
||||
const estimatedPrice = Math.round(basePrice * typeMultiplier * timelineMultiplier * featuresMultiplier);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
estimate: {
|
||||
basePrice,
|
||||
estimatedPrice,
|
||||
breakdown: {
|
||||
basePrice,
|
||||
projectType: projectType,
|
||||
typeMultiplier,
|
||||
timeline,
|
||||
timelineMultiplier,
|
||||
features: features || [],
|
||||
featuresMultiplier
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// API Routes for portfolio interactions
|
||||
app.post('/api/portfolio/:id/like', (req, res) => {
|
||||
const portfolio = mockPortfolio.find(p => p._id === req.params.id);
|
||||
if (portfolio) {
|
||||
portfolio.likes = (portfolio.likes || 0) + 1;
|
||||
res.json({ success: true, likes: portfolio.likes });
|
||||
} else {
|
||||
res.status(404).json({ success: false, message: 'Portfolio not found' });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/portfolio/:id/view', (req, res) => {
|
||||
const portfolio = mockPortfolio.find(p => p._id === req.params.id);
|
||||
if (portfolio) {
|
||||
portfolio.viewCount = (portfolio.viewCount || 0) + 1;
|
||||
res.json({ success: true, viewCount: portfolio.viewCount });
|
||||
} else {
|
||||
res.status(404).json({ success: false, message: 'Portfolio not found' });
|
||||
}
|
||||
});
|
||||
|
||||
// Error handling
|
||||
app.use((req, res) => {
|
||||
res.status(404).render('error', {
|
||||
title: '404 - 페이지를 찾을 수 없습니다',
|
||||
message: '요청하신 페이지를 찾을 수 없습니다.',
|
||||
currentPage: '404'
|
||||
});
|
||||
});
|
||||
|
||||
app.use((err, req, res, next) => {
|
||||
console.error(err.stack);
|
||||
res.status(500).render('error', {
|
||||
title: '500 - 서버 오류',
|
||||
message: '서버에서 오류가 발생했습니다.',
|
||||
currentPage: 'error'
|
||||
});
|
||||
});
|
||||
|
||||
// Start server
|
||||
app.listen(PORT, () => {
|
||||
console.log(`🚀 SmartSolTech Demo Server running on http://localhost:${PORT}`);
|
||||
console.log('📋 Available pages:');
|
||||
console.log(' • Home: http://localhost:3000/');
|
||||
console.log(' • Portfolio: http://localhost:3000/portfolio');
|
||||
console.log(' • Services: http://localhost:3000/services');
|
||||
console.log(' • Contact: http://localhost:3000/contact');
|
||||
console.log(' • Calculator: http://localhost:3000/calculator');
|
||||
console.log('');
|
||||
console.log('💡 This is a demo version using mock data');
|
||||
console.log('💾 To use with MongoDB, run: npm start');
|
||||
});
|
||||
|
||||
module.exports = app;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user