AdminLTE3
63
.history/ADMINLTE_CSP_FIX_20251026213607.md
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
# AdminLTE CSP Fix - Исправление Content Security Policy
|
||||||
|
|
||||||
|
## Проблема
|
||||||
|
Content Security Policy (CSP) блокировал загрузку jQuery и Bootstrap из CDN, что приводило к ошибкам:
|
||||||
|
|
||||||
|
```
|
||||||
|
Content-Security-Policy: The page's settings blocked a script (script-src-elem) at https://code.jquery.com/jquery-3.6.0.min.js
|
||||||
|
Content-Security-Policy: The page's settings blocked a script (script-src-elem) at https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/js/bootstrap.bundle.min.js
|
||||||
|
```
|
||||||
|
|
||||||
|
## Причина
|
||||||
|
В server.js в настройках helmet CSP для `scriptSrc` отсутствовали домены:
|
||||||
|
- `https://code.jquery.com` (для jQuery)
|
||||||
|
- `https://cdn.jsdelivr.net` (для Bootstrap)
|
||||||
|
|
||||||
|
## Решение
|
||||||
|
Обновлены CSP настройки в `/server.js`:
|
||||||
|
|
||||||
|
### ДО:
|
||||||
|
```javascript
|
||||||
|
scriptSrc: ["'self'", "'unsafe-inline'", "https://cdnjs.cloudflare.com", "https://unpkg.com", "https://cdn.tailwindcss.com"],
|
||||||
|
```
|
||||||
|
|
||||||
|
### ПОСЛЕ:
|
||||||
|
```javascript
|
||||||
|
scriptSrc: ["'self'", "'unsafe-inline'", "https://cdnjs.cloudflare.com", "https://unpkg.com", "https://cdn.tailwindcss.com", "https://code.jquery.com", "https://cdn.jsdelivr.net"],
|
||||||
|
```
|
||||||
|
|
||||||
|
## Полная CSP конфигурация
|
||||||
|
```javascript
|
||||||
|
app.use(helmet({
|
||||||
|
contentSecurityPolicy: {
|
||||||
|
directives: {
|
||||||
|
defaultSrc: ["'self'"],
|
||||||
|
styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com", "https://cdnjs.cloudflare.com", "https://cdn.jsdelivr.net", "https://unpkg.com", "https://cdn.tailwindcss.com"],
|
||||||
|
fontSrc: ["'self'", "https://fonts.gstatic.com", "https://cdnjs.cloudflare.com"],
|
||||||
|
scriptSrc: ["'self'", "'unsafe-inline'", "https://cdnjs.cloudflare.com", "https://unpkg.com", "https://cdn.tailwindcss.com", "https://code.jquery.com", "https://cdn.jsdelivr.net"],
|
||||||
|
imgSrc: ["'self'", "data:", "https:"],
|
||||||
|
connectSrc: ["'self'", "ws:", "wss:", "https://cdnjs.cloudflare.com", "https://cdn.jsdelivr.net", "https://unpkg.com", "https://fonts.googleapis.com", "https://fonts.gstatic.com", "https://cdn.tailwindcss.com"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
```
|
||||||
|
|
||||||
|
## Результат
|
||||||
|
✅ AdminLTE 3 теперь работает полностью:
|
||||||
|
- ✅ CSS стили загружаются
|
||||||
|
- ✅ jQuery загружается без ошибок CSP
|
||||||
|
- ✅ Bootstrap загружается без ошибок CSP
|
||||||
|
- ✅ AdminLTE JavaScript функции работают
|
||||||
|
- ✅ Интерактивные элементы функциональны
|
||||||
|
|
||||||
|
## Тестирование
|
||||||
|
После исправления CSP:
|
||||||
|
1. Перезапустить сервер: `npm start`
|
||||||
|
2. Открыть админку: `http://localhost:3000/admin`
|
||||||
|
3. Проверить консоль браузера - ошибки CSP исчезли
|
||||||
|
4. Проверить функциональность: навигация, выпадающие меню, модальные окна
|
||||||
|
|
||||||
|
## Примечания
|
||||||
|
- Исправление обеспечивает безопасность через CSP при разрешении необходимых CDN
|
||||||
|
- AdminLTE теперь полностью функциональна с корейской локализацией
|
||||||
|
- Все зависимости загружаются правильно
|
||||||
63
.history/ADMINLTE_CSP_FIX_20251026213623.md
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
# AdminLTE CSP Fix - Исправление Content Security Policy
|
||||||
|
|
||||||
|
## Проблема
|
||||||
|
Content Security Policy (CSP) блокировал загрузку jQuery и Bootstrap из CDN, что приводило к ошибкам:
|
||||||
|
|
||||||
|
```
|
||||||
|
Content-Security-Policy: The page's settings blocked a script (script-src-elem) at https://code.jquery.com/jquery-3.6.0.min.js
|
||||||
|
Content-Security-Policy: The page's settings blocked a script (script-src-elem) at https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/js/bootstrap.bundle.min.js
|
||||||
|
```
|
||||||
|
|
||||||
|
## Причина
|
||||||
|
В server.js в настройках helmet CSP для `scriptSrc` отсутствовали домены:
|
||||||
|
- `https://code.jquery.com` (для jQuery)
|
||||||
|
- `https://cdn.jsdelivr.net` (для Bootstrap)
|
||||||
|
|
||||||
|
## Решение
|
||||||
|
Обновлены CSP настройки в `/server.js`:
|
||||||
|
|
||||||
|
### ДО:
|
||||||
|
```javascript
|
||||||
|
scriptSrc: ["'self'", "'unsafe-inline'", "https://cdnjs.cloudflare.com", "https://unpkg.com", "https://cdn.tailwindcss.com"],
|
||||||
|
```
|
||||||
|
|
||||||
|
### ПОСЛЕ:
|
||||||
|
```javascript
|
||||||
|
scriptSrc: ["'self'", "'unsafe-inline'", "https://cdnjs.cloudflare.com", "https://unpkg.com", "https://cdn.tailwindcss.com", "https://code.jquery.com", "https://cdn.jsdelivr.net"],
|
||||||
|
```
|
||||||
|
|
||||||
|
## Полная CSP конфигурация
|
||||||
|
```javascript
|
||||||
|
app.use(helmet({
|
||||||
|
contentSecurityPolicy: {
|
||||||
|
directives: {
|
||||||
|
defaultSrc: ["'self'"],
|
||||||
|
styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com", "https://cdnjs.cloudflare.com", "https://cdn.jsdelivr.net", "https://unpkg.com", "https://cdn.tailwindcss.com"],
|
||||||
|
fontSrc: ["'self'", "https://fonts.gstatic.com", "https://cdnjs.cloudflare.com"],
|
||||||
|
scriptSrc: ["'self'", "'unsafe-inline'", "https://cdnjs.cloudflare.com", "https://unpkg.com", "https://cdn.tailwindcss.com", "https://code.jquery.com", "https://cdn.jsdelivr.net"],
|
||||||
|
imgSrc: ["'self'", "data:", "https:"],
|
||||||
|
connectSrc: ["'self'", "ws:", "wss:", "https://cdnjs.cloudflare.com", "https://cdn.jsdelivr.net", "https://unpkg.com", "https://fonts.googleapis.com", "https://fonts.gstatic.com", "https://cdn.tailwindcss.com"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
```
|
||||||
|
|
||||||
|
## Результат
|
||||||
|
✅ AdminLTE 3 теперь работает полностью:
|
||||||
|
- ✅ CSS стили загружаются
|
||||||
|
- ✅ jQuery загружается без ошибок CSP
|
||||||
|
- ✅ Bootstrap загружается без ошибок CSP
|
||||||
|
- ✅ AdminLTE JavaScript функции работают
|
||||||
|
- ✅ Интерактивные элементы функциональны
|
||||||
|
|
||||||
|
## Тестирование
|
||||||
|
После исправления CSP:
|
||||||
|
1. Перезапустить сервер: `npm start`
|
||||||
|
2. Открыть админку: `http://localhost:3000/admin`
|
||||||
|
3. Проверить консоль браузера - ошибки CSP исчезли
|
||||||
|
4. Проверить функциональность: навигация, выпадающие меню, модальные окна
|
||||||
|
|
||||||
|
## Примечания
|
||||||
|
- Исправление обеспечивает безопасность через CSP при разрешении необходимых CDN
|
||||||
|
- AdminLTE теперь полностью функциональна с корейской локализацией
|
||||||
|
- Все зависимости загружаются правильно
|
||||||
109
.history/ADMINLTE_SETUP_COMPLETE_20251026212843.md
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
# SmartSolTech AdminLTE 3 - Документация
|
||||||
|
|
||||||
|
## ✅ Успешно настроен AdminLTE 3!
|
||||||
|
|
||||||
|
AdminLTE 3 теперь полностью интегрирован в ваш сайт SmartSolTech как основная админ-панель.
|
||||||
|
|
||||||
|
## 🎯 Что было сделано:
|
||||||
|
|
||||||
|
### 1. **Полная замена старой админки**
|
||||||
|
- ❌ Удалена старая Tailwind-админка
|
||||||
|
- ❌ Удалены все демо-файлы и сравнения
|
||||||
|
- ❌ Удален Tabler и связанные файлы
|
||||||
|
- ✅ AdminLTE 3 стал основной админкой
|
||||||
|
|
||||||
|
### 2. **Новый дизайн и функциональность**
|
||||||
|
- 🎨 **Корейская локализация** - все элементы переведены
|
||||||
|
- 📊 **Современный dashboard** с информативными карточками
|
||||||
|
- 🔢 **Статистика в sidebar** - счетчики портфолио, услуг, сообщений
|
||||||
|
- 📱 **Адаптивный дизайн** для всех устройств
|
||||||
|
- 🎭 **Профессиональная цветовая схема**
|
||||||
|
|
||||||
|
### 3. **Интеграция с существующей системой**
|
||||||
|
- 🔗 **Полная совместимость** с текущими маршрутами
|
||||||
|
- 📊 **Middleware для статистики** на всех страницах
|
||||||
|
- 🔐 **Сохранена вся авторизация** и безопасность
|
||||||
|
- 💾 **Все данные остались** без изменений
|
||||||
|
|
||||||
|
## 🎨 Основные возможности AdminLTE:
|
||||||
|
|
||||||
|
### Dashboard (대시보드)
|
||||||
|
- 📈 **Статистические карточки** с progress bar
|
||||||
|
- 📋 **Последние проекты** с статусами (완료/진행중/계획)
|
||||||
|
- 📧 **Последние сообщения** с цветными статусами
|
||||||
|
- ⚡ **Быстрые действия** с градиентными кнопками
|
||||||
|
- 📊 **Системный статус** и метрики
|
||||||
|
|
||||||
|
### Навигация
|
||||||
|
- 🎯 **Активные индикаторы** текущей страницы
|
||||||
|
- 🔢 **Счетчики в sidebar** для каждого раздела
|
||||||
|
- 🌟 **Группировка разделов** (основные + системные)
|
||||||
|
- 📱 **Складное меню** для мобильных
|
||||||
|
|
||||||
|
### Дизайн
|
||||||
|
- 🇰🇷 **Шрифт Noto Sans KR** для корейского текста
|
||||||
|
- 🎨 **Градиентные элементы** и современные цвета
|
||||||
|
- 💎 **Закругленные карточки** с тенями
|
||||||
|
- ⚡ **Плавные анимации** и переходы
|
||||||
|
|
||||||
|
## 🔧 Техническая информация:
|
||||||
|
|
||||||
|
### Файлы
|
||||||
|
- `views/admin/layout.ejs` - Основной layout AdminLTE
|
||||||
|
- `views/admin/dashboard.ejs` - Новый dashboard
|
||||||
|
- `routes/admin.js` - Обновленные маршруты с middleware
|
||||||
|
|
||||||
|
### CSS & JS
|
||||||
|
- AdminLTE 3 CSS из `node_modules/admin-lte/dist/css/`
|
||||||
|
- jQuery + Bootstrap 4 для функциональности
|
||||||
|
- Font Awesome 6 для иконок
|
||||||
|
- Custom CSS для корейских шрифтов
|
||||||
|
|
||||||
|
### Middleware
|
||||||
|
```javascript
|
||||||
|
// Автоматически добавляет статистику на все админ-страницы
|
||||||
|
const addStats = async (req, res, next) => {
|
||||||
|
// portfolioCount, servicesCount, contactsCount
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🚀 Доступные URL:
|
||||||
|
|
||||||
|
- **Админ-панель:** http://localhost:3000/admin
|
||||||
|
- **Dashboard:** http://localhost:3000/admin/dashboard
|
||||||
|
- **Портфолио:** http://localhost:3000/admin/portfolio
|
||||||
|
- **Услуги:** http://localhost:3000/admin/services
|
||||||
|
- **Сообщения:** http://localhost:3000/admin/contacts
|
||||||
|
- **Медиа:** http://localhost:3000/admin/media
|
||||||
|
- **Настройки:** http://localhost:3000/admin/settings
|
||||||
|
- **Телеграм:** http://localhost:3000/admin/telegram
|
||||||
|
- **Баннеры:** http://localhost:3000/admin/banner-editor
|
||||||
|
|
||||||
|
## 💡 Преимущества новой админки:
|
||||||
|
|
||||||
|
1. **Профессиональный вид** - соответствует корейским стандартам
|
||||||
|
2. **Удобство использования** - интуитивный интерфейс
|
||||||
|
3. **Производительность** - быстрая загрузка
|
||||||
|
4. **Мобильность** - отлично работает на телефонах
|
||||||
|
5. **Стабильность** - проверенное решение
|
||||||
|
6. **Бесплатность** - никаких лицензионных ограничений
|
||||||
|
|
||||||
|
## 🎯 Что дальше?
|
||||||
|
|
||||||
|
AdminLTE 3 полностью готов к работе! Можете:
|
||||||
|
|
||||||
|
1. **Войти в админку** по адресу `/admin`
|
||||||
|
2. **Управлять контентом** через новый интерфейс
|
||||||
|
3. **Добавлять проекты** и услуги
|
||||||
|
4. **Настраивать сайт** через удобные формы
|
||||||
|
5. **Следить за статистикой** в реальном времени
|
||||||
|
|
||||||
|
## 🔑 Логин:
|
||||||
|
- **Email:** admin@smartsoltech.kr
|
||||||
|
- **Пароль:** как настроено в .env
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**🎉 AdminLTE 3 успешно настроен и готов к использованию!**
|
||||||
|
|
||||||
|
Старая админка полностью удалена, новая AdminLTE интегрирована и работает со всеми существующими функциями сайта.
|
||||||
109
.history/ADMINLTE_SETUP_COMPLETE_20251026212848.md
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
# SmartSolTech AdminLTE 3 - Документация
|
||||||
|
|
||||||
|
## ✅ Успешно настроен AdminLTE 3!
|
||||||
|
|
||||||
|
AdminLTE 3 теперь полностью интегрирован в ваш сайт SmartSolTech как основная админ-панель.
|
||||||
|
|
||||||
|
## 🎯 Что было сделано:
|
||||||
|
|
||||||
|
### 1. **Полная замена старой админки**
|
||||||
|
- ❌ Удалена старая Tailwind-админка
|
||||||
|
- ❌ Удалены все демо-файлы и сравнения
|
||||||
|
- ❌ Удален Tabler и связанные файлы
|
||||||
|
- ✅ AdminLTE 3 стал основной админкой
|
||||||
|
|
||||||
|
### 2. **Новый дизайн и функциональность**
|
||||||
|
- 🎨 **Корейская локализация** - все элементы переведены
|
||||||
|
- 📊 **Современный dashboard** с информативными карточками
|
||||||
|
- 🔢 **Статистика в sidebar** - счетчики портфолио, услуг, сообщений
|
||||||
|
- 📱 **Адаптивный дизайн** для всех устройств
|
||||||
|
- 🎭 **Профессиональная цветовая схема**
|
||||||
|
|
||||||
|
### 3. **Интеграция с существующей системой**
|
||||||
|
- 🔗 **Полная совместимость** с текущими маршрутами
|
||||||
|
- 📊 **Middleware для статистики** на всех страницах
|
||||||
|
- 🔐 **Сохранена вся авторизация** и безопасность
|
||||||
|
- 💾 **Все данные остались** без изменений
|
||||||
|
|
||||||
|
## 🎨 Основные возможности AdminLTE:
|
||||||
|
|
||||||
|
### Dashboard (대시보드)
|
||||||
|
- 📈 **Статистические карточки** с progress bar
|
||||||
|
- 📋 **Последние проекты** с статусами (완료/진행중/계획)
|
||||||
|
- 📧 **Последние сообщения** с цветными статусами
|
||||||
|
- ⚡ **Быстрые действия** с градиентными кнопками
|
||||||
|
- 📊 **Системный статус** и метрики
|
||||||
|
|
||||||
|
### Навигация
|
||||||
|
- 🎯 **Активные индикаторы** текущей страницы
|
||||||
|
- 🔢 **Счетчики в sidebar** для каждого раздела
|
||||||
|
- 🌟 **Группировка разделов** (основные + системные)
|
||||||
|
- 📱 **Складное меню** для мобильных
|
||||||
|
|
||||||
|
### Дизайн
|
||||||
|
- 🇰🇷 **Шрифт Noto Sans KR** для корейского текста
|
||||||
|
- 🎨 **Градиентные элементы** и современные цвета
|
||||||
|
- 💎 **Закругленные карточки** с тенями
|
||||||
|
- ⚡ **Плавные анимации** и переходы
|
||||||
|
|
||||||
|
## 🔧 Техническая информация:
|
||||||
|
|
||||||
|
### Файлы
|
||||||
|
- `views/admin/layout.ejs` - Основной layout AdminLTE
|
||||||
|
- `views/admin/dashboard.ejs` - Новый dashboard
|
||||||
|
- `routes/admin.js` - Обновленные маршруты с middleware
|
||||||
|
|
||||||
|
### CSS & JS
|
||||||
|
- AdminLTE 3 CSS из `node_modules/admin-lte/dist/css/`
|
||||||
|
- jQuery + Bootstrap 4 для функциональности
|
||||||
|
- Font Awesome 6 для иконок
|
||||||
|
- Custom CSS для корейских шрифтов
|
||||||
|
|
||||||
|
### Middleware
|
||||||
|
```javascript
|
||||||
|
// Автоматически добавляет статистику на все админ-страницы
|
||||||
|
const addStats = async (req, res, next) => {
|
||||||
|
// portfolioCount, servicesCount, contactsCount
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🚀 Доступные URL:
|
||||||
|
|
||||||
|
- **Админ-панель:** http://localhost:3000/admin
|
||||||
|
- **Dashboard:** http://localhost:3000/admin/dashboard
|
||||||
|
- **Портфолио:** http://localhost:3000/admin/portfolio
|
||||||
|
- **Услуги:** http://localhost:3000/admin/services
|
||||||
|
- **Сообщения:** http://localhost:3000/admin/contacts
|
||||||
|
- **Медиа:** http://localhost:3000/admin/media
|
||||||
|
- **Настройки:** http://localhost:3000/admin/settings
|
||||||
|
- **Телеграм:** http://localhost:3000/admin/telegram
|
||||||
|
- **Баннеры:** http://localhost:3000/admin/banner-editor
|
||||||
|
|
||||||
|
## 💡 Преимущества новой админки:
|
||||||
|
|
||||||
|
1. **Профессиональный вид** - соответствует корейским стандартам
|
||||||
|
2. **Удобство использования** - интуитивный интерфейс
|
||||||
|
3. **Производительность** - быстрая загрузка
|
||||||
|
4. **Мобильность** - отлично работает на телефонах
|
||||||
|
5. **Стабильность** - проверенное решение
|
||||||
|
6. **Бесплатность** - никаких лицензионных ограничений
|
||||||
|
|
||||||
|
## 🎯 Что дальше?
|
||||||
|
|
||||||
|
AdminLTE 3 полностью готов к работе! Можете:
|
||||||
|
|
||||||
|
1. **Войти в админку** по адресу `/admin`
|
||||||
|
2. **Управлять контентом** через новый интерфейс
|
||||||
|
3. **Добавлять проекты** и услуги
|
||||||
|
4. **Настраивать сайт** через удобные формы
|
||||||
|
5. **Следить за статистикой** в реальном времени
|
||||||
|
|
||||||
|
## 🔑 Логин:
|
||||||
|
- **Email:** admin@smartsoltech.kr
|
||||||
|
- **Пароль:** как настроено в .env
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**🎉 AdminLTE 3 успешно настроен и готов к использованию!**
|
||||||
|
|
||||||
|
Старая админка полностью удалена, новая AdminLTE интегрирована и работает со всеми существующими функциями сайта.
|
||||||
145
.history/ADMIN_BUNDLE_RECOMMENDATIONS_20251026211921.md
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
# SmartSolTech Admin Bundle Рекомендации
|
||||||
|
|
||||||
|
## 🎯 Итоговый Выбор: AdminLTE 3
|
||||||
|
|
||||||
|
После детального анализа и тестирования обеих админ-панелей, **AdminLTE 3** является оптимальным выбором для SmartSolTech.
|
||||||
|
|
||||||
|
## 📊 Анализ Установленных Решений
|
||||||
|
|
||||||
|
### 1. AdminLTE 3 ✅ (Рекомендуется)
|
||||||
|
|
||||||
|
**Установка:** `npm install admin-lte`
|
||||||
|
|
||||||
|
**Преимущества:**
|
||||||
|
- ✅ **Полностью бесплатен** - нет скрытых платежей
|
||||||
|
- ✅ **Отличная поддержка корейского языка** - идеально для вашей аудитории
|
||||||
|
- ✅ **Богатая экосистема компонентов** - все необходимые элементы UI
|
||||||
|
- ✅ **Стабильность** - проверенное временем решение
|
||||||
|
- ✅ **Большое сообщество** - легко найти помощь и примеры
|
||||||
|
- ✅ **jQuery совместимость** - работает с существующим кодом
|
||||||
|
|
||||||
|
**Интеграция:**
|
||||||
|
```javascript
|
||||||
|
// Уже создано:
|
||||||
|
/views/admin/layout-adminlte.ejs // Основной layout
|
||||||
|
/views/admin/dashboard-adminlte.ejs // Dashboard с корейской локализацией
|
||||||
|
/demo/demo-adminlte // Демо URL
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Tabler ⚠️ (Альтернатива)
|
||||||
|
|
||||||
|
**Установка:** `npm install @tabler/core @tabler/icons`
|
||||||
|
|
||||||
|
**Преимущества:**
|
||||||
|
- ✅ **Современный дизайн** - более свежий внешний вид
|
||||||
|
- ✅ **Высокая производительность** - быстрая загрузка
|
||||||
|
- ✅ **3000+ SVG иконок** - богатая иконография
|
||||||
|
- ✅ **Bootstrap 5** - современные стандарты
|
||||||
|
|
||||||
|
**Недостатки:**
|
||||||
|
- ⚠️ **Требует Node.js 20+** (у вас 18.19.1)
|
||||||
|
- ⚠️ **Менее зрелое решение** - меньше документации на корейском
|
||||||
|
- ⚠️ **Меньше готовых компонентов** - больше кастомизации
|
||||||
|
|
||||||
|
## 🔧 Готовые к Использованию URL
|
||||||
|
|
||||||
|
После запуска сервера доступны:
|
||||||
|
|
||||||
|
1. **Страница сравнения:** http://localhost:3000/demo/admin-comparison
|
||||||
|
2. **AdminLTE Demo:** http://localhost:3000/demo/demo-adminlte
|
||||||
|
3. **Tabler Demo:** http://localhost:3000/demo/demo-tabler
|
||||||
|
|
||||||
|
## 🚀 Интеграция с Существующей Админкой
|
||||||
|
|
||||||
|
### Вариант 1: Постепенная Миграция (Рекомендуется)
|
||||||
|
|
||||||
|
1. **Обновить существующий layout:**
|
||||||
|
```bash
|
||||||
|
# Заменить существующий layout на AdminLTE
|
||||||
|
cp views/admin/layout-adminlte.ejs views/admin/layout.ejs
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Обновить dashboard:**
|
||||||
|
```bash
|
||||||
|
# Заменить dashboard на AdminLTE версию
|
||||||
|
cp views/admin/dashboard-adminlte.ejs views/admin/dashboard.ejs
|
||||||
|
```
|
||||||
|
|
||||||
|
### Вариант 2: Параллельная Система
|
||||||
|
|
||||||
|
Оставить существующую админку и добавить новую через роуты:
|
||||||
|
```javascript
|
||||||
|
// В routes/admin.js добавить:
|
||||||
|
router.get('/modern', (req, res) => {
|
||||||
|
res.render('admin/dashboard-adminlte', {
|
||||||
|
layout: 'admin/layout-adminlte'
|
||||||
|
// ... остальные данные
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎨 Кастомизация под SmartSolTech
|
||||||
|
|
||||||
|
### Цветовая Схема
|
||||||
|
```css
|
||||||
|
/* Добавлено в layout-adminlte.ejs: */
|
||||||
|
.nav-sidebar .nav-link.active {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-text {
|
||||||
|
color: #007bff !important;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Корейская Локализация
|
||||||
|
- ✅ Все меню переведены на корейский
|
||||||
|
- ✅ Даты в корейском формате
|
||||||
|
- ✅ Статусы и сообщения на корейском
|
||||||
|
- ✅ Шрифт Noto Sans KR для корректного отображения
|
||||||
|
|
||||||
|
## 📋 Следующие Шаги
|
||||||
|
|
||||||
|
1. **Тестирование:**
|
||||||
|
```bash
|
||||||
|
# Откройте демо:
|
||||||
|
http://localhost:3000/demo/admin-comparison
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Интеграция:**
|
||||||
|
```bash
|
||||||
|
# Если решили использовать AdminLTE:
|
||||||
|
cp views/admin/layout-adminlte.ejs views/admin/layout.ejs
|
||||||
|
cp views/admin/dashboard-adminlte.ejs views/admin/dashboard.ejs
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Создание дополнительных страниц:**
|
||||||
|
- Список портфолио в стиле AdminLTE
|
||||||
|
- Управление услугами
|
||||||
|
- Страница контактов
|
||||||
|
- Настройки системы
|
||||||
|
|
||||||
|
## 💡 Дополнительные Рекомендации
|
||||||
|
|
||||||
|
### Для Улучшения UX:
|
||||||
|
1. **Темная тема** - AdminLTE поддерживает переключение тем
|
||||||
|
2. **Мобильная оптимизация** - уже включена
|
||||||
|
3. **Уведомления** - можно добавить toast-уведомления
|
||||||
|
4. **Виджеты** - использовать info-box для статистики
|
||||||
|
|
||||||
|
### Производительность:
|
||||||
|
- AdminLTE загружается быстро (~500KB)
|
||||||
|
- Минимальные зависимости
|
||||||
|
- Отличная совместимость с вашим стеком
|
||||||
|
|
||||||
|
## 🎯 Финальная Рекомендация
|
||||||
|
|
||||||
|
**Используйте AdminLTE 3** для SmartSolTech потому что:
|
||||||
|
|
||||||
|
1. **Готов к production** - стабилен и протестирован
|
||||||
|
2. **Корейская аудитория** - отличная поддержка языка
|
||||||
|
3. **Бесплатный** - нет лицензионных ограничений
|
||||||
|
4. **Совместимость** - работает с вашей текущей архитектурой
|
||||||
|
5. **Поддержка** - большое сообщество и документация
|
||||||
|
|
||||||
|
Начните с демо-версии по адресу `/demo/demo-adminlte` и при желании интегрируйте в основную админку!
|
||||||
145
.history/ADMIN_BUNDLE_RECOMMENDATIONS_20251026212002.md
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
# SmartSolTech Admin Bundle Рекомендации
|
||||||
|
|
||||||
|
## 🎯 Итоговый Выбор: AdminLTE 3
|
||||||
|
|
||||||
|
После детального анализа и тестирования обеих админ-панелей, **AdminLTE 3** является оптимальным выбором для SmartSolTech.
|
||||||
|
|
||||||
|
## 📊 Анализ Установленных Решений
|
||||||
|
|
||||||
|
### 1. AdminLTE 3 ✅ (Рекомендуется)
|
||||||
|
|
||||||
|
**Установка:** `npm install admin-lte`
|
||||||
|
|
||||||
|
**Преимущества:**
|
||||||
|
- ✅ **Полностью бесплатен** - нет скрытых платежей
|
||||||
|
- ✅ **Отличная поддержка корейского языка** - идеально для вашей аудитории
|
||||||
|
- ✅ **Богатая экосистема компонентов** - все необходимые элементы UI
|
||||||
|
- ✅ **Стабильность** - проверенное временем решение
|
||||||
|
- ✅ **Большое сообщество** - легко найти помощь и примеры
|
||||||
|
- ✅ **jQuery совместимость** - работает с существующим кодом
|
||||||
|
|
||||||
|
**Интеграция:**
|
||||||
|
```javascript
|
||||||
|
// Уже создано:
|
||||||
|
/views/admin/layout-adminlte.ejs // Основной layout
|
||||||
|
/views/admin/dashboard-adminlte.ejs // Dashboard с корейской локализацией
|
||||||
|
/demo/demo-adminlte // Демо URL
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Tabler ⚠️ (Альтернатива)
|
||||||
|
|
||||||
|
**Установка:** `npm install @tabler/core @tabler/icons`
|
||||||
|
|
||||||
|
**Преимущества:**
|
||||||
|
- ✅ **Современный дизайн** - более свежий внешний вид
|
||||||
|
- ✅ **Высокая производительность** - быстрая загрузка
|
||||||
|
- ✅ **3000+ SVG иконок** - богатая иконография
|
||||||
|
- ✅ **Bootstrap 5** - современные стандарты
|
||||||
|
|
||||||
|
**Недостатки:**
|
||||||
|
- ⚠️ **Требует Node.js 20+** (у вас 18.19.1)
|
||||||
|
- ⚠️ **Менее зрелое решение** - меньше документации на корейском
|
||||||
|
- ⚠️ **Меньше готовых компонентов** - больше кастомизации
|
||||||
|
|
||||||
|
## 🔧 Готовые к Использованию URL
|
||||||
|
|
||||||
|
После запуска сервера доступны:
|
||||||
|
|
||||||
|
1. **Страница сравнения:** http://localhost:3000/demo/admin-comparison
|
||||||
|
2. **AdminLTE Demo:** http://localhost:3000/demo/demo-adminlte
|
||||||
|
3. **Tabler Demo:** http://localhost:3000/demo/demo-tabler
|
||||||
|
|
||||||
|
## 🚀 Интеграция с Существующей Админкой
|
||||||
|
|
||||||
|
### Вариант 1: Постепенная Миграция (Рекомендуется)
|
||||||
|
|
||||||
|
1. **Обновить существующий layout:**
|
||||||
|
```bash
|
||||||
|
# Заменить существующий layout на AdminLTE
|
||||||
|
cp views/admin/layout-adminlte.ejs views/admin/layout.ejs
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Обновить dashboard:**
|
||||||
|
```bash
|
||||||
|
# Заменить dashboard на AdminLTE версию
|
||||||
|
cp views/admin/dashboard-adminlte.ejs views/admin/dashboard.ejs
|
||||||
|
```
|
||||||
|
|
||||||
|
### Вариант 2: Параллельная Система
|
||||||
|
|
||||||
|
Оставить существующую админку и добавить новую через роуты:
|
||||||
|
```javascript
|
||||||
|
// В routes/admin.js добавить:
|
||||||
|
router.get('/modern', (req, res) => {
|
||||||
|
res.render('admin/dashboard-adminlte', {
|
||||||
|
layout: 'admin/layout-adminlte'
|
||||||
|
// ... остальные данные
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎨 Кастомизация под SmartSolTech
|
||||||
|
|
||||||
|
### Цветовая Схема
|
||||||
|
```css
|
||||||
|
/* Добавлено в layout-adminlte.ejs: */
|
||||||
|
.nav-sidebar .nav-link.active {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-text {
|
||||||
|
color: #007bff !important;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Корейская Локализация
|
||||||
|
- ✅ Все меню переведены на корейский
|
||||||
|
- ✅ Даты в корейском формате
|
||||||
|
- ✅ Статусы и сообщения на корейском
|
||||||
|
- ✅ Шрифт Noto Sans KR для корректного отображения
|
||||||
|
|
||||||
|
## 📋 Следующие Шаги
|
||||||
|
|
||||||
|
1. **Тестирование:**
|
||||||
|
```bash
|
||||||
|
# Откройте демо:
|
||||||
|
http://localhost:3000/demo/admin-comparison
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Интеграция:**
|
||||||
|
```bash
|
||||||
|
# Если решили использовать AdminLTE:
|
||||||
|
cp views/admin/layout-adminlte.ejs views/admin/layout.ejs
|
||||||
|
cp views/admin/dashboard-adminlte.ejs views/admin/dashboard.ejs
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Создание дополнительных страниц:**
|
||||||
|
- Список портфолио в стиле AdminLTE
|
||||||
|
- Управление услугами
|
||||||
|
- Страница контактов
|
||||||
|
- Настройки системы
|
||||||
|
|
||||||
|
## 💡 Дополнительные Рекомендации
|
||||||
|
|
||||||
|
### Для Улучшения UX:
|
||||||
|
1. **Темная тема** - AdminLTE поддерживает переключение тем
|
||||||
|
2. **Мобильная оптимизация** - уже включена
|
||||||
|
3. **Уведомления** - можно добавить toast-уведомления
|
||||||
|
4. **Виджеты** - использовать info-box для статистики
|
||||||
|
|
||||||
|
### Производительность:
|
||||||
|
- AdminLTE загружается быстро (~500KB)
|
||||||
|
- Минимальные зависимости
|
||||||
|
- Отличная совместимость с вашим стеком
|
||||||
|
|
||||||
|
## 🎯 Финальная Рекомендация
|
||||||
|
|
||||||
|
**Используйте AdminLTE 3** для SmartSolTech потому что:
|
||||||
|
|
||||||
|
1. **Готов к production** - стабилен и протестирован
|
||||||
|
2. **Корейская аудитория** - отличная поддержка языка
|
||||||
|
3. **Бесплатный** - нет лицензионных ограничений
|
||||||
|
4. **Совместимость** - работает с вашей текущей архитектурой
|
||||||
|
5. **Поддержка** - большое сообщество и документация
|
||||||
|
|
||||||
|
Начните с демо-версии по адресу `/demo/demo-adminlte` и при желании интегрируйте в основную админку!
|
||||||
103
.history/models/Service_20251026221128.js
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
const { DataTypes } = require('sequelize');
|
||||||
|
const { sequelize } = require('../config/database');
|
||||||
|
|
||||||
|
const Service = sequelize.define('Service', {
|
||||||
|
id: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
defaultValue: DataTypes.UUIDV4,
|
||||||
|
primaryKey: true
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: false,
|
||||||
|
validate: {
|
||||||
|
len: [1, 255]
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
this.setDataValue('name', value.trim());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
type: DataTypes.TEXT,
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
shortDescription: {
|
||||||
|
type: DataTypes.STRING(150),
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
icon: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
category: {
|
||||||
|
type: DataTypes.ENUM(
|
||||||
|
'web-development',
|
||||||
|
'mobile-development',
|
||||||
|
'ui-ux-design',
|
||||||
|
'consulting',
|
||||||
|
'support',
|
||||||
|
'other'
|
||||||
|
),
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
features: {
|
||||||
|
type: DataTypes.JSONB,
|
||||||
|
defaultValue: []
|
||||||
|
},
|
||||||
|
pricing: {
|
||||||
|
type: DataTypes.JSONB,
|
||||||
|
allowNull: false,
|
||||||
|
validate: {
|
||||||
|
isValidPricing(value) {
|
||||||
|
if (!value.basePrice || value.basePrice < 0) {
|
||||||
|
throw new Error('Base price must be a positive number');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
estimatedTime: {
|
||||||
|
type: DataTypes.JSONB,
|
||||||
|
allowNull: false,
|
||||||
|
validate: {
|
||||||
|
isValidTime(value) {
|
||||||
|
if (!value.min || !value.max || value.min > value.max) {
|
||||||
|
throw new Error('Invalid estimated time range');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
isActive: {
|
||||||
|
type: DataTypes.BOOLEAN,
|
||||||
|
defaultValue: true
|
||||||
|
},
|
||||||
|
featured: {
|
||||||
|
type: DataTypes.BOOLEAN,
|
||||||
|
defaultValue: false
|
||||||
|
},
|
||||||
|
order: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
defaultValue: 0
|
||||||
|
},
|
||||||
|
tags: {
|
||||||
|
type: DataTypes.ARRAY(DataTypes.STRING),
|
||||||
|
defaultValue: []
|
||||||
|
},
|
||||||
|
seo: {
|
||||||
|
type: DataTypes.JSONB,
|
||||||
|
defaultValue: {}
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
tableName: 'services',
|
||||||
|
timestamps: true,
|
||||||
|
indexes: [
|
||||||
|
{
|
||||||
|
fields: ['category', 'featured', 'order']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'gin',
|
||||||
|
fields: ['tags']
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = Service;
|
||||||
96
.history/models/Service_20251026221134.js
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
const { DataTypes } = require('sequelize');
|
||||||
|
const { sequelize } = require('../config/database');
|
||||||
|
|
||||||
|
const Service = sequelize.define('Service', {
|
||||||
|
id: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
defaultValue: DataTypes.UUIDV4,
|
||||||
|
primaryKey: true
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: false,
|
||||||
|
validate: {
|
||||||
|
len: [1, 255]
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
this.setDataValue('name', value.trim());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
type: DataTypes.TEXT,
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
shortDescription: {
|
||||||
|
type: DataTypes.STRING(150),
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
icon: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
category: {
|
||||||
|
type: DataTypes.ENUM(
|
||||||
|
'web-development',
|
||||||
|
'mobile-development',
|
||||||
|
'ui-ux-design',
|
||||||
|
'consulting',
|
||||||
|
'support',
|
||||||
|
'other'
|
||||||
|
),
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
features: {
|
||||||
|
type: DataTypes.JSONB,
|
||||||
|
defaultValue: []
|
||||||
|
},
|
||||||
|
pricing: {
|
||||||
|
type: DataTypes.JSONB,
|
||||||
|
allowNull: false,
|
||||||
|
validate: {
|
||||||
|
isValidPricing(value) {
|
||||||
|
if (!value.basePrice || value.basePrice < 0) {
|
||||||
|
throw new Error('Base price must be a positive number');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
estimatedTime: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true
|
||||||
|
},
|
||||||
|
isActive: {
|
||||||
|
type: DataTypes.BOOLEAN,
|
||||||
|
defaultValue: true
|
||||||
|
},
|
||||||
|
featured: {
|
||||||
|
type: DataTypes.BOOLEAN,
|
||||||
|
defaultValue: false
|
||||||
|
},
|
||||||
|
order: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
defaultValue: 0
|
||||||
|
},
|
||||||
|
tags: {
|
||||||
|
type: DataTypes.ARRAY(DataTypes.STRING),
|
||||||
|
defaultValue: []
|
||||||
|
},
|
||||||
|
seo: {
|
||||||
|
type: DataTypes.JSONB,
|
||||||
|
defaultValue: {}
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
tableName: 'services',
|
||||||
|
timestamps: true,
|
||||||
|
indexes: [
|
||||||
|
{
|
||||||
|
fields: ['category', 'featured', 'order']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'gin',
|
||||||
|
fields: ['tags']
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = Service;
|
||||||
96
.history/models/Service_20251026221140.js
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
const { DataTypes } = require('sequelize');
|
||||||
|
const { sequelize } = require('../config/database');
|
||||||
|
|
||||||
|
const Service = sequelize.define('Service', {
|
||||||
|
id: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
defaultValue: DataTypes.UUIDV4,
|
||||||
|
primaryKey: true
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: false,
|
||||||
|
validate: {
|
||||||
|
len: [1, 255]
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
this.setDataValue('name', value.trim());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
type: DataTypes.TEXT,
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
shortDescription: {
|
||||||
|
type: DataTypes.STRING(150),
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
icon: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
category: {
|
||||||
|
type: DataTypes.ENUM(
|
||||||
|
'web-development',
|
||||||
|
'mobile-development',
|
||||||
|
'ui-ux-design',
|
||||||
|
'consulting',
|
||||||
|
'support',
|
||||||
|
'other'
|
||||||
|
),
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
features: {
|
||||||
|
type: DataTypes.JSONB,
|
||||||
|
defaultValue: []
|
||||||
|
},
|
||||||
|
pricing: {
|
||||||
|
type: DataTypes.JSONB,
|
||||||
|
allowNull: false,
|
||||||
|
validate: {
|
||||||
|
isValidPricing(value) {
|
||||||
|
if (!value.basePrice || value.basePrice < 0) {
|
||||||
|
throw new Error('Base price must be a positive number');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
estimatedTime: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true
|
||||||
|
},
|
||||||
|
isActive: {
|
||||||
|
type: DataTypes.BOOLEAN,
|
||||||
|
defaultValue: true
|
||||||
|
},
|
||||||
|
featured: {
|
||||||
|
type: DataTypes.BOOLEAN,
|
||||||
|
defaultValue: false
|
||||||
|
},
|
||||||
|
order: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
defaultValue: 0
|
||||||
|
},
|
||||||
|
tags: {
|
||||||
|
type: DataTypes.JSONB,
|
||||||
|
defaultValue: []
|
||||||
|
},
|
||||||
|
seo: {
|
||||||
|
type: DataTypes.JSONB,
|
||||||
|
defaultValue: {}
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
tableName: 'services',
|
||||||
|
timestamps: true,
|
||||||
|
indexes: [
|
||||||
|
{
|
||||||
|
fields: ['category', 'featured', 'order']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'gin',
|
||||||
|
fields: ['tags']
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = Service;
|
||||||
90
.history/models/Service_20251026221146.js
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
const { DataTypes } = require('sequelize');
|
||||||
|
const { sequelize } = require('../config/database');
|
||||||
|
|
||||||
|
const Service = sequelize.define('Service', {
|
||||||
|
id: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
defaultValue: DataTypes.UUIDV4,
|
||||||
|
primaryKey: true
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: false,
|
||||||
|
validate: {
|
||||||
|
len: [1, 255]
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
this.setDataValue('name', value.trim());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
type: DataTypes.TEXT,
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
shortDescription: {
|
||||||
|
type: DataTypes.STRING(150),
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
icon: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
category: {
|
||||||
|
type: DataTypes.ENUM(
|
||||||
|
'web-development',
|
||||||
|
'mobile-development',
|
||||||
|
'ui-ux-design',
|
||||||
|
'consulting',
|
||||||
|
'support',
|
||||||
|
'other'
|
||||||
|
),
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
features: {
|
||||||
|
type: DataTypes.JSONB,
|
||||||
|
defaultValue: []
|
||||||
|
},
|
||||||
|
pricing: {
|
||||||
|
type: DataTypes.JSONB,
|
||||||
|
allowNull: true,
|
||||||
|
defaultValue: {}
|
||||||
|
},
|
||||||
|
estimatedTime: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true
|
||||||
|
},
|
||||||
|
isActive: {
|
||||||
|
type: DataTypes.BOOLEAN,
|
||||||
|
defaultValue: true
|
||||||
|
},
|
||||||
|
featured: {
|
||||||
|
type: DataTypes.BOOLEAN,
|
||||||
|
defaultValue: false
|
||||||
|
},
|
||||||
|
order: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
defaultValue: 0
|
||||||
|
},
|
||||||
|
tags: {
|
||||||
|
type: DataTypes.JSONB,
|
||||||
|
defaultValue: []
|
||||||
|
},
|
||||||
|
seo: {
|
||||||
|
type: DataTypes.JSONB,
|
||||||
|
defaultValue: {}
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
tableName: 'services',
|
||||||
|
timestamps: true,
|
||||||
|
indexes: [
|
||||||
|
{
|
||||||
|
fields: ['category', 'featured', 'order']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'gin',
|
||||||
|
fields: ['tags']
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = Service;
|
||||||
90
.history/models/Service_20251026221152.js
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
const { DataTypes } = require('sequelize');
|
||||||
|
const { sequelize } = require('../config/database');
|
||||||
|
|
||||||
|
const Service = sequelize.define('Service', {
|
||||||
|
id: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
defaultValue: DataTypes.UUIDV4,
|
||||||
|
primaryKey: true
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: false,
|
||||||
|
validate: {
|
||||||
|
len: [1, 255]
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
this.setDataValue('name', value.trim());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
type: DataTypes.TEXT,
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
shortDescription: {
|
||||||
|
type: DataTypes.STRING(500),
|
||||||
|
allowNull: true
|
||||||
|
},
|
||||||
|
icon: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
category: {
|
||||||
|
type: DataTypes.ENUM(
|
||||||
|
'web-development',
|
||||||
|
'mobile-development',
|
||||||
|
'ui-ux-design',
|
||||||
|
'consulting',
|
||||||
|
'support',
|
||||||
|
'other'
|
||||||
|
),
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
features: {
|
||||||
|
type: DataTypes.JSONB,
|
||||||
|
defaultValue: []
|
||||||
|
},
|
||||||
|
pricing: {
|
||||||
|
type: DataTypes.JSONB,
|
||||||
|
allowNull: true,
|
||||||
|
defaultValue: {}
|
||||||
|
},
|
||||||
|
estimatedTime: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true
|
||||||
|
},
|
||||||
|
isActive: {
|
||||||
|
type: DataTypes.BOOLEAN,
|
||||||
|
defaultValue: true
|
||||||
|
},
|
||||||
|
featured: {
|
||||||
|
type: DataTypes.BOOLEAN,
|
||||||
|
defaultValue: false
|
||||||
|
},
|
||||||
|
order: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
defaultValue: 0
|
||||||
|
},
|
||||||
|
tags: {
|
||||||
|
type: DataTypes.JSONB,
|
||||||
|
defaultValue: []
|
||||||
|
},
|
||||||
|
seo: {
|
||||||
|
type: DataTypes.JSONB,
|
||||||
|
defaultValue: {}
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
tableName: 'services',
|
||||||
|
timestamps: true,
|
||||||
|
indexes: [
|
||||||
|
{
|
||||||
|
fields: ['category', 'featured', 'order']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'gin',
|
||||||
|
fields: ['tags']
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = Service;
|
||||||
90
.history/models/Service_20251026221158.js
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
const { DataTypes } = require('sequelize');
|
||||||
|
const { sequelize } = require('../config/database');
|
||||||
|
|
||||||
|
const Service = sequelize.define('Service', {
|
||||||
|
id: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
defaultValue: DataTypes.UUIDV4,
|
||||||
|
primaryKey: true
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: false,
|
||||||
|
validate: {
|
||||||
|
len: [1, 255]
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
this.setDataValue('name', value.trim());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
type: DataTypes.TEXT,
|
||||||
|
allowNull: true
|
||||||
|
},
|
||||||
|
shortDescription: {
|
||||||
|
type: DataTypes.STRING(500),
|
||||||
|
allowNull: true
|
||||||
|
},
|
||||||
|
icon: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
category: {
|
||||||
|
type: DataTypes.ENUM(
|
||||||
|
'web-development',
|
||||||
|
'mobile-development',
|
||||||
|
'ui-ux-design',
|
||||||
|
'consulting',
|
||||||
|
'support',
|
||||||
|
'other'
|
||||||
|
),
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
features: {
|
||||||
|
type: DataTypes.JSONB,
|
||||||
|
defaultValue: []
|
||||||
|
},
|
||||||
|
pricing: {
|
||||||
|
type: DataTypes.JSONB,
|
||||||
|
allowNull: true,
|
||||||
|
defaultValue: {}
|
||||||
|
},
|
||||||
|
estimatedTime: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true
|
||||||
|
},
|
||||||
|
isActive: {
|
||||||
|
type: DataTypes.BOOLEAN,
|
||||||
|
defaultValue: true
|
||||||
|
},
|
||||||
|
featured: {
|
||||||
|
type: DataTypes.BOOLEAN,
|
||||||
|
defaultValue: false
|
||||||
|
},
|
||||||
|
order: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
defaultValue: 0
|
||||||
|
},
|
||||||
|
tags: {
|
||||||
|
type: DataTypes.JSONB,
|
||||||
|
defaultValue: []
|
||||||
|
},
|
||||||
|
seo: {
|
||||||
|
type: DataTypes.JSONB,
|
||||||
|
defaultValue: {}
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
tableName: 'services',
|
||||||
|
timestamps: true,
|
||||||
|
indexes: [
|
||||||
|
{
|
||||||
|
fields: ['category', 'featured', 'order']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'gin',
|
||||||
|
fields: ['tags']
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = Service;
|
||||||
91
.history/models/Service_20251026221203.js
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
const { DataTypes } = require('sequelize');
|
||||||
|
const { sequelize } = require('../config/database');
|
||||||
|
|
||||||
|
const Service = sequelize.define('Service', {
|
||||||
|
id: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
defaultValue: DataTypes.UUIDV4,
|
||||||
|
primaryKey: true
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: false,
|
||||||
|
validate: {
|
||||||
|
len: [1, 255]
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
this.setDataValue('name', value.trim());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
type: DataTypes.TEXT,
|
||||||
|
allowNull: true
|
||||||
|
},
|
||||||
|
shortDescription: {
|
||||||
|
type: DataTypes.STRING(500),
|
||||||
|
allowNull: true
|
||||||
|
},
|
||||||
|
icon: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true,
|
||||||
|
defaultValue: 'fas fa-cog'
|
||||||
|
},
|
||||||
|
category: {
|
||||||
|
type: DataTypes.ENUM(
|
||||||
|
'web-development',
|
||||||
|
'mobile-development',
|
||||||
|
'ui-ux-design',
|
||||||
|
'consulting',
|
||||||
|
'support',
|
||||||
|
'other'
|
||||||
|
),
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
features: {
|
||||||
|
type: DataTypes.JSONB,
|
||||||
|
defaultValue: []
|
||||||
|
},
|
||||||
|
pricing: {
|
||||||
|
type: DataTypes.JSONB,
|
||||||
|
allowNull: true,
|
||||||
|
defaultValue: {}
|
||||||
|
},
|
||||||
|
estimatedTime: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true
|
||||||
|
},
|
||||||
|
isActive: {
|
||||||
|
type: DataTypes.BOOLEAN,
|
||||||
|
defaultValue: true
|
||||||
|
},
|
||||||
|
featured: {
|
||||||
|
type: DataTypes.BOOLEAN,
|
||||||
|
defaultValue: false
|
||||||
|
},
|
||||||
|
order: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
defaultValue: 0
|
||||||
|
},
|
||||||
|
tags: {
|
||||||
|
type: DataTypes.JSONB,
|
||||||
|
defaultValue: []
|
||||||
|
},
|
||||||
|
seo: {
|
||||||
|
type: DataTypes.JSONB,
|
||||||
|
defaultValue: {}
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
tableName: 'services',
|
||||||
|
timestamps: true,
|
||||||
|
indexes: [
|
||||||
|
{
|
||||||
|
fields: ['category', 'featured', 'order']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'gin',
|
||||||
|
fields: ['tags']
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = Service;
|
||||||
91
.history/models/Service_20251026221317.js
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
const { DataTypes } = require('sequelize');
|
||||||
|
const { sequelize } = require('../config/database');
|
||||||
|
|
||||||
|
const Service = sequelize.define('Service', {
|
||||||
|
id: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
defaultValue: DataTypes.UUIDV4,
|
||||||
|
primaryKey: true
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: false,
|
||||||
|
validate: {
|
||||||
|
len: [1, 255]
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
this.setDataValue('name', value.trim());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
type: DataTypes.TEXT,
|
||||||
|
allowNull: true
|
||||||
|
},
|
||||||
|
shortDescription: {
|
||||||
|
type: DataTypes.STRING(500),
|
||||||
|
allowNull: true
|
||||||
|
},
|
||||||
|
icon: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true,
|
||||||
|
defaultValue: 'fas fa-cog'
|
||||||
|
},
|
||||||
|
category: {
|
||||||
|
type: DataTypes.ENUM(
|
||||||
|
'web-development',
|
||||||
|
'mobile-development',
|
||||||
|
'ui-ux-design',
|
||||||
|
'consulting',
|
||||||
|
'support',
|
||||||
|
'other'
|
||||||
|
),
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
features: {
|
||||||
|
type: DataTypes.JSONB,
|
||||||
|
defaultValue: []
|
||||||
|
},
|
||||||
|
pricing: {
|
||||||
|
type: DataTypes.JSONB,
|
||||||
|
allowNull: true,
|
||||||
|
defaultValue: {}
|
||||||
|
},
|
||||||
|
estimatedTime: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true
|
||||||
|
},
|
||||||
|
isActive: {
|
||||||
|
type: DataTypes.BOOLEAN,
|
||||||
|
defaultValue: true
|
||||||
|
},
|
||||||
|
featured: {
|
||||||
|
type: DataTypes.BOOLEAN,
|
||||||
|
defaultValue: false
|
||||||
|
},
|
||||||
|
order: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
defaultValue: 0
|
||||||
|
},
|
||||||
|
tags: {
|
||||||
|
type: DataTypes.JSONB,
|
||||||
|
defaultValue: []
|
||||||
|
},
|
||||||
|
seo: {
|
||||||
|
type: DataTypes.JSONB,
|
||||||
|
defaultValue: {}
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
tableName: 'services',
|
||||||
|
timestamps: true,
|
||||||
|
indexes: [
|
||||||
|
{
|
||||||
|
fields: ['category', 'featured', 'order']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'gin',
|
||||||
|
fields: ['tags']
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = Service;
|
||||||
203
.history/public/css/enhanced-animations_20251026210710.css
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
/* Enhanced Animations for SmartSolTech */
|
||||||
|
|
||||||
|
/* Gradient Animation */
|
||||||
|
@keyframes gradient-x {
|
||||||
|
0%, 100% {
|
||||||
|
background-position: 0% 50%;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
background-position: 100% 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-gradient-x {
|
||||||
|
background-size: 200% 200%;
|
||||||
|
animation: gradient-x 4s ease infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Enhanced Blob Animation */
|
||||||
|
@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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animation-delay-6000 {
|
||||||
|
animation-delay: 6s;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Pulse Animation with Delay */
|
||||||
|
@keyframes pulse-delayed {
|
||||||
|
0%, 100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-pulse-delayed {
|
||||||
|
animation: pulse-delayed 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Floating Animation */
|
||||||
|
@keyframes float {
|
||||||
|
0%, 100% {
|
||||||
|
transform: translateY(0px);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: translateY(-10px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-float {
|
||||||
|
animation: float 3s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Card Hover Effects */
|
||||||
|
.card-hover {
|
||||||
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-hover:hover {
|
||||||
|
transform: translateY(-8px);
|
||||||
|
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Background Pattern */
|
||||||
|
.bg-pattern {
|
||||||
|
background-image: radial-gradient(circle at 1px 1px, rgba(59, 130, 246, 0.15) 1px, transparent 0);
|
||||||
|
background-size: 20px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Glowing Effect */
|
||||||
|
.glow {
|
||||||
|
box-shadow: 0 0 20px rgba(59, 130, 246, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.glow-purple {
|
||||||
|
box-shadow: 0 0 20px rgba(139, 92, 246, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.glow-green {
|
||||||
|
box-shadow: 0 0 20px rgba(16, 185, 129, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.glow-orange {
|
||||||
|
box-shadow: 0 0 20px rgba(251, 146, 60, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hero Section Specific */
|
||||||
|
.hero-section {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-section::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background:
|
||||||
|
radial-gradient(1200px 600px at 10% -10%, rgba(99, 102, 241, 0.35), transparent 60%),
|
||||||
|
radial-gradient(1000px 500px at 110% 10%, rgba(168, 85, 247, 0.35), transparent 60%);
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Glass Effect */
|
||||||
|
.glass-effect {
|
||||||
|
backdrop-filter: blur(16px);
|
||||||
|
-webkit-backdrop-filter: blur(16px);
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Smooth Transitions */
|
||||||
|
.smooth-transition {
|
||||||
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Text Shimmer Effect */
|
||||||
|
@keyframes shimmer {
|
||||||
|
0% {
|
||||||
|
background-position: -200% center;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
background-position: 200% center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-shimmer {
|
||||||
|
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.8), transparent);
|
||||||
|
background-size: 200% 100%;
|
||||||
|
animation: shimmer 2s infinite;
|
||||||
|
background-clip: text;
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Button Hover Effects */
|
||||||
|
.btn-enhanced {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-enhanced::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: -100%;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
|
||||||
|
transition: left 0.5s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-enhanced:hover::before {
|
||||||
|
left: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive Adjustments */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.animate-blob {
|
||||||
|
animation-duration: 10s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-hover:hover {
|
||||||
|
transform: translateY(-4px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark Mode Adjustments */
|
||||||
|
.dark .glass-effect {
|
||||||
|
background: rgba(0, 0, 0, 0.1);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .bg-pattern {
|
||||||
|
background-image: radial-gradient(circle at 1px 1px, rgba(59, 130, 246, 0.1) 1px, transparent 0);
|
||||||
|
}
|
||||||
203
.history/public/css/enhanced-animations_20251026210722.css
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
/* Enhanced Animations for SmartSolTech */
|
||||||
|
|
||||||
|
/* Gradient Animation */
|
||||||
|
@keyframes gradient-x {
|
||||||
|
0%, 100% {
|
||||||
|
background-position: 0% 50%;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
background-position: 100% 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-gradient-x {
|
||||||
|
background-size: 200% 200%;
|
||||||
|
animation: gradient-x 4s ease infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Enhanced Blob Animation */
|
||||||
|
@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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animation-delay-6000 {
|
||||||
|
animation-delay: 6s;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Pulse Animation with Delay */
|
||||||
|
@keyframes pulse-delayed {
|
||||||
|
0%, 100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-pulse-delayed {
|
||||||
|
animation: pulse-delayed 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Floating Animation */
|
||||||
|
@keyframes float {
|
||||||
|
0%, 100% {
|
||||||
|
transform: translateY(0px);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: translateY(-10px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-float {
|
||||||
|
animation: float 3s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Card Hover Effects */
|
||||||
|
.card-hover {
|
||||||
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-hover:hover {
|
||||||
|
transform: translateY(-8px);
|
||||||
|
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Background Pattern */
|
||||||
|
.bg-pattern {
|
||||||
|
background-image: radial-gradient(circle at 1px 1px, rgba(59, 130, 246, 0.15) 1px, transparent 0);
|
||||||
|
background-size: 20px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Glowing Effect */
|
||||||
|
.glow {
|
||||||
|
box-shadow: 0 0 20px rgba(59, 130, 246, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.glow-purple {
|
||||||
|
box-shadow: 0 0 20px rgba(139, 92, 246, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.glow-green {
|
||||||
|
box-shadow: 0 0 20px rgba(16, 185, 129, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.glow-orange {
|
||||||
|
box-shadow: 0 0 20px rgba(251, 146, 60, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hero Section Specific */
|
||||||
|
.hero-section {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-section::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background:
|
||||||
|
radial-gradient(1200px 600px at 10% -10%, rgba(99, 102, 241, 0.35), transparent 60%),
|
||||||
|
radial-gradient(1000px 500px at 110% 10%, rgba(168, 85, 247, 0.35), transparent 60%);
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Glass Effect */
|
||||||
|
.glass-effect {
|
||||||
|
backdrop-filter: blur(16px);
|
||||||
|
-webkit-backdrop-filter: blur(16px);
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Smooth Transitions */
|
||||||
|
.smooth-transition {
|
||||||
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Text Shimmer Effect */
|
||||||
|
@keyframes shimmer {
|
||||||
|
0% {
|
||||||
|
background-position: -200% center;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
background-position: 200% center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-shimmer {
|
||||||
|
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.8), transparent);
|
||||||
|
background-size: 200% 100%;
|
||||||
|
animation: shimmer 2s infinite;
|
||||||
|
background-clip: text;
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Button Hover Effects */
|
||||||
|
.btn-enhanced {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-enhanced::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: -100%;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
|
||||||
|
transition: left 0.5s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-enhanced:hover::before {
|
||||||
|
left: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive Adjustments */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.animate-blob {
|
||||||
|
animation-duration: 10s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-hover:hover {
|
||||||
|
transform: translateY(-4px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark Mode Adjustments */
|
||||||
|
.dark .glass-effect {
|
||||||
|
background: rgba(0, 0, 0, 0.1);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .bg-pattern {
|
||||||
|
background-image: radial-gradient(circle at 1px 1px, rgba(59, 130, 246, 0.1) 1px, transparent 0);
|
||||||
|
}
|
||||||
55
.history/public/images/services/branding_20251026180829.svg
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200" fill="none">
|
||||||
|
<!-- Background circle -->
|
||||||
|
<circle cx="100" cy="100" r="90" fill="rgba(255,255,255,0.1)" stroke="rgba(255,255,255,0.2)" stroke-width="2"/>
|
||||||
|
|
||||||
|
<!-- Logo design canvas -->
|
||||||
|
<rect x="60" y="50" width="80" height="80" rx="8" fill="rgba(255,255,255,0.9)" stroke="rgba(255,255,255,0.3)" stroke-width="2"/>
|
||||||
|
|
||||||
|
<!-- Logo elements - abstract brand symbol -->
|
||||||
|
<circle cx="100" cy="90" r="20" fill="none" stroke="rgba(59,130,246,0.6)" stroke-width="3"/>
|
||||||
|
<path d="M85 90 Q100 75 115 90 Q100 105 85 90" fill="rgba(139,92,246,0.5)"/>
|
||||||
|
<circle cx="100" cy="90" r="8" fill="rgba(245,158,11,0.7)"/>
|
||||||
|
|
||||||
|
<!-- Brand typography -->
|
||||||
|
<rect x="70" y="115" width="60" height="8" rx="4" fill="rgba(59,130,246,0.3)"/>
|
||||||
|
|
||||||
|
<!-- Color swatches -->
|
||||||
|
<rect x="20" y="30" width="15" height="15" rx="3" fill="rgba(59,130,246,0.8)"/>
|
||||||
|
<rect x="20" y="50" width="15" height="15" rx="3" fill="rgba(139,92,246,0.8)"/>
|
||||||
|
<rect x="20" y="70" width="15" height="15" rx="3" fill="rgba(245,158,11,0.8)"/>
|
||||||
|
<rect x="20" y="90" width="15" height="15" rx="3" fill="rgba(34,197,94,0.8)"/>
|
||||||
|
<rect x="20" y="110" width="15" height="15" rx="3" fill="rgba(239,68,68,0.8)"/>
|
||||||
|
|
||||||
|
<!-- Business cards -->
|
||||||
|
<rect x="150" y="40" width="30" height="20" rx="3" fill="rgba(255,255,255,0.8)" stroke="rgba(255,255,255,0.4)" stroke-width="1"/>
|
||||||
|
<rect x="152" y="45" width="8" height="3" rx="1.5" fill="rgba(59,130,246,0.5)"/>
|
||||||
|
<rect x="152" y="50" width="12" height="2" rx="1" fill="rgba(139,92,246,0.3)"/>
|
||||||
|
<rect x="152" y="54" width="10" height="2" rx="1" fill="rgba(245,158,11,0.3)"/>
|
||||||
|
|
||||||
|
<!-- Brand guidelines -->
|
||||||
|
<rect x="25" y="140" width="40" height="30" rx="4" fill="rgba(255,255,255,0.7)" stroke="rgba(255,255,255,0.3)" stroke-width="1"/>
|
||||||
|
<rect x="30" y="150" width="30" height="3" rx="1.5" fill="rgba(59,130,246,0.4)"/>
|
||||||
|
<rect x="30" y="157" width="20" height="2" rx="1" fill="rgba(139,92,246,0.3)"/>
|
||||||
|
<rect x="30" y="162" width="25" height="2" rx="1" fill="rgba(245,158,11,0.3)"/>
|
||||||
|
|
||||||
|
<!-- Letterhead -->
|
||||||
|
<rect x="140" y="140" width="35" height="45" rx="4" fill="rgba(255,255,255,0.8)" stroke="rgba(255,255,255,0.4)" stroke-width="1"/>
|
||||||
|
<circle cx="157" cy="155" r="6" fill="rgba(59,130,246,0.3)" stroke="rgba(59,130,246,0.5)" stroke-width="1"/>
|
||||||
|
<rect x="145" y="165" width="20" height="2" rx="1" fill="rgba(139,92,246,0.3)"/>
|
||||||
|
<rect x="145" y="170" width="15" height="2" rx="1" fill="rgba(245,158,11,0.3)"/>
|
||||||
|
<rect x="145" y="175" width="18" height="2" rx="1" fill="rgba(34,197,94,0.3)"/>
|
||||||
|
|
||||||
|
<!-- Pencil/Design tool -->
|
||||||
|
<rect x="160" y="15" width="4" height="25" rx="2" fill="rgba(245,158,11,0.7)"/>
|
||||||
|
<path d="M160 40 L164 40 L162 45 Z" fill="rgba(139,92,246,0.6)"/>
|
||||||
|
<circle cx="162" cy="12" r="3" fill="rgba(236,72,153,0.6)"/>
|
||||||
|
|
||||||
|
<!-- Typography samples -->
|
||||||
|
<text x="75" y="180" fill="rgba(255,255,255,0.5)" font-family="serif" font-size="12" font-weight="bold">Aa</text>
|
||||||
|
<text x="95" y="180" fill="rgba(255,255,255,0.4)" font-family="sans-serif" font-size="10">Brand</text>
|
||||||
|
<text x="125" y="180" fill="rgba(255,255,255,0.3)" font-family="monospace" font-size="8">123</text>
|
||||||
|
|
||||||
|
<!-- Copyright symbol -->
|
||||||
|
<circle cx="175" cy="100" r="8" fill="none" stroke="rgba(255,255,255,0.4)" stroke-width="2"/>
|
||||||
|
<text x="171" y="105" fill="rgba(255,255,255,0.4)" font-family="serif" font-size="10" font-weight="bold">©</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 3.3 KiB |
55
.history/public/images/services/branding_20251026181004.svg
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200" fill="none">
|
||||||
|
<!-- Background circle -->
|
||||||
|
<circle cx="100" cy="100" r="90" fill="rgba(255,255,255,0.1)" stroke="rgba(255,255,255,0.2)" stroke-width="2"/>
|
||||||
|
|
||||||
|
<!-- Logo design canvas -->
|
||||||
|
<rect x="60" y="50" width="80" height="80" rx="8" fill="rgba(255,255,255,0.9)" stroke="rgba(255,255,255,0.3)" stroke-width="2"/>
|
||||||
|
|
||||||
|
<!-- Logo elements - abstract brand symbol -->
|
||||||
|
<circle cx="100" cy="90" r="20" fill="none" stroke="rgba(59,130,246,0.6)" stroke-width="3"/>
|
||||||
|
<path d="M85 90 Q100 75 115 90 Q100 105 85 90" fill="rgba(139,92,246,0.5)"/>
|
||||||
|
<circle cx="100" cy="90" r="8" fill="rgba(245,158,11,0.7)"/>
|
||||||
|
|
||||||
|
<!-- Brand typography -->
|
||||||
|
<rect x="70" y="115" width="60" height="8" rx="4" fill="rgba(59,130,246,0.3)"/>
|
||||||
|
|
||||||
|
<!-- Color swatches -->
|
||||||
|
<rect x="20" y="30" width="15" height="15" rx="3" fill="rgba(59,130,246,0.8)"/>
|
||||||
|
<rect x="20" y="50" width="15" height="15" rx="3" fill="rgba(139,92,246,0.8)"/>
|
||||||
|
<rect x="20" y="70" width="15" height="15" rx="3" fill="rgba(245,158,11,0.8)"/>
|
||||||
|
<rect x="20" y="90" width="15" height="15" rx="3" fill="rgba(34,197,94,0.8)"/>
|
||||||
|
<rect x="20" y="110" width="15" height="15" rx="3" fill="rgba(239,68,68,0.8)"/>
|
||||||
|
|
||||||
|
<!-- Business cards -->
|
||||||
|
<rect x="150" y="40" width="30" height="20" rx="3" fill="rgba(255,255,255,0.8)" stroke="rgba(255,255,255,0.4)" stroke-width="1"/>
|
||||||
|
<rect x="152" y="45" width="8" height="3" rx="1.5" fill="rgba(59,130,246,0.5)"/>
|
||||||
|
<rect x="152" y="50" width="12" height="2" rx="1" fill="rgba(139,92,246,0.3)"/>
|
||||||
|
<rect x="152" y="54" width="10" height="2" rx="1" fill="rgba(245,158,11,0.3)"/>
|
||||||
|
|
||||||
|
<!-- Brand guidelines -->
|
||||||
|
<rect x="25" y="140" width="40" height="30" rx="4" fill="rgba(255,255,255,0.7)" stroke="rgba(255,255,255,0.3)" stroke-width="1"/>
|
||||||
|
<rect x="30" y="150" width="30" height="3" rx="1.5" fill="rgba(59,130,246,0.4)"/>
|
||||||
|
<rect x="30" y="157" width="20" height="2" rx="1" fill="rgba(139,92,246,0.3)"/>
|
||||||
|
<rect x="30" y="162" width="25" height="2" rx="1" fill="rgba(245,158,11,0.3)"/>
|
||||||
|
|
||||||
|
<!-- Letterhead -->
|
||||||
|
<rect x="140" y="140" width="35" height="45" rx="4" fill="rgba(255,255,255,0.8)" stroke="rgba(255,255,255,0.4)" stroke-width="1"/>
|
||||||
|
<circle cx="157" cy="155" r="6" fill="rgba(59,130,246,0.3)" stroke="rgba(59,130,246,0.5)" stroke-width="1"/>
|
||||||
|
<rect x="145" y="165" width="20" height="2" rx="1" fill="rgba(139,92,246,0.3)"/>
|
||||||
|
<rect x="145" y="170" width="15" height="2" rx="1" fill="rgba(245,158,11,0.3)"/>
|
||||||
|
<rect x="145" y="175" width="18" height="2" rx="1" fill="rgba(34,197,94,0.3)"/>
|
||||||
|
|
||||||
|
<!-- Pencil/Design tool -->
|
||||||
|
<rect x="160" y="15" width="4" height="25" rx="2" fill="rgba(245,158,11,0.7)"/>
|
||||||
|
<path d="M160 40 L164 40 L162 45 Z" fill="rgba(139,92,246,0.6)"/>
|
||||||
|
<circle cx="162" cy="12" r="3" fill="rgba(236,72,153,0.6)"/>
|
||||||
|
|
||||||
|
<!-- Typography samples -->
|
||||||
|
<text x="75" y="180" fill="rgba(255,255,255,0.5)" font-family="serif" font-size="12" font-weight="bold">Aa</text>
|
||||||
|
<text x="95" y="180" fill="rgba(255,255,255,0.4)" font-family="sans-serif" font-size="10">Brand</text>
|
||||||
|
<text x="125" y="180" fill="rgba(255,255,255,0.3)" font-family="monospace" font-size="8">123</text>
|
||||||
|
|
||||||
|
<!-- Copyright symbol -->
|
||||||
|
<circle cx="175" cy="100" r="8" fill="none" stroke="rgba(255,255,255,0.4)" stroke-width="2"/>
|
||||||
|
<text x="171" y="105" fill="rgba(255,255,255,0.4)" font-family="serif" font-size="10" font-weight="bold">©</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 3.3 KiB |
@@ -0,0 +1,69 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200" fill="none">
|
||||||
|
<!-- Background circle -->
|
||||||
|
<circle cx="100" cy="100" r="90" fill="rgba(255,255,255,0.1)" stroke="rgba(255,255,255,0.2)" stroke-width="2"/>
|
||||||
|
|
||||||
|
<!-- Light bulb -->
|
||||||
|
<circle cx="100" cy="85" r="25" fill="rgba(245,158,11,0.2)" stroke="rgba(245,158,11,0.6)" stroke-width="3"/>
|
||||||
|
<rect x="95" y="105" width="10" height="8" rx="2" fill="rgba(255,255,255,0.7)"/>
|
||||||
|
<rect x="93" y="113" width="14" height="4" rx="2" fill="rgba(255,255,255,0.8)"/>
|
||||||
|
|
||||||
|
<!-- Light rays -->
|
||||||
|
<path d="M75 60 L80 65" stroke="rgba(245,158,11,0.6)" stroke-width="2"/>
|
||||||
|
<path d="M125 60 L120 65" stroke="rgba(245,158,11,0.6)" stroke-width="2"/>
|
||||||
|
<path d="M60 85 L65 85" stroke="rgba(245,158,11,0.6)" stroke-width="2"/>
|
||||||
|
<path d="M140 85 L135 85" stroke="rgba(245,158,11,0.6)" stroke-width="2"/>
|
||||||
|
<path d="M75 110 L80 105" stroke="rgba(245,158,11,0.6)" stroke-width="2"/>
|
||||||
|
<path d="M125 110 L120 105" stroke="rgba(245,158,11,0.6)" stroke-width="2"/>
|
||||||
|
|
||||||
|
<!-- Filament inside bulb -->
|
||||||
|
<path d="M90 75 Q100 70 110 75 Q100 80 90 75" stroke="rgba(245,158,11,0.8)" stroke-width="2" fill="none"/>
|
||||||
|
<path d="M90 85 Q100 90 110 85 Q100 95 90 85" stroke="rgba(245,158,11,0.8)" stroke-width="2" fill="none"/>
|
||||||
|
|
||||||
|
<!-- Gears -->
|
||||||
|
<circle cx="50" cy="140" r="12" fill="none" stroke="rgba(255,255,255,0.5)" stroke-width="2"/>
|
||||||
|
<circle cx="50" cy="140" r="8" fill="rgba(59,130,246,0.3)"/>
|
||||||
|
<rect x="46" y="132" width="8" height="3" rx="1.5" fill="rgba(255,255,255,0.6)"/>
|
||||||
|
<rect x="46" y="145" width="8" height="3" rx="1.5" fill="rgba(255,255,255,0.6)"/>
|
||||||
|
<rect x="42" y="138" width="3" height="8" rx="1.5" fill="rgba(255,255,255,0.6)"/>
|
||||||
|
<rect x="55" y="138" width="3" height="8" rx="1.5" fill="rgba(255,255,255,0.6)"/>
|
||||||
|
|
||||||
|
<circle cx="150" cy="50" r="10" fill="none" stroke="rgba(255,255,255,0.4)" stroke-width="2"/>
|
||||||
|
<circle cx="150" cy="50" r="6" fill="rgba(34,197,94,0.3)"/>
|
||||||
|
<rect x="147" y="43" width="6" height="2" rx="1" fill="rgba(255,255,255,0.5)"/>
|
||||||
|
<rect x="147" y="55" width="6" height="2" rx="1" fill="rgba(255,255,255,0.5)"/>
|
||||||
|
<rect x="144" y="48" width="2" height="6" rx="1" fill="rgba(255,255,255,0.5)"/>
|
||||||
|
<rect x="154" y="48" width="2" height="6" rx="1" fill="rgba(255,255,255,0.5)"/>
|
||||||
|
|
||||||
|
<!-- Charts/Analytics -->
|
||||||
|
<rect x="130" y="130" width="50" height="30" rx="4" fill="rgba(255,255,255,0.8)"/>
|
||||||
|
<rect x="135" y="145" width="6" height="12" fill="rgba(59,130,246,0.6)"/>
|
||||||
|
<rect x="143" y="140" width="6" height="17" fill="rgba(34,197,94,0.6)"/>
|
||||||
|
<rect x="151" y="148" width="6" height="9" fill="rgba(245,158,11,0.6)"/>
|
||||||
|
<rect x="159" y="142" width="6" height="15" fill="rgba(139,92,246,0.6)"/>
|
||||||
|
<rect x="167" y="146" width="6" height="11" fill="rgba(236,72,153,0.6)"/>
|
||||||
|
|
||||||
|
<!-- Puzzle pieces -->
|
||||||
|
<path d="M20 40 L35 40 Q40 35 45 40 L60 40 L60 55 Q55 60 60 65 L60 80 L45 80 Q40 75 35 80 L20 80 Z"
|
||||||
|
fill="rgba(255,255,255,0.6)" stroke="rgba(255,255,255,0.4)" stroke-width="1"/>
|
||||||
|
<circle cx="40" cy="60" r="3" fill="rgba(59,130,246,0.5)"/>
|
||||||
|
|
||||||
|
<!-- Arrow pointing up (growth) -->
|
||||||
|
<path d="M170 140 L170 120" stroke="rgba(34,197,94,0.8)" stroke-width="3"/>
|
||||||
|
<path d="M165 125 L170 120 L175 125" stroke="rgba(34,197,94,0.8)" stroke-width="3" fill="none"/>
|
||||||
|
|
||||||
|
<!-- Document/Report -->
|
||||||
|
<rect x="25" y="160" width="20" height="25" rx="2" fill="rgba(255,255,255,0.8)"/>
|
||||||
|
<rect x="28" y="165" width="14" height="2" rx="1" fill="rgba(59,130,246,0.4)"/>
|
||||||
|
<rect x="28" y="170" width="10" height="1.5" rx="0.75" fill="rgba(139,92,246,0.3)"/>
|
||||||
|
<rect x="28" y="174" width="12" height="1.5" rx="0.75" fill="rgba(245,158,11,0.3)"/>
|
||||||
|
<rect x="28" y="178" width="8" height="1.5" rx="0.75" fill="rgba(34,197,94,0.3)"/>
|
||||||
|
|
||||||
|
<!-- Clock/Time -->
|
||||||
|
<circle cx="170" cy="170" r="8" fill="rgba(255,255,255,0.7)" stroke="rgba(255,255,255,0.4)" stroke-width="1"/>
|
||||||
|
<path d="M170 165 L170 170 L175 170" stroke="rgba(59,130,246,0.8)" stroke-width="2" fill="none"/>
|
||||||
|
|
||||||
|
<!-- Target/Goals -->
|
||||||
|
<circle cx="30" cy="30" r="8" fill="none" stroke="rgba(239,68,68,0.5)" stroke-width="2"/>
|
||||||
|
<circle cx="30" cy="30" r="5" fill="none" stroke="rgba(239,68,68,0.6)" stroke-width="1"/>
|
||||||
|
<circle cx="30" cy="30" r="2" fill="rgba(239,68,68,0.7)"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 4.1 KiB |
@@ -0,0 +1,69 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200" fill="none">
|
||||||
|
<!-- Background circle -->
|
||||||
|
<circle cx="100" cy="100" r="90" fill="rgba(255,255,255,0.1)" stroke="rgba(255,255,255,0.2)" stroke-width="2"/>
|
||||||
|
|
||||||
|
<!-- Light bulb -->
|
||||||
|
<circle cx="100" cy="85" r="25" fill="rgba(245,158,11,0.2)" stroke="rgba(245,158,11,0.6)" stroke-width="3"/>
|
||||||
|
<rect x="95" y="105" width="10" height="8" rx="2" fill="rgba(255,255,255,0.7)"/>
|
||||||
|
<rect x="93" y="113" width="14" height="4" rx="2" fill="rgba(255,255,255,0.8)"/>
|
||||||
|
|
||||||
|
<!-- Light rays -->
|
||||||
|
<path d="M75 60 L80 65" stroke="rgba(245,158,11,0.6)" stroke-width="2"/>
|
||||||
|
<path d="M125 60 L120 65" stroke="rgba(245,158,11,0.6)" stroke-width="2"/>
|
||||||
|
<path d="M60 85 L65 85" stroke="rgba(245,158,11,0.6)" stroke-width="2"/>
|
||||||
|
<path d="M140 85 L135 85" stroke="rgba(245,158,11,0.6)" stroke-width="2"/>
|
||||||
|
<path d="M75 110 L80 105" stroke="rgba(245,158,11,0.6)" stroke-width="2"/>
|
||||||
|
<path d="M125 110 L120 105" stroke="rgba(245,158,11,0.6)" stroke-width="2"/>
|
||||||
|
|
||||||
|
<!-- Filament inside bulb -->
|
||||||
|
<path d="M90 75 Q100 70 110 75 Q100 80 90 75" stroke="rgba(245,158,11,0.8)" stroke-width="2" fill="none"/>
|
||||||
|
<path d="M90 85 Q100 90 110 85 Q100 95 90 85" stroke="rgba(245,158,11,0.8)" stroke-width="2" fill="none"/>
|
||||||
|
|
||||||
|
<!-- Gears -->
|
||||||
|
<circle cx="50" cy="140" r="12" fill="none" stroke="rgba(255,255,255,0.5)" stroke-width="2"/>
|
||||||
|
<circle cx="50" cy="140" r="8" fill="rgba(59,130,246,0.3)"/>
|
||||||
|
<rect x="46" y="132" width="8" height="3" rx="1.5" fill="rgba(255,255,255,0.6)"/>
|
||||||
|
<rect x="46" y="145" width="8" height="3" rx="1.5" fill="rgba(255,255,255,0.6)"/>
|
||||||
|
<rect x="42" y="138" width="3" height="8" rx="1.5" fill="rgba(255,255,255,0.6)"/>
|
||||||
|
<rect x="55" y="138" width="3" height="8" rx="1.5" fill="rgba(255,255,255,0.6)"/>
|
||||||
|
|
||||||
|
<circle cx="150" cy="50" r="10" fill="none" stroke="rgba(255,255,255,0.4)" stroke-width="2"/>
|
||||||
|
<circle cx="150" cy="50" r="6" fill="rgba(34,197,94,0.3)"/>
|
||||||
|
<rect x="147" y="43" width="6" height="2" rx="1" fill="rgba(255,255,255,0.5)"/>
|
||||||
|
<rect x="147" y="55" width="6" height="2" rx="1" fill="rgba(255,255,255,0.5)"/>
|
||||||
|
<rect x="144" y="48" width="2" height="6" rx="1" fill="rgba(255,255,255,0.5)"/>
|
||||||
|
<rect x="154" y="48" width="2" height="6" rx="1" fill="rgba(255,255,255,0.5)"/>
|
||||||
|
|
||||||
|
<!-- Charts/Analytics -->
|
||||||
|
<rect x="130" y="130" width="50" height="30" rx="4" fill="rgba(255,255,255,0.8)"/>
|
||||||
|
<rect x="135" y="145" width="6" height="12" fill="rgba(59,130,246,0.6)"/>
|
||||||
|
<rect x="143" y="140" width="6" height="17" fill="rgba(34,197,94,0.6)"/>
|
||||||
|
<rect x="151" y="148" width="6" height="9" fill="rgba(245,158,11,0.6)"/>
|
||||||
|
<rect x="159" y="142" width="6" height="15" fill="rgba(139,92,246,0.6)"/>
|
||||||
|
<rect x="167" y="146" width="6" height="11" fill="rgba(236,72,153,0.6)"/>
|
||||||
|
|
||||||
|
<!-- Puzzle pieces -->
|
||||||
|
<path d="M20 40 L35 40 Q40 35 45 40 L60 40 L60 55 Q55 60 60 65 L60 80 L45 80 Q40 75 35 80 L20 80 Z"
|
||||||
|
fill="rgba(255,255,255,0.6)" stroke="rgba(255,255,255,0.4)" stroke-width="1"/>
|
||||||
|
<circle cx="40" cy="60" r="3" fill="rgba(59,130,246,0.5)"/>
|
||||||
|
|
||||||
|
<!-- Arrow pointing up (growth) -->
|
||||||
|
<path d="M170 140 L170 120" stroke="rgba(34,197,94,0.8)" stroke-width="3"/>
|
||||||
|
<path d="M165 125 L170 120 L175 125" stroke="rgba(34,197,94,0.8)" stroke-width="3" fill="none"/>
|
||||||
|
|
||||||
|
<!-- Document/Report -->
|
||||||
|
<rect x="25" y="160" width="20" height="25" rx="2" fill="rgba(255,255,255,0.8)"/>
|
||||||
|
<rect x="28" y="165" width="14" height="2" rx="1" fill="rgba(59,130,246,0.4)"/>
|
||||||
|
<rect x="28" y="170" width="10" height="1.5" rx="0.75" fill="rgba(139,92,246,0.3)"/>
|
||||||
|
<rect x="28" y="174" width="12" height="1.5" rx="0.75" fill="rgba(245,158,11,0.3)"/>
|
||||||
|
<rect x="28" y="178" width="8" height="1.5" rx="0.75" fill="rgba(34,197,94,0.3)"/>
|
||||||
|
|
||||||
|
<!-- Clock/Time -->
|
||||||
|
<circle cx="170" cy="170" r="8" fill="rgba(255,255,255,0.7)" stroke="rgba(255,255,255,0.4)" stroke-width="1"/>
|
||||||
|
<path d="M170 165 L170 170 L175 170" stroke="rgba(59,130,246,0.8)" stroke-width="2" fill="none"/>
|
||||||
|
|
||||||
|
<!-- Target/Goals -->
|
||||||
|
<circle cx="30" cy="30" r="8" fill="none" stroke="rgba(239,68,68,0.5)" stroke-width="2"/>
|
||||||
|
<circle cx="30" cy="30" r="5" fill="none" stroke="rgba(239,68,68,0.6)" stroke-width="1"/>
|
||||||
|
<circle cx="30" cy="30" r="2" fill="rgba(239,68,68,0.7)"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 4.1 KiB |
52
.history/public/images/services/default_20251026180915.svg
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200" fill="none">
|
||||||
|
<!-- Background circle -->
|
||||||
|
<circle cx="100" cy="100" r="90" fill="rgba(255,255,255,0.1)" stroke="rgba(255,255,255,0.2)" stroke-width="2"/>
|
||||||
|
|
||||||
|
<!-- Main gear -->
|
||||||
|
<circle cx="100" cy="100" r="30" fill="none" stroke="rgba(255,255,255,0.6)" stroke-width="4"/>
|
||||||
|
<circle cx="100" cy="100" r="20" fill="rgba(59,130,246,0.3)"/>
|
||||||
|
|
||||||
|
<!-- Gear teeth -->
|
||||||
|
<rect x="95" y="65" width="10" height="8" rx="2" fill="rgba(255,255,255,0.7)"/>
|
||||||
|
<rect x="95" y="127" width="10" height="8" rx="2" fill="rgba(255,255,255,0.7)"/>
|
||||||
|
<rect x="65" y="95" width="8" height="10" rx="2" fill="rgba(255,255,255,0.7)"/>
|
||||||
|
<rect x="127" y="95" width="8" height="10" rx="2" fill="rgba(255,255,255,0.7)"/>
|
||||||
|
|
||||||
|
<!-- Diagonal gear teeth -->
|
||||||
|
<rect x="80" y="75" width="8" height="8" rx="2" fill="rgba(255,255,255,0.6)" transform="rotate(45 84 79)"/>
|
||||||
|
<rect x="112" y="75" width="8" height="8" rx="2" fill="rgba(255,255,255,0.6)" transform="rotate(-45 116 79)"/>
|
||||||
|
<rect x="80" y="117" width="8" height="8" rx="2" fill="rgba(255,255,255,0.6)" transform="rotate(-45 84 121)"/>
|
||||||
|
<rect x="112" y="117" width="8" height="8" rx="2" fill="rgba(255,255,255,0.6)" transform="rotate(45 116 121)"/>
|
||||||
|
|
||||||
|
<!-- Center circle -->
|
||||||
|
<circle cx="100" cy="100" r="8" fill="rgba(255,255,255,0.8)"/>
|
||||||
|
|
||||||
|
<!-- Smaller gears -->
|
||||||
|
<circle cx="60" cy="60" r="15" fill="none" stroke="rgba(255,255,255,0.4)" stroke-width="2"/>
|
||||||
|
<circle cx="60" cy="60" r="10" fill="rgba(34,197,94,0.3)"/>
|
||||||
|
<rect x="57" y="48" width="6" height="4" rx="1" fill="rgba(255,255,255,0.5)"/>
|
||||||
|
<rect x="57" y="68" width="6" height="4" rx="1" fill="rgba(255,255,255,0.5)"/>
|
||||||
|
<rect x="48" y="57" width="4" height="6" rx="1" fill="rgba(255,255,255,0.5)"/>
|
||||||
|
<rect x="68" y="57" width="4" height="6" rx="1" fill="rgba(255,255,255,0.5)"/>
|
||||||
|
|
||||||
|
<circle cx="140" cy="140" r="15" fill="none" stroke="rgba(255,255,255,0.4)" stroke-width="2"/>
|
||||||
|
<circle cx="140" cy="140" r="10" fill="rgba(245,158,11,0.3)"/>
|
||||||
|
<rect x="137" y="128" width="6" height="4" rx="1" fill="rgba(255,255,255,0.5)"/>
|
||||||
|
<rect x="137" y="148" width="6" height="4" rx="1" fill="rgba(255,255,255,0.5)"/>
|
||||||
|
<rect x="128" y="137" width="4" height="6" rx="1" fill="rgba(255,255,255,0.5)"/>
|
||||||
|
<rect x="148" y="137" width="4" height="6" rx="1" fill="rgba(255,255,255,0.5)"/>
|
||||||
|
|
||||||
|
<!-- Connecting lines -->
|
||||||
|
<path d="M70 70 L90 90" stroke="rgba(255,255,255,0.3)" stroke-width="2" stroke-dasharray="4,4"/>
|
||||||
|
<path d="M130 130 L110 110" stroke="rgba(255,255,255,0.3)" stroke-width="2" stroke-dasharray="4,4"/>
|
||||||
|
|
||||||
|
<!-- Decorative elements -->
|
||||||
|
<circle cx="30" cy="170" r="4" fill="rgba(255,255,255,0.2)"/>
|
||||||
|
<circle cx="170" cy="30" r="4" fill="rgba(255,255,255,0.2)"/>
|
||||||
|
|
||||||
|
<!-- Tool icons -->
|
||||||
|
<rect x="20" y="25" width="15" height="3" rx="1.5" fill="rgba(255,255,255,0.4)"/>
|
||||||
|
<circle cx="22" cy="26.5" r="1.5" fill="rgba(255,255,255,0.6)"/>
|
||||||
|
|
||||||
|
<path d="M165 165 L175 175 M170 160 L180 170" stroke="rgba(255,255,255,0.4)" stroke-width="2"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.9 KiB |
52
.history/public/images/services/default_20251026181004.svg
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200" fill="none">
|
||||||
|
<!-- Background circle -->
|
||||||
|
<circle cx="100" cy="100" r="90" fill="rgba(255,255,255,0.1)" stroke="rgba(255,255,255,0.2)" stroke-width="2"/>
|
||||||
|
|
||||||
|
<!-- Main gear -->
|
||||||
|
<circle cx="100" cy="100" r="30" fill="none" stroke="rgba(255,255,255,0.6)" stroke-width="4"/>
|
||||||
|
<circle cx="100" cy="100" r="20" fill="rgba(59,130,246,0.3)"/>
|
||||||
|
|
||||||
|
<!-- Gear teeth -->
|
||||||
|
<rect x="95" y="65" width="10" height="8" rx="2" fill="rgba(255,255,255,0.7)"/>
|
||||||
|
<rect x="95" y="127" width="10" height="8" rx="2" fill="rgba(255,255,255,0.7)"/>
|
||||||
|
<rect x="65" y="95" width="8" height="10" rx="2" fill="rgba(255,255,255,0.7)"/>
|
||||||
|
<rect x="127" y="95" width="8" height="10" rx="2" fill="rgba(255,255,255,0.7)"/>
|
||||||
|
|
||||||
|
<!-- Diagonal gear teeth -->
|
||||||
|
<rect x="80" y="75" width="8" height="8" rx="2" fill="rgba(255,255,255,0.6)" transform="rotate(45 84 79)"/>
|
||||||
|
<rect x="112" y="75" width="8" height="8" rx="2" fill="rgba(255,255,255,0.6)" transform="rotate(-45 116 79)"/>
|
||||||
|
<rect x="80" y="117" width="8" height="8" rx="2" fill="rgba(255,255,255,0.6)" transform="rotate(-45 84 121)"/>
|
||||||
|
<rect x="112" y="117" width="8" height="8" rx="2" fill="rgba(255,255,255,0.6)" transform="rotate(45 116 121)"/>
|
||||||
|
|
||||||
|
<!-- Center circle -->
|
||||||
|
<circle cx="100" cy="100" r="8" fill="rgba(255,255,255,0.8)"/>
|
||||||
|
|
||||||
|
<!-- Smaller gears -->
|
||||||
|
<circle cx="60" cy="60" r="15" fill="none" stroke="rgba(255,255,255,0.4)" stroke-width="2"/>
|
||||||
|
<circle cx="60" cy="60" r="10" fill="rgba(34,197,94,0.3)"/>
|
||||||
|
<rect x="57" y="48" width="6" height="4" rx="1" fill="rgba(255,255,255,0.5)"/>
|
||||||
|
<rect x="57" y="68" width="6" height="4" rx="1" fill="rgba(255,255,255,0.5)"/>
|
||||||
|
<rect x="48" y="57" width="4" height="6" rx="1" fill="rgba(255,255,255,0.5)"/>
|
||||||
|
<rect x="68" y="57" width="4" height="6" rx="1" fill="rgba(255,255,255,0.5)"/>
|
||||||
|
|
||||||
|
<circle cx="140" cy="140" r="15" fill="none" stroke="rgba(255,255,255,0.4)" stroke-width="2"/>
|
||||||
|
<circle cx="140" cy="140" r="10" fill="rgba(245,158,11,0.3)"/>
|
||||||
|
<rect x="137" y="128" width="6" height="4" rx="1" fill="rgba(255,255,255,0.5)"/>
|
||||||
|
<rect x="137" y="148" width="6" height="4" rx="1" fill="rgba(255,255,255,0.5)"/>
|
||||||
|
<rect x="128" y="137" width="4" height="6" rx="1" fill="rgba(255,255,255,0.5)"/>
|
||||||
|
<rect x="148" y="137" width="4" height="6" rx="1" fill="rgba(255,255,255,0.5)"/>
|
||||||
|
|
||||||
|
<!-- Connecting lines -->
|
||||||
|
<path d="M70 70 L90 90" stroke="rgba(255,255,255,0.3)" stroke-width="2" stroke-dasharray="4,4"/>
|
||||||
|
<path d="M130 130 L110 110" stroke="rgba(255,255,255,0.3)" stroke-width="2" stroke-dasharray="4,4"/>
|
||||||
|
|
||||||
|
<!-- Decorative elements -->
|
||||||
|
<circle cx="30" cy="170" r="4" fill="rgba(255,255,255,0.2)"/>
|
||||||
|
<circle cx="170" cy="30" r="4" fill="rgba(255,255,255,0.2)"/>
|
||||||
|
|
||||||
|
<!-- Tool icons -->
|
||||||
|
<rect x="20" y="25" width="15" height="3" rx="1.5" fill="rgba(255,255,255,0.4)"/>
|
||||||
|
<circle cx="22" cy="26.5" r="1.5" fill="rgba(255,255,255,0.6)"/>
|
||||||
|
|
||||||
|
<path d="M165 165 L175 175 M170 160 L180 170" stroke="rgba(255,255,255,0.4)" stroke-width="2"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.9 KiB |
@@ -0,0 +1,56 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200" fill="none">
|
||||||
|
<!-- Background circle -->
|
||||||
|
<circle cx="100" cy="100" r="90" fill="rgba(255,255,255,0.1)" stroke="rgba(255,255,255,0.2)" stroke-width="2"/>
|
||||||
|
|
||||||
|
<!-- Chart/Graph -->
|
||||||
|
<rect x="40" y="60" width="120" height="80" rx="8" fill="rgba(255,255,255,0.9)"/>
|
||||||
|
|
||||||
|
<!-- Chart bars -->
|
||||||
|
<rect x="55" y="100" width="12" height="30" fill="rgba(34,197,94,0.7)"/>
|
||||||
|
<rect x="72" y="85" width="12" height="45" fill="rgba(59,130,246,0.7)"/>
|
||||||
|
<rect x="89" y="95" width="12" height="35" fill="rgba(245,158,11,0.7)"/>
|
||||||
|
<rect x="106" y="75" width="12" height="55" fill="rgba(239,68,68,0.7)"/>
|
||||||
|
<rect x="123" y="90" width="12" height="40" fill="rgba(139,92,246,0.7)"/>
|
||||||
|
|
||||||
|
<!-- Trend line -->
|
||||||
|
<path d="M55 110 L72 95 L89 105 L106 85 L123 100" stroke="rgba(236,72,153,0.8)" stroke-width="3" fill="none"/>
|
||||||
|
|
||||||
|
<!-- Chart dots -->
|
||||||
|
<circle cx="61" cy="110" r="3" fill="rgba(236,72,153,0.8)"/>
|
||||||
|
<circle cx="78" cy="95" r="3" fill="rgba(236,72,153,0.8)"/>
|
||||||
|
<circle cx="95" cy="105" r="3" fill="rgba(236,72,153,0.8)"/>
|
||||||
|
<circle cx="112" cy="85" r="3" fill="rgba(236,72,153,0.8)"/>
|
||||||
|
<circle cx="129" cy="100" r="3" fill="rgba(236,72,153,0.8)"/>
|
||||||
|
|
||||||
|
<!-- Megaphone/Speaker -->
|
||||||
|
<path d="M20 120 L35 115 L35 125 Z" fill="rgba(255,255,255,0.8)"/>
|
||||||
|
<rect x="35" y="117" width="15" height="6" rx="3" fill="rgba(255,255,255,0.8)"/>
|
||||||
|
<path d="M50 115 Q60 115 60 120 Q60 125 50 125" stroke="rgba(255,255,255,0.6)" stroke-width="2" fill="none"/>
|
||||||
|
|
||||||
|
<!-- Social media icons -->
|
||||||
|
<circle cx="170" cy="40" r="8" fill="rgba(59,130,246,0.7)"/>
|
||||||
|
<text x="166" y="45" fill="white" font-family="sans-serif" font-size="8" font-weight="bold">f</text>
|
||||||
|
|
||||||
|
<circle cx="170" cy="60" r="8" fill="rgba(29,161,242,0.7)"/>
|
||||||
|
<text x="167" y="65" fill="white" font-family="sans-serif" font-size="8" font-weight="bold">t</text>
|
||||||
|
|
||||||
|
<circle cx="170" cy="80" r="8" fill="rgba(225,48,108,0.7)"/>
|
||||||
|
<text x="167" y="85" fill="white" font-family="sans-serif" font-size="8" font-weight="bold">i</text>
|
||||||
|
|
||||||
|
<!-- Target/Bullseye -->
|
||||||
|
<circle cx="30" cy="170" r="12" fill="none" stroke="rgba(255,255,255,0.5)" stroke-width="2"/>
|
||||||
|
<circle cx="30" cy="170" r="8" fill="none" stroke="rgba(255,255,255,0.5)" stroke-width="2"/>
|
||||||
|
<circle cx="30" cy="170" r="4" fill="rgba(239,68,68,0.7)"/>
|
||||||
|
|
||||||
|
<!-- SEO elements -->
|
||||||
|
<text x="140" y="170" fill="rgba(255,255,255,0.5)" font-family="sans-serif" font-size="12" font-weight="bold">SEO</text>
|
||||||
|
|
||||||
|
<!-- Growth arrow -->
|
||||||
|
<path d="M140 110 L155 95" stroke="rgba(34,197,94,0.8)" stroke-width="3" fill="none"/>
|
||||||
|
<path d="M150 95 L155 95 L155 100" stroke="rgba(34,197,94,0.8)" stroke-width="3" fill="none"/>
|
||||||
|
|
||||||
|
<!-- Analytics symbols -->
|
||||||
|
<circle cx="60" cy="40" r="3" fill="rgba(255,255,255,0.4)"/>
|
||||||
|
<circle cx="75" cy="35" r="4" fill="rgba(255,255,255,0.3)"/>
|
||||||
|
<circle cx="90" cy="45" r="2" fill="rgba(255,255,255,0.4)"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.8 KiB |
@@ -0,0 +1,56 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200" fill="none">
|
||||||
|
<!-- Background circle -->
|
||||||
|
<circle cx="100" cy="100" r="90" fill="rgba(255,255,255,0.1)" stroke="rgba(255,255,255,0.2)" stroke-width="2"/>
|
||||||
|
|
||||||
|
<!-- Chart/Graph -->
|
||||||
|
<rect x="40" y="60" width="120" height="80" rx="8" fill="rgba(255,255,255,0.9)"/>
|
||||||
|
|
||||||
|
<!-- Chart bars -->
|
||||||
|
<rect x="55" y="100" width="12" height="30" fill="rgba(34,197,94,0.7)"/>
|
||||||
|
<rect x="72" y="85" width="12" height="45" fill="rgba(59,130,246,0.7)"/>
|
||||||
|
<rect x="89" y="95" width="12" height="35" fill="rgba(245,158,11,0.7)"/>
|
||||||
|
<rect x="106" y="75" width="12" height="55" fill="rgba(239,68,68,0.7)"/>
|
||||||
|
<rect x="123" y="90" width="12" height="40" fill="rgba(139,92,246,0.7)"/>
|
||||||
|
|
||||||
|
<!-- Trend line -->
|
||||||
|
<path d="M55 110 L72 95 L89 105 L106 85 L123 100" stroke="rgba(236,72,153,0.8)" stroke-width="3" fill="none"/>
|
||||||
|
|
||||||
|
<!-- Chart dots -->
|
||||||
|
<circle cx="61" cy="110" r="3" fill="rgba(236,72,153,0.8)"/>
|
||||||
|
<circle cx="78" cy="95" r="3" fill="rgba(236,72,153,0.8)"/>
|
||||||
|
<circle cx="95" cy="105" r="3" fill="rgba(236,72,153,0.8)"/>
|
||||||
|
<circle cx="112" cy="85" r="3" fill="rgba(236,72,153,0.8)"/>
|
||||||
|
<circle cx="129" cy="100" r="3" fill="rgba(236,72,153,0.8)"/>
|
||||||
|
|
||||||
|
<!-- Megaphone/Speaker -->
|
||||||
|
<path d="M20 120 L35 115 L35 125 Z" fill="rgba(255,255,255,0.8)"/>
|
||||||
|
<rect x="35" y="117" width="15" height="6" rx="3" fill="rgba(255,255,255,0.8)"/>
|
||||||
|
<path d="M50 115 Q60 115 60 120 Q60 125 50 125" stroke="rgba(255,255,255,0.6)" stroke-width="2" fill="none"/>
|
||||||
|
|
||||||
|
<!-- Social media icons -->
|
||||||
|
<circle cx="170" cy="40" r="8" fill="rgba(59,130,246,0.7)"/>
|
||||||
|
<text x="166" y="45" fill="white" font-family="sans-serif" font-size="8" font-weight="bold">f</text>
|
||||||
|
|
||||||
|
<circle cx="170" cy="60" r="8" fill="rgba(29,161,242,0.7)"/>
|
||||||
|
<text x="167" y="65" fill="white" font-family="sans-serif" font-size="8" font-weight="bold">t</text>
|
||||||
|
|
||||||
|
<circle cx="170" cy="80" r="8" fill="rgba(225,48,108,0.7)"/>
|
||||||
|
<text x="167" y="85" fill="white" font-family="sans-serif" font-size="8" font-weight="bold">i</text>
|
||||||
|
|
||||||
|
<!-- Target/Bullseye -->
|
||||||
|
<circle cx="30" cy="170" r="12" fill="none" stroke="rgba(255,255,255,0.5)" stroke-width="2"/>
|
||||||
|
<circle cx="30" cy="170" r="8" fill="none" stroke="rgba(255,255,255,0.5)" stroke-width="2"/>
|
||||||
|
<circle cx="30" cy="170" r="4" fill="rgba(239,68,68,0.7)"/>
|
||||||
|
|
||||||
|
<!-- SEO elements -->
|
||||||
|
<text x="140" y="170" fill="rgba(255,255,255,0.5)" font-family="sans-serif" font-size="12" font-weight="bold">SEO</text>
|
||||||
|
|
||||||
|
<!-- Growth arrow -->
|
||||||
|
<path d="M140 110 L155 95" stroke="rgba(34,197,94,0.8)" stroke-width="3" fill="none"/>
|
||||||
|
<path d="M150 95 L155 95 L155 100" stroke="rgba(34,197,94,0.8)" stroke-width="3" fill="none"/>
|
||||||
|
|
||||||
|
<!-- Analytics symbols -->
|
||||||
|
<circle cx="60" cy="40" r="3" fill="rgba(255,255,255,0.4)"/>
|
||||||
|
<circle cx="75" cy="35" r="4" fill="rgba(255,255,255,0.3)"/>
|
||||||
|
<circle cx="90" cy="45" r="2" fill="rgba(255,255,255,0.4)"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.8 KiB |
@@ -0,0 +1,43 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200" fill="none">
|
||||||
|
<!-- Background circle -->
|
||||||
|
<circle cx="100" cy="100" r="90" fill="rgba(255,255,255,0.1)" stroke="rgba(255,255,255,0.2)" stroke-width="2"/>
|
||||||
|
|
||||||
|
<!-- Main phone -->
|
||||||
|
<rect x="70" y="40" width="60" height="120" rx="12" fill="rgba(255,255,255,0.95)" stroke="rgba(255,255,255,0.3)" stroke-width="2"/>
|
||||||
|
|
||||||
|
<!-- Screen -->
|
||||||
|
<rect x="75" y="55" width="50" height="90" rx="6" fill="rgba(34,197,94,0.2)"/>
|
||||||
|
|
||||||
|
<!-- Status bar -->
|
||||||
|
<rect x="78" y="58" width="44" height="8" fill="rgba(34,197,94,0.4)"/>
|
||||||
|
|
||||||
|
<!-- App icons grid -->
|
||||||
|
<rect x="80" y="75" width="12" height="12" rx="3" fill="rgba(59,130,246,0.7)"/>
|
||||||
|
<rect x="95" y="75" width="12" height="12" rx="3" fill="rgba(239,68,68,0.7)"/>
|
||||||
|
<rect x="110" y="75" width="12" height="12" rx="3" fill="rgba(245,158,11,0.7)"/>
|
||||||
|
|
||||||
|
<rect x="80" y="92" width="12" height="12" rx="3" fill="rgba(139,92,246,0.7)"/>
|
||||||
|
<rect x="95" y="92" width="12" height="12" rx="3" fill="rgba(16,185,129,0.7)"/>
|
||||||
|
<rect x="110" y="92" width="12" height="12" rx="3" fill="rgba(236,72,153,0.7)"/>
|
||||||
|
|
||||||
|
<!-- Navigation bar -->
|
||||||
|
<circle cx="100" cy="130" r="8" fill="rgba(255,255,255,0.8)" stroke="rgba(34,197,94,0.3)" stroke-width="2"/>
|
||||||
|
|
||||||
|
<!-- Home button -->
|
||||||
|
<circle cx="100" cy="150" r="4" fill="rgba(255,255,255,0.6)"/>
|
||||||
|
|
||||||
|
<!-- Secondary phones -->
|
||||||
|
<rect x="35" y="80" width="25" height="45" rx="6" fill="rgba(255,255,255,0.7)" stroke="rgba(255,255,255,0.2)" stroke-width="1"/>
|
||||||
|
<rect x="38" y="88" width="19" height="25" fill="rgba(34,197,94,0.3)"/>
|
||||||
|
|
||||||
|
<rect x="140" y="90" width="25" height="45" rx="6" fill="rgba(255,255,255,0.7)" stroke="rgba(255,255,255,0.2)" stroke-width="1"/>
|
||||||
|
<rect x="143" y="98" width="19" height="25" fill="rgba(34,197,94,0.3)"/>
|
||||||
|
|
||||||
|
<!-- Decorative elements -->
|
||||||
|
<circle cx="50" cy="50" r="4" fill="rgba(255,255,255,0.1)" stroke="rgba(255,255,255,0.2)" stroke-width="1"/>
|
||||||
|
<circle cx="150" cy="160" r="6" fill="rgba(255,255,255,0.1)" stroke="rgba(255,255,255,0.2)" stroke-width="1"/>
|
||||||
|
|
||||||
|
<!-- WiFi and signal indicators -->
|
||||||
|
<path d="M25 25 L35 25 M30 20 L30 30 M27 23 L33 23" stroke="rgba(255,255,255,0.3)" stroke-width="2" fill="none"/>
|
||||||
|
<path d="M165 25 Q175 25 175 35 Q175 45 165 45" stroke="rgba(255,255,255,0.3)" stroke-width="2" fill="none"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.3 KiB |
@@ -0,0 +1,43 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200" fill="none">
|
||||||
|
<!-- Background circle -->
|
||||||
|
<circle cx="100" cy="100" r="90" fill="rgba(255,255,255,0.1)" stroke="rgba(255,255,255,0.2)" stroke-width="2"/>
|
||||||
|
|
||||||
|
<!-- Main phone -->
|
||||||
|
<rect x="70" y="40" width="60" height="120" rx="12" fill="rgba(255,255,255,0.95)" stroke="rgba(255,255,255,0.3)" stroke-width="2"/>
|
||||||
|
|
||||||
|
<!-- Screen -->
|
||||||
|
<rect x="75" y="55" width="50" height="90" rx="6" fill="rgba(34,197,94,0.2)"/>
|
||||||
|
|
||||||
|
<!-- Status bar -->
|
||||||
|
<rect x="78" y="58" width="44" height="8" fill="rgba(34,197,94,0.4)"/>
|
||||||
|
|
||||||
|
<!-- App icons grid -->
|
||||||
|
<rect x="80" y="75" width="12" height="12" rx="3" fill="rgba(59,130,246,0.7)"/>
|
||||||
|
<rect x="95" y="75" width="12" height="12" rx="3" fill="rgba(239,68,68,0.7)"/>
|
||||||
|
<rect x="110" y="75" width="12" height="12" rx="3" fill="rgba(245,158,11,0.7)"/>
|
||||||
|
|
||||||
|
<rect x="80" y="92" width="12" height="12" rx="3" fill="rgba(139,92,246,0.7)"/>
|
||||||
|
<rect x="95" y="92" width="12" height="12" rx="3" fill="rgba(16,185,129,0.7)"/>
|
||||||
|
<rect x="110" y="92" width="12" height="12" rx="3" fill="rgba(236,72,153,0.7)"/>
|
||||||
|
|
||||||
|
<!-- Navigation bar -->
|
||||||
|
<circle cx="100" cy="130" r="8" fill="rgba(255,255,255,0.8)" stroke="rgba(34,197,94,0.3)" stroke-width="2"/>
|
||||||
|
|
||||||
|
<!-- Home button -->
|
||||||
|
<circle cx="100" cy="150" r="4" fill="rgba(255,255,255,0.6)"/>
|
||||||
|
|
||||||
|
<!-- Secondary phones -->
|
||||||
|
<rect x="35" y="80" width="25" height="45" rx="6" fill="rgba(255,255,255,0.7)" stroke="rgba(255,255,255,0.2)" stroke-width="1"/>
|
||||||
|
<rect x="38" y="88" width="19" height="25" fill="rgba(34,197,94,0.3)"/>
|
||||||
|
|
||||||
|
<rect x="140" y="90" width="25" height="45" rx="6" fill="rgba(255,255,255,0.7)" stroke="rgba(255,255,255,0.2)" stroke-width="1"/>
|
||||||
|
<rect x="143" y="98" width="19" height="25" fill="rgba(34,197,94,0.3)"/>
|
||||||
|
|
||||||
|
<!-- Decorative elements -->
|
||||||
|
<circle cx="50" cy="50" r="4" fill="rgba(255,255,255,0.1)" stroke="rgba(255,255,255,0.2)" stroke-width="1"/>
|
||||||
|
<circle cx="150" cy="160" r="6" fill="rgba(255,255,255,0.1)" stroke="rgba(255,255,255,0.2)" stroke-width="1"/>
|
||||||
|
|
||||||
|
<!-- WiFi and signal indicators -->
|
||||||
|
<path d="M25 25 L35 25 M30 20 L30 30 M27 23 L33 23" stroke="rgba(255,255,255,0.3)" stroke-width="2" fill="none"/>
|
||||||
|
<path d="M165 25 Q175 25 175 35 Q175 45 165 45" stroke="rgba(255,255,255,0.3)" stroke-width="2" fill="none"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.3 KiB |
@@ -0,0 +1,47 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200" fill="none">
|
||||||
|
<!-- Background circle -->
|
||||||
|
<circle cx="100" cy="100" r="90" fill="rgba(255,255,255,0.1)" stroke="rgba(255,255,255,0.2)" stroke-width="2"/>
|
||||||
|
|
||||||
|
<!-- Design canvas/artboard -->
|
||||||
|
<rect x="50" y="50" width="100" height="80" rx="8" fill="rgba(255,255,255,0.9)" stroke="rgba(255,255,255,0.3)" stroke-width="2"/>
|
||||||
|
|
||||||
|
<!-- Design elements - wireframe -->
|
||||||
|
<rect x="60" y="65" width="80" height="8" rx="4" fill="rgba(139,92,246,0.3)"/>
|
||||||
|
<rect x="60" y="80" width="50" height="6" rx="3" fill="rgba(236,72,153,0.4)"/>
|
||||||
|
<rect x="60" y="92" width="70" height="6" rx="3" fill="rgba(59,130,246,0.4)"/>
|
||||||
|
|
||||||
|
<!-- Button mockups -->
|
||||||
|
<rect x="60" y="105" width="25" height="12" rx="6" fill="rgba(34,197,94,0.6)"/>
|
||||||
|
<rect x="90" y="105" width="25" height="12" rx="6" fill="rgba(239,68,68,0.3)" stroke="rgba(239,68,68,0.5)" stroke-width="1"/>
|
||||||
|
|
||||||
|
<!-- Color palette -->
|
||||||
|
<circle cx="170" cy="60" r="8" fill="rgba(59,130,246,0.8)"/>
|
||||||
|
<circle cx="170" cy="80" r="8" fill="rgba(34,197,94,0.8)"/>
|
||||||
|
<circle cx="170" cy="100" r="8" fill="rgba(245,158,11,0.8)"/>
|
||||||
|
<circle cx="170" cy="120" r="8" fill="rgba(236,72,153,0.8)"/>
|
||||||
|
|
||||||
|
<!-- Design tools -->
|
||||||
|
<rect x="20" y="30" width="40" height="6" rx="3" fill="rgba(255,255,255,0.7)"/>
|
||||||
|
<circle cx="25" cy="33" r="2" fill="rgba(59,130,246,0.7)"/>
|
||||||
|
<circle cx="32" cy="33" r="2" fill="rgba(34,197,94,0.7)"/>
|
||||||
|
<circle cx="39" cy="33" r="2" fill="rgba(239,68,68,0.7)"/>
|
||||||
|
|
||||||
|
<!-- Cursor/pointer -->
|
||||||
|
<path d="M25 160 L35 170 L30 175 L20 170 Z" fill="rgba(255,255,255,0.8)" stroke="rgba(255,255,255,0.4)" stroke-width="1"/>
|
||||||
|
|
||||||
|
<!-- Grid lines -->
|
||||||
|
<defs>
|
||||||
|
<pattern id="grid" width="10" height="10" patternUnits="userSpaceOnUse">
|
||||||
|
<path d="M 10 0 L 0 0 0 10" fill="none" stroke="rgba(255,255,255,0.1)" stroke-width="0.5"/>
|
||||||
|
</pattern>
|
||||||
|
</defs>
|
||||||
|
<rect x="50" y="50" width="100" height="80" fill="url(#grid)"/>
|
||||||
|
|
||||||
|
<!-- Floating design elements -->
|
||||||
|
<rect x="160" y="140" width="20" height="12" rx="6" fill="rgba(255,255,255,0.5)" stroke="rgba(255,255,255,0.3)" stroke-width="1"/>
|
||||||
|
<circle cx="30" cy="140" r="6" fill="rgba(255,255,255,0.3)" stroke="rgba(255,255,255,0.2)" stroke-width="1"/>
|
||||||
|
|
||||||
|
<!-- Typography indicator -->
|
||||||
|
<text x="25" y="180" fill="rgba(255,255,255,0.4)" font-family="serif" font-size="16" font-weight="bold">Aa</text>
|
||||||
|
<text x="150" y="180" fill="rgba(255,255,255,0.4)" font-family="sans-serif" font-size="12">UI/UX</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.4 KiB |
@@ -0,0 +1,47 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200" fill="none">
|
||||||
|
<!-- Background circle -->
|
||||||
|
<circle cx="100" cy="100" r="90" fill="rgba(255,255,255,0.1)" stroke="rgba(255,255,255,0.2)" stroke-width="2"/>
|
||||||
|
|
||||||
|
<!-- Design canvas/artboard -->
|
||||||
|
<rect x="50" y="50" width="100" height="80" rx="8" fill="rgba(255,255,255,0.9)" stroke="rgba(255,255,255,0.3)" stroke-width="2"/>
|
||||||
|
|
||||||
|
<!-- Design elements - wireframe -->
|
||||||
|
<rect x="60" y="65" width="80" height="8" rx="4" fill="rgba(139,92,246,0.3)"/>
|
||||||
|
<rect x="60" y="80" width="50" height="6" rx="3" fill="rgba(236,72,153,0.4)"/>
|
||||||
|
<rect x="60" y="92" width="70" height="6" rx="3" fill="rgba(59,130,246,0.4)"/>
|
||||||
|
|
||||||
|
<!-- Button mockups -->
|
||||||
|
<rect x="60" y="105" width="25" height="12" rx="6" fill="rgba(34,197,94,0.6)"/>
|
||||||
|
<rect x="90" y="105" width="25" height="12" rx="6" fill="rgba(239,68,68,0.3)" stroke="rgba(239,68,68,0.5)" stroke-width="1"/>
|
||||||
|
|
||||||
|
<!-- Color palette -->
|
||||||
|
<circle cx="170" cy="60" r="8" fill="rgba(59,130,246,0.8)"/>
|
||||||
|
<circle cx="170" cy="80" r="8" fill="rgba(34,197,94,0.8)"/>
|
||||||
|
<circle cx="170" cy="100" r="8" fill="rgba(245,158,11,0.8)"/>
|
||||||
|
<circle cx="170" cy="120" r="8" fill="rgba(236,72,153,0.8)"/>
|
||||||
|
|
||||||
|
<!-- Design tools -->
|
||||||
|
<rect x="20" y="30" width="40" height="6" rx="3" fill="rgba(255,255,255,0.7)"/>
|
||||||
|
<circle cx="25" cy="33" r="2" fill="rgba(59,130,246,0.7)"/>
|
||||||
|
<circle cx="32" cy="33" r="2" fill="rgba(34,197,94,0.7)"/>
|
||||||
|
<circle cx="39" cy="33" r="2" fill="rgba(239,68,68,0.7)"/>
|
||||||
|
|
||||||
|
<!-- Cursor/pointer -->
|
||||||
|
<path d="M25 160 L35 170 L30 175 L20 170 Z" fill="rgba(255,255,255,0.8)" stroke="rgba(255,255,255,0.4)" stroke-width="1"/>
|
||||||
|
|
||||||
|
<!-- Grid lines -->
|
||||||
|
<defs>
|
||||||
|
<pattern id="grid" width="10" height="10" patternUnits="userSpaceOnUse">
|
||||||
|
<path d="M 10 0 L 0 0 0 10" fill="none" stroke="rgba(255,255,255,0.1)" stroke-width="0.5"/>
|
||||||
|
</pattern>
|
||||||
|
</defs>
|
||||||
|
<rect x="50" y="50" width="100" height="80" fill="url(#grid)"/>
|
||||||
|
|
||||||
|
<!-- Floating design elements -->
|
||||||
|
<rect x="160" y="140" width="20" height="12" rx="6" fill="rgba(255,255,255,0.5)" stroke="rgba(255,255,255,0.3)" stroke-width="1"/>
|
||||||
|
<circle cx="30" cy="140" r="6" fill="rgba(255,255,255,0.3)" stroke="rgba(255,255,255,0.2)" stroke-width="1"/>
|
||||||
|
|
||||||
|
<!-- Typography indicator -->
|
||||||
|
<text x="25" y="180" fill="rgba(255,255,255,0.4)" font-family="serif" font-size="16" font-weight="bold">Aa</text>
|
||||||
|
<text x="150" y="180" fill="rgba(255,255,255,0.4)" font-family="sans-serif" font-size="12">UI/UX</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.4 KiB |
@@ -0,0 +1,34 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200" fill="none">
|
||||||
|
<!-- Background circle -->
|
||||||
|
<circle cx="100" cy="100" r="90" fill="rgba(255,255,255,0.1)" stroke="rgba(255,255,255,0.2)" stroke-width="2"/>
|
||||||
|
|
||||||
|
<!-- Browser window -->
|
||||||
|
<rect x="40" y="60" width="120" height="80" rx="8" fill="rgba(255,255,255,0.9)"/>
|
||||||
|
|
||||||
|
<!-- Browser header -->
|
||||||
|
<rect x="40" y="60" width="120" height="20" rx="8" fill="rgba(59,130,246,0.8)"/>
|
||||||
|
|
||||||
|
<!-- Browser dots -->
|
||||||
|
<circle cx="50" cy="70" r="3" fill="rgba(239,68,68,0.8)"/>
|
||||||
|
<circle cx="60" cy="70" r="3" fill="rgba(245,158,11,0.8)"/>
|
||||||
|
<circle cx="70" cy="70" r="3" fill="rgba(34,197,94,0.8)"/>
|
||||||
|
|
||||||
|
<!-- Code lines -->
|
||||||
|
<rect x="50" y="90" width="60" height="3" rx="1.5" fill="rgba(59,130,246,0.6)"/>
|
||||||
|
<rect x="50" y="100" width="40" height="3" rx="1.5" fill="rgba(16,185,129,0.6)"/>
|
||||||
|
<rect x="50" y="110" width="80" height="3" rx="1.5" fill="rgba(139,92,246,0.6)"/>
|
||||||
|
<rect x="50" y="120" width="30" height="3" rx="1.5" fill="rgba(245,158,11,0.6)"/>
|
||||||
|
|
||||||
|
<!-- Mobile phone -->
|
||||||
|
<rect x="130" y="110" width="25" height="40" rx="4" fill="rgba(255,255,255,0.9)" stroke="rgba(255,255,255,0.3)" stroke-width="1"/>
|
||||||
|
<rect x="132" y="115" width="21" height="25" fill="rgba(59,130,246,0.3)"/>
|
||||||
|
<circle cx="142.5" cy="145" r="2" fill="rgba(255,255,255,0.7)"/>
|
||||||
|
|
||||||
|
<!-- Decorative elements -->
|
||||||
|
<circle cx="170" cy="80" r="8" fill="rgba(255,255,255,0.1)" stroke="rgba(255,255,255,0.2)" stroke-width="1"/>
|
||||||
|
<circle cx="30" cy="120" r="6" fill="rgba(255,255,255,0.1)" stroke="rgba(255,255,255,0.2)" stroke-width="1"/>
|
||||||
|
|
||||||
|
<!-- Code brackets -->
|
||||||
|
<text x="20" y="50" fill="rgba(255,255,255,0.4)" font-family="monospace" font-size="20" font-weight="bold"></></text>
|
||||||
|
<text x="160" y="170" fill="rgba(255,255,255,0.4)" font-family="monospace" font-size="16" font-weight="bold">{}</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.8 KiB |
@@ -0,0 +1,34 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200" fill="none">
|
||||||
|
<!-- Background circle -->
|
||||||
|
<circle cx="100" cy="100" r="90" fill="rgba(255,255,255,0.1)" stroke="rgba(255,255,255,0.2)" stroke-width="2"/>
|
||||||
|
|
||||||
|
<!-- Browser window -->
|
||||||
|
<rect x="40" y="60" width="120" height="80" rx="8" fill="rgba(255,255,255,0.9)"/>
|
||||||
|
|
||||||
|
<!-- Browser header -->
|
||||||
|
<rect x="40" y="60" width="120" height="20" rx="8" fill="rgba(59,130,246,0.8)"/>
|
||||||
|
|
||||||
|
<!-- Browser dots -->
|
||||||
|
<circle cx="50" cy="70" r="3" fill="rgba(239,68,68,0.8)"/>
|
||||||
|
<circle cx="60" cy="70" r="3" fill="rgba(245,158,11,0.8)"/>
|
||||||
|
<circle cx="70" cy="70" r="3" fill="rgba(34,197,94,0.8)"/>
|
||||||
|
|
||||||
|
<!-- Code lines -->
|
||||||
|
<rect x="50" y="90" width="60" height="3" rx="1.5" fill="rgba(59,130,246,0.6)"/>
|
||||||
|
<rect x="50" y="100" width="40" height="3" rx="1.5" fill="rgba(16,185,129,0.6)"/>
|
||||||
|
<rect x="50" y="110" width="80" height="3" rx="1.5" fill="rgba(139,92,246,0.6)"/>
|
||||||
|
<rect x="50" y="120" width="30" height="3" rx="1.5" fill="rgba(245,158,11,0.6)"/>
|
||||||
|
|
||||||
|
<!-- Mobile phone -->
|
||||||
|
<rect x="130" y="110" width="25" height="40" rx="4" fill="rgba(255,255,255,0.9)" stroke="rgba(255,255,255,0.3)" stroke-width="1"/>
|
||||||
|
<rect x="132" y="115" width="21" height="25" fill="rgba(59,130,246,0.3)"/>
|
||||||
|
<circle cx="142.5" cy="145" r="2" fill="rgba(255,255,255,0.7)"/>
|
||||||
|
|
||||||
|
<!-- Decorative elements -->
|
||||||
|
<circle cx="170" cy="80" r="8" fill="rgba(255,255,255,0.1)" stroke="rgba(255,255,255,0.2)" stroke-width="1"/>
|
||||||
|
<circle cx="30" cy="120" r="6" fill="rgba(255,255,255,0.1)" stroke="rgba(255,255,255,0.2)" stroke-width="1"/>
|
||||||
|
|
||||||
|
<!-- Code brackets -->
|
||||||
|
<text x="20" y="50" fill="rgba(255,255,255,0.4)" font-family="monospace" font-size="20" font-weight="bold"></></text>
|
||||||
|
<text x="160" y="170" fill="rgba(255,255,255,0.4)" font-family="monospace" font-size="16" font-weight="bold">{}</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.8 KiB |
414
.history/public/sw_20251026215851.js
Normal file
@@ -0,0 +1,414 @@
|
|||||||
|
// Service Worker for SmartSolTech PWA
|
||||||
|
const CACHE_NAME = 'smartsoltech-v1.0.2';
|
||||||
|
const STATIC_CACHE_NAME = 'smartsoltech-static-v1.0.2';
|
||||||
|
const DYNAMIC_CACHE_NAME = 'smartsoltech-dynamic-v1.0.2';
|
||||||
|
|
||||||
|
// Files to cache immediately
|
||||||
|
const STATIC_FILES = [
|
||||||
|
'/',
|
||||||
|
'/css/main.css',
|
||||||
|
'/css/fixes.css',
|
||||||
|
'/css/dark-theme.css',
|
||||||
|
'/js/main.js',
|
||||||
|
'/vendor/jquery/jquery-3.6.0.min.js',
|
||||||
|
'/vendor/bootstrap/bootstrap.bundle.min.js',
|
||||||
|
'/vendor/bootstrap/bootstrap.min.css',
|
||||||
|
'/vendor/adminlte/adminlte.min.js',
|
||||||
|
'/vendor/adminlte/adminlte.min.css',
|
||||||
|
'/vendor/adminlte/fontawesome.min.css',
|
||||||
|
'/images/logo.png',
|
||||||
|
'/images/icon-192x192.png',
|
||||||
|
'/images/icon-144x144.png',
|
||||||
|
'/manifest.json',
|
||||||
|
'https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap',
|
||||||
|
'https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@300;400;500;700&display=swap',
|
||||||
|
'https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css',
|
||||||
|
'https://unpkg.com/aos@2.3.1/dist/aos.css',
|
||||||
|
'https://unpkg.com/aos@2.3.1/dist/aos.js'
|
||||||
|
];
|
||||||
|
|
||||||
|
// Routes to cache dynamically
|
||||||
|
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) || url.pathname === '/') {
|
||||||
|
// For main pages, always check network first to ensure language consistency
|
||||||
|
event.respondWith(networkFirst(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) {
|
||||||
|
try {
|
||||||
|
const cache = await caches.open(DYNAMIC_CACHE_NAME);
|
||||||
|
const cachedResponse = await cache.match(request);
|
||||||
|
|
||||||
|
const fetchPromise = fetch(request).then(networkResponse => {
|
||||||
|
if (networkResponse && networkResponse.ok) {
|
||||||
|
cache.put(request, networkResponse.clone());
|
||||||
|
}
|
||||||
|
return networkResponse;
|
||||||
|
}).catch(error => {
|
||||||
|
console.log('staleWhileRevalidate fetch failed:', error);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
return cachedResponse || fetchPromise || new Response('Not available', { status: 503 });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('staleWhileRevalidate error:', error);
|
||||||
|
return new Response('Service unavailable', { status: 503 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper functions
|
||||||
|
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
|
||||||
|
});
|
||||||
414
.history/public/sw_20251026215934.js
Normal file
@@ -0,0 +1,414 @@
|
|||||||
|
// Service Worker for SmartSolTech PWA
|
||||||
|
const CACHE_NAME = 'smartsoltech-v1.0.2';
|
||||||
|
const STATIC_CACHE_NAME = 'smartsoltech-static-v1.0.2';
|
||||||
|
const DYNAMIC_CACHE_NAME = 'smartsoltech-dynamic-v1.0.2';
|
||||||
|
|
||||||
|
// Files to cache immediately
|
||||||
|
const STATIC_FILES = [
|
||||||
|
'/',
|
||||||
|
'/css/main.css',
|
||||||
|
'/css/fixes.css',
|
||||||
|
'/css/dark-theme.css',
|
||||||
|
'/js/main.js',
|
||||||
|
'/vendor/jquery/jquery-3.6.0.min.js',
|
||||||
|
'/vendor/bootstrap/bootstrap.bundle.min.js',
|
||||||
|
'/vendor/bootstrap/bootstrap.min.css',
|
||||||
|
'/vendor/adminlte/adminlte.min.js',
|
||||||
|
'/vendor/adminlte/adminlte.min.css',
|
||||||
|
'/vendor/adminlte/fontawesome.min.css',
|
||||||
|
'/images/logo.png',
|
||||||
|
'/images/icon-192x192.png',
|
||||||
|
'/images/icon-144x144.png',
|
||||||
|
'/manifest.json',
|
||||||
|
'https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap',
|
||||||
|
'https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@300;400;500;700&display=swap',
|
||||||
|
'https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css',
|
||||||
|
'https://unpkg.com/aos@2.3.1/dist/aos.css',
|
||||||
|
'https://unpkg.com/aos@2.3.1/dist/aos.js'
|
||||||
|
];
|
||||||
|
|
||||||
|
// Routes to cache dynamically
|
||||||
|
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) || url.pathname === '/') {
|
||||||
|
// For main pages, always check network first to ensure language consistency
|
||||||
|
event.respondWith(networkFirst(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) {
|
||||||
|
try {
|
||||||
|
const cache = await caches.open(DYNAMIC_CACHE_NAME);
|
||||||
|
const cachedResponse = await cache.match(request);
|
||||||
|
|
||||||
|
const fetchPromise = fetch(request).then(networkResponse => {
|
||||||
|
if (networkResponse && networkResponse.ok) {
|
||||||
|
cache.put(request, networkResponse.clone());
|
||||||
|
}
|
||||||
|
return networkResponse;
|
||||||
|
}).catch(error => {
|
||||||
|
console.log('staleWhileRevalidate fetch failed:', error);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
return cachedResponse || fetchPromise || new Response('Not available', { status: 503 });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('staleWhileRevalidate error:', error);
|
||||||
|
return new Response('Service unavailable', { status: 503 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper functions
|
||||||
|
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
|
||||||
|
});
|
||||||
1634
.history/routes/admin_20251026212605.js
Normal file
1634
.history/routes/admin_20251026212607.js
Normal file
1634
.history/routes/admin_20251026212617.js
Normal file
1634
.history/routes/admin_20251026212628.js
Normal file
1634
.history/routes/admin_20251026212638.js
Normal file
1634
.history/routes/admin_20251026212647.js
Normal file
1634
.history/routes/admin_20251026212658.js
Normal file
1634
.history/routes/admin_20251026212708.js
Normal file
1634
.history/routes/admin_20251026212718.js
Normal file
1634
.history/routes/admin_20251026212756.js
Normal file
1652
.history/routes/admin_20251026215040.js
Normal file
1652
.history/routes/admin_20251026215051.js
Normal file
1653
.history/routes/admin_20251026220319.js
Normal file
1654
.history/routes/admin_20251026220340.js
Normal file
1655
.history/routes/admin_20251026220351.js
Normal file
1656
.history/routes/admin_20251026220402.js
Normal file
1657
.history/routes/admin_20251026220414.js
Normal file
1658
.history/routes/admin_20251026220429.js
Normal file
1659
.history/routes/admin_20251026220439.js
Normal file
1659
.history/routes/admin_20251026220452.js
Normal file
496
.history/routes/api/admin_20251026221106.js
Normal file
@@ -0,0 +1,496 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
const multer = require('multer');
|
||||||
|
const path = require('path');
|
||||||
|
const { Portfolio, Service, Contact, User } = require('../../models');
|
||||||
|
|
||||||
|
// Multer configuration for file uploads
|
||||||
|
const storage = multer.diskStorage({
|
||||||
|
destination: (req, file, cb) => {
|
||||||
|
cb(null, 'public/uploads/');
|
||||||
|
},
|
||||||
|
filename: (req, file, cb) => {
|
||||||
|
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
|
||||||
|
cb(null, file.fieldname + '-' + uniqueSuffix + path.extname(file.originalname));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const upload = multer({
|
||||||
|
storage: storage,
|
||||||
|
fileFilter: (req, file, cb) => {
|
||||||
|
if (file.mimetype.startsWith('image/')) {
|
||||||
|
cb(null, true);
|
||||||
|
} else {
|
||||||
|
cb(new Error('Only image files are allowed!'), false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
limits: {
|
||||||
|
fileSize: 10 * 1024 * 1024 // 10MB
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Authentication middleware
|
||||||
|
const requireAuth = (req, res, next) => {
|
||||||
|
if (!req.session.user) {
|
||||||
|
return res.status(401).json({ success: false, message: 'Authentication required' });
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Portfolio API Routes
|
||||||
|
router.post('/portfolio', requireAuth, upload.array('images', 10), async (req, res) => {
|
||||||
|
try {
|
||||||
|
const {
|
||||||
|
title,
|
||||||
|
shortDescription,
|
||||||
|
description,
|
||||||
|
category,
|
||||||
|
clientName,
|
||||||
|
projectUrl,
|
||||||
|
githubUrl,
|
||||||
|
technologies,
|
||||||
|
featured,
|
||||||
|
isPublished
|
||||||
|
} = req.body;
|
||||||
|
|
||||||
|
// Process uploaded images
|
||||||
|
const images = req.files ? req.files.map((file, index) => ({
|
||||||
|
url: `/uploads/${file.filename}`,
|
||||||
|
alt: `${title} image ${index + 1}`,
|
||||||
|
isPrimary: index === 0
|
||||||
|
})) : [];
|
||||||
|
|
||||||
|
// Parse technologies
|
||||||
|
let techArray = [];
|
||||||
|
if (technologies) {
|
||||||
|
try {
|
||||||
|
techArray = JSON.parse(technologies);
|
||||||
|
} catch (e) {
|
||||||
|
techArray = technologies.split(',').map(t => t.trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const portfolio = await Portfolio.create({
|
||||||
|
title,
|
||||||
|
shortDescription,
|
||||||
|
description,
|
||||||
|
category,
|
||||||
|
clientName,
|
||||||
|
projectUrl: projectUrl || null,
|
||||||
|
githubUrl: githubUrl || null,
|
||||||
|
technologies: techArray,
|
||||||
|
images,
|
||||||
|
featured: featured === 'on',
|
||||||
|
isPublished: isPublished === 'on',
|
||||||
|
status: 'completed',
|
||||||
|
publishedAt: isPublished === 'on' ? new Date() : null
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json({ success: true, portfolio });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Portfolio creation error:', error);
|
||||||
|
res.status(500).json({ success: false, message: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.patch('/portfolio/:id', requireAuth, upload.array('images', 10), async (req, res) => {
|
||||||
|
try {
|
||||||
|
const portfolio = await Portfolio.findByPk(req.params.id);
|
||||||
|
if (!portfolio) {
|
||||||
|
return res.status(404).json({ success: false, message: 'Portfolio not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const updates = { ...req.body };
|
||||||
|
|
||||||
|
// Handle checkboxes
|
||||||
|
updates.featured = updates.featured === 'on';
|
||||||
|
updates.isPublished = updates.isPublished === 'on';
|
||||||
|
|
||||||
|
// Process technologies
|
||||||
|
if (updates.technologies) {
|
||||||
|
try {
|
||||||
|
updates.technologies = JSON.parse(updates.technologies);
|
||||||
|
} catch (e) {
|
||||||
|
updates.technologies = updates.technologies.split(',').map(t => t.trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process new images
|
||||||
|
if (req.files && req.files.length > 0) {
|
||||||
|
const newImages = req.files.map((file, index) => ({
|
||||||
|
url: `/uploads/${file.filename}`,
|
||||||
|
alt: `${updates.title || portfolio.title} image ${index + 1}`,
|
||||||
|
isPrimary: index === 0 && (!portfolio.images || portfolio.images.length === 0)
|
||||||
|
}));
|
||||||
|
|
||||||
|
updates.images = [...(portfolio.images || []), ...newImages];
|
||||||
|
}
|
||||||
|
|
||||||
|
await portfolio.update(updates);
|
||||||
|
res.json({ success: true, portfolio });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Portfolio update error:', error);
|
||||||
|
res.status(500).json({ success: false, message: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.delete('/portfolio/:id', requireAuth, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const portfolio = await Portfolio.findByPk(req.params.id);
|
||||||
|
if (!portfolio) {
|
||||||
|
return res.status(404).json({ success: false, message: 'Portfolio not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
await portfolio.destroy();
|
||||||
|
res.json({ success: true });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Portfolio deletion error:', error);
|
||||||
|
res.status(500).json({ success: false, message: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Services API Routes
|
||||||
|
router.post('/services', requireAuth, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const {
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
shortDescription,
|
||||||
|
icon,
|
||||||
|
category,
|
||||||
|
features,
|
||||||
|
pricing,
|
||||||
|
estimatedTime,
|
||||||
|
isActive,
|
||||||
|
featured,
|
||||||
|
tags
|
||||||
|
} = req.body;
|
||||||
|
|
||||||
|
// Parse arrays
|
||||||
|
let featuresArray = [];
|
||||||
|
let tagsArray = [];
|
||||||
|
let pricingObj = {};
|
||||||
|
|
||||||
|
if (features) {
|
||||||
|
try {
|
||||||
|
featuresArray = JSON.parse(features);
|
||||||
|
} catch (e) {
|
||||||
|
featuresArray = features.split(',').map(f => f.trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tags) {
|
||||||
|
try {
|
||||||
|
tagsArray = JSON.parse(tags);
|
||||||
|
} catch (e) {
|
||||||
|
tagsArray = tags.split(',').map(t => t.trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pricing) {
|
||||||
|
try {
|
||||||
|
pricingObj = JSON.parse(pricing);
|
||||||
|
} catch (e) {
|
||||||
|
pricingObj = { basePrice: pricing };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const service = await Service.create({
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
shortDescription,
|
||||||
|
icon,
|
||||||
|
category,
|
||||||
|
features: featuresArray,
|
||||||
|
pricing: pricingObj,
|
||||||
|
estimatedTime,
|
||||||
|
isActive: isActive === 'on',
|
||||||
|
featured: featured === 'on',
|
||||||
|
tags: tagsArray
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json({ success: true, service });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Service creation error:', error);
|
||||||
|
res.status(500).json({ success: false, message: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.put('/services/:id', requireAuth, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const service = await Service.findByPk(req.params.id);
|
||||||
|
if (!service) {
|
||||||
|
return res.status(404).json({ success: false, message: 'Service not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
shortDescription,
|
||||||
|
icon,
|
||||||
|
category,
|
||||||
|
features,
|
||||||
|
pricing,
|
||||||
|
estimatedTime,
|
||||||
|
isActive,
|
||||||
|
featured,
|
||||||
|
tags,
|
||||||
|
order,
|
||||||
|
seo
|
||||||
|
} = req.body;
|
||||||
|
|
||||||
|
// Parse arrays and objects
|
||||||
|
let featuresArray = [];
|
||||||
|
let tagsArray = [];
|
||||||
|
let pricingObj = {};
|
||||||
|
let seoObj = {};
|
||||||
|
|
||||||
|
if (features) {
|
||||||
|
if (Array.isArray(features)) {
|
||||||
|
featuresArray = features;
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
featuresArray = JSON.parse(features);
|
||||||
|
} catch (e) {
|
||||||
|
featuresArray = features.split(',').map(f => f.trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tags) {
|
||||||
|
if (Array.isArray(tags)) {
|
||||||
|
tagsArray = tags;
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
tagsArray = JSON.parse(tags);
|
||||||
|
} catch (e) {
|
||||||
|
tagsArray = tags.split(',').map(t => t.trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pricing) {
|
||||||
|
if (typeof pricing === 'object') {
|
||||||
|
pricingObj = pricing;
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
pricingObj = JSON.parse(pricing);
|
||||||
|
} catch (e) {
|
||||||
|
pricingObj = { basePrice: pricing };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (seo) {
|
||||||
|
if (typeof seo === 'object') {
|
||||||
|
seoObj = seo;
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
seoObj = JSON.parse(seo);
|
||||||
|
} catch (e) {
|
||||||
|
seoObj = {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateData = {
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
shortDescription,
|
||||||
|
icon,
|
||||||
|
category,
|
||||||
|
features: featuresArray,
|
||||||
|
pricing: pricingObj,
|
||||||
|
estimatedTime,
|
||||||
|
isActive: Boolean(isActive),
|
||||||
|
featured: Boolean(featured),
|
||||||
|
tags: tagsArray,
|
||||||
|
order: order ? parseInt(order) : 0,
|
||||||
|
seo: seoObj
|
||||||
|
};
|
||||||
|
|
||||||
|
// Remove undefined values
|
||||||
|
Object.keys(updateData).forEach(key => {
|
||||||
|
if (updateData[key] === undefined) {
|
||||||
|
delete updateData[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await service.update(updateData);
|
||||||
|
res.json({ success: true, service });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Service update error:', error);
|
||||||
|
res.status(500).json({ success: false, message: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.delete('/services/:id', requireAuth, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const service = await Service.findByPk(req.params.id);
|
||||||
|
if (!service) {
|
||||||
|
return res.status(404).json({ success: false, message: 'Service not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
await service.destroy();
|
||||||
|
res.json({ success: true });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Service deletion error:', error);
|
||||||
|
res.status(500).json({ success: false, message: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Contacts API Routes
|
||||||
|
router.patch('/contacts/:id', requireAuth, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const contact = await Contact.findByPk(req.params.id);
|
||||||
|
if (!contact) {
|
||||||
|
return res.status(404).json({ success: false, message: 'Contact not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
await contact.update(req.body);
|
||||||
|
res.json({ success: true, contact });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Contact update error:', error);
|
||||||
|
res.status(500).json({ success: false, message: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.delete('/contacts/:id', requireAuth, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const contact = await Contact.findByPk(req.params.id);
|
||||||
|
if (!contact) {
|
||||||
|
return res.status(404).json({ success: false, message: 'Contact not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
await contact.destroy();
|
||||||
|
res.json({ success: true });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Contact deletion error:', error);
|
||||||
|
res.status(500).json({ success: false, message: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Telegram notification for contact
|
||||||
|
router.post('/contacts/:id/telegram', requireAuth, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const contact = await Contact.findByPk(req.params.id);
|
||||||
|
if (!contact) {
|
||||||
|
return res.status(404).json({ success: false, message: 'Contact not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send Telegram notification
|
||||||
|
const telegramService = require('../../services/telegram');
|
||||||
|
const result = await telegramService.sendContactNotification(contact);
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
res.json({ success: true });
|
||||||
|
} else {
|
||||||
|
res.status(500).json({ success: false, message: result.message || result.error });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Telegram notification error:', error);
|
||||||
|
res.status(500).json({ success: false, message: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test Telegram connection
|
||||||
|
router.post('/telegram/test', requireAuth, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { botToken, chatId } = req.body;
|
||||||
|
|
||||||
|
// Temporarily set up telegram service with provided credentials
|
||||||
|
const axios = require('axios');
|
||||||
|
|
||||||
|
// Test bot info
|
||||||
|
const botResponse = await axios.get(`https://api.telegram.org/bot${botToken}/getMe`);
|
||||||
|
|
||||||
|
// Test sending a message
|
||||||
|
const testMessage = '✅ Telegram bot подключен успешно!\n\nЭто тестовое сообщение от SmartSolTech Admin Panel.';
|
||||||
|
await axios.post(`https://api.telegram.org/bot${botToken}/sendMessage`, {
|
||||||
|
chat_id: chatId,
|
||||||
|
text: testMessage,
|
||||||
|
parse_mode: 'Markdown'
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
bot: botResponse.data.result,
|
||||||
|
message: 'Test message sent successfully'
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Telegram test error:', error);
|
||||||
|
let message = 'Connection failed';
|
||||||
|
|
||||||
|
if (error.response?.data?.description) {
|
||||||
|
message = error.response.data.description;
|
||||||
|
} else if (error.message) {
|
||||||
|
message = error.message;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(400).json({ success: false, message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Settings API
|
||||||
|
const { SiteSettings } = require('../../models');
|
||||||
|
|
||||||
|
router.get('/settings', requireAuth, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const settings = await SiteSettings.findOne() || {};
|
||||||
|
res.json({ success: true, settings });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Settings fetch error:', error);
|
||||||
|
res.status(500).json({ success: false, message: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post('/settings', requireAuth, upload.fields([
|
||||||
|
{ name: 'logo', maxCount: 1 },
|
||||||
|
{ name: 'favicon', maxCount: 1 }
|
||||||
|
]), async (req, res) => {
|
||||||
|
try {
|
||||||
|
let settings = await SiteSettings.findOne();
|
||||||
|
if (!settings) {
|
||||||
|
settings = await SiteSettings.create({});
|
||||||
|
}
|
||||||
|
|
||||||
|
const updates = {};
|
||||||
|
|
||||||
|
// Handle nested objects
|
||||||
|
Object.keys(req.body).forEach(key => {
|
||||||
|
if (key.includes('.')) {
|
||||||
|
const [parent, child] = key.split('.');
|
||||||
|
if (!updates[parent]) updates[parent] = {};
|
||||||
|
updates[parent][child] = req.body[key];
|
||||||
|
} else {
|
||||||
|
updates[key] = req.body[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle file uploads
|
||||||
|
if (req.files.logo) {
|
||||||
|
updates.logo = `/uploads/${req.files.logo[0].filename}`;
|
||||||
|
}
|
||||||
|
if (req.files.favicon) {
|
||||||
|
updates.favicon = `/uploads/${req.files.favicon[0].filename}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update existing settings with new values
|
||||||
|
Object.keys(updates).forEach(key => {
|
||||||
|
if (typeof updates[key] === 'object' && updates[key] !== null) {
|
||||||
|
settings[key] = { ...settings[key], ...updates[key] };
|
||||||
|
} else {
|
||||||
|
settings[key] = updates[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await settings.save();
|
||||||
|
|
||||||
|
res.json({ success: true, settings });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Settings update error:', error);
|
||||||
|
res.status(500).json({ success: false, message: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
496
.history/routes/api/admin_20251026221118.js
Normal file
@@ -0,0 +1,496 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
const multer = require('multer');
|
||||||
|
const path = require('path');
|
||||||
|
const { Portfolio, Service, Contact, User } = require('../../models');
|
||||||
|
|
||||||
|
// Multer configuration for file uploads
|
||||||
|
const storage = multer.diskStorage({
|
||||||
|
destination: (req, file, cb) => {
|
||||||
|
cb(null, 'public/uploads/');
|
||||||
|
},
|
||||||
|
filename: (req, file, cb) => {
|
||||||
|
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
|
||||||
|
cb(null, file.fieldname + '-' + uniqueSuffix + path.extname(file.originalname));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const upload = multer({
|
||||||
|
storage: storage,
|
||||||
|
fileFilter: (req, file, cb) => {
|
||||||
|
if (file.mimetype.startsWith('image/')) {
|
||||||
|
cb(null, true);
|
||||||
|
} else {
|
||||||
|
cb(new Error('Only image files are allowed!'), false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
limits: {
|
||||||
|
fileSize: 10 * 1024 * 1024 // 10MB
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Authentication middleware
|
||||||
|
const requireAuth = (req, res, next) => {
|
||||||
|
if (!req.session.user) {
|
||||||
|
return res.status(401).json({ success: false, message: 'Authentication required' });
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Portfolio API Routes
|
||||||
|
router.post('/portfolio', requireAuth, upload.array('images', 10), async (req, res) => {
|
||||||
|
try {
|
||||||
|
const {
|
||||||
|
title,
|
||||||
|
shortDescription,
|
||||||
|
description,
|
||||||
|
category,
|
||||||
|
clientName,
|
||||||
|
projectUrl,
|
||||||
|
githubUrl,
|
||||||
|
technologies,
|
||||||
|
featured,
|
||||||
|
isPublished
|
||||||
|
} = req.body;
|
||||||
|
|
||||||
|
// Process uploaded images
|
||||||
|
const images = req.files ? req.files.map((file, index) => ({
|
||||||
|
url: `/uploads/${file.filename}`,
|
||||||
|
alt: `${title} image ${index + 1}`,
|
||||||
|
isPrimary: index === 0
|
||||||
|
})) : [];
|
||||||
|
|
||||||
|
// Parse technologies
|
||||||
|
let techArray = [];
|
||||||
|
if (technologies) {
|
||||||
|
try {
|
||||||
|
techArray = JSON.parse(technologies);
|
||||||
|
} catch (e) {
|
||||||
|
techArray = technologies.split(',').map(t => t.trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const portfolio = await Portfolio.create({
|
||||||
|
title,
|
||||||
|
shortDescription,
|
||||||
|
description,
|
||||||
|
category,
|
||||||
|
clientName,
|
||||||
|
projectUrl: projectUrl || null,
|
||||||
|
githubUrl: githubUrl || null,
|
||||||
|
technologies: techArray,
|
||||||
|
images,
|
||||||
|
featured: featured === 'on',
|
||||||
|
isPublished: isPublished === 'on',
|
||||||
|
status: 'completed',
|
||||||
|
publishedAt: isPublished === 'on' ? new Date() : null
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json({ success: true, portfolio });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Portfolio creation error:', error);
|
||||||
|
res.status(500).json({ success: false, message: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.patch('/portfolio/:id', requireAuth, upload.array('images', 10), async (req, res) => {
|
||||||
|
try {
|
||||||
|
const portfolio = await Portfolio.findByPk(req.params.id);
|
||||||
|
if (!portfolio) {
|
||||||
|
return res.status(404).json({ success: false, message: 'Portfolio not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const updates = { ...req.body };
|
||||||
|
|
||||||
|
// Handle checkboxes
|
||||||
|
updates.featured = updates.featured === 'on';
|
||||||
|
updates.isPublished = updates.isPublished === 'on';
|
||||||
|
|
||||||
|
// Process technologies
|
||||||
|
if (updates.technologies) {
|
||||||
|
try {
|
||||||
|
updates.technologies = JSON.parse(updates.technologies);
|
||||||
|
} catch (e) {
|
||||||
|
updates.technologies = updates.technologies.split(',').map(t => t.trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process new images
|
||||||
|
if (req.files && req.files.length > 0) {
|
||||||
|
const newImages = req.files.map((file, index) => ({
|
||||||
|
url: `/uploads/${file.filename}`,
|
||||||
|
alt: `${updates.title || portfolio.title} image ${index + 1}`,
|
||||||
|
isPrimary: index === 0 && (!portfolio.images || portfolio.images.length === 0)
|
||||||
|
}));
|
||||||
|
|
||||||
|
updates.images = [...(portfolio.images || []), ...newImages];
|
||||||
|
}
|
||||||
|
|
||||||
|
await portfolio.update(updates);
|
||||||
|
res.json({ success: true, portfolio });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Portfolio update error:', error);
|
||||||
|
res.status(500).json({ success: false, message: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.delete('/portfolio/:id', requireAuth, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const portfolio = await Portfolio.findByPk(req.params.id);
|
||||||
|
if (!portfolio) {
|
||||||
|
return res.status(404).json({ success: false, message: 'Portfolio not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
await portfolio.destroy();
|
||||||
|
res.json({ success: true });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Portfolio deletion error:', error);
|
||||||
|
res.status(500).json({ success: false, message: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Services API Routes
|
||||||
|
router.post('/services', requireAuth, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const {
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
shortDescription,
|
||||||
|
icon,
|
||||||
|
category,
|
||||||
|
features,
|
||||||
|
pricing,
|
||||||
|
estimatedTime,
|
||||||
|
isActive,
|
||||||
|
featured,
|
||||||
|
tags
|
||||||
|
} = req.body;
|
||||||
|
|
||||||
|
// Parse arrays
|
||||||
|
let featuresArray = [];
|
||||||
|
let tagsArray = [];
|
||||||
|
let pricingObj = {};
|
||||||
|
|
||||||
|
if (features) {
|
||||||
|
try {
|
||||||
|
featuresArray = JSON.parse(features);
|
||||||
|
} catch (e) {
|
||||||
|
featuresArray = features.split(',').map(f => f.trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tags) {
|
||||||
|
try {
|
||||||
|
tagsArray = JSON.parse(tags);
|
||||||
|
} catch (e) {
|
||||||
|
tagsArray = tags.split(',').map(t => t.trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pricing) {
|
||||||
|
try {
|
||||||
|
pricingObj = JSON.parse(pricing);
|
||||||
|
} catch (e) {
|
||||||
|
pricingObj = { basePrice: pricing };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const service = await Service.create({
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
shortDescription,
|
||||||
|
icon,
|
||||||
|
category,
|
||||||
|
features: featuresArray,
|
||||||
|
pricing: pricingObj,
|
||||||
|
estimatedTime,
|
||||||
|
isActive: isActive === 'on',
|
||||||
|
featured: featured === 'on',
|
||||||
|
tags: tagsArray
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json({ success: true, service });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Service creation error:', error);
|
||||||
|
res.status(500).json({ success: false, message: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.put('/services/:id', requireAuth, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const service = await Service.findByPk(req.params.id);
|
||||||
|
if (!service) {
|
||||||
|
return res.status(404).json({ success: false, message: 'Service not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
shortDescription,
|
||||||
|
icon,
|
||||||
|
category,
|
||||||
|
features,
|
||||||
|
pricing,
|
||||||
|
estimatedTime,
|
||||||
|
isActive,
|
||||||
|
featured,
|
||||||
|
tags,
|
||||||
|
order,
|
||||||
|
seo
|
||||||
|
} = req.body;
|
||||||
|
|
||||||
|
// Parse arrays and objects
|
||||||
|
let featuresArray = [];
|
||||||
|
let tagsArray = [];
|
||||||
|
let pricingObj = {};
|
||||||
|
let seoObj = {};
|
||||||
|
|
||||||
|
if (features) {
|
||||||
|
if (Array.isArray(features)) {
|
||||||
|
featuresArray = features;
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
featuresArray = JSON.parse(features);
|
||||||
|
} catch (e) {
|
||||||
|
featuresArray = features.split(',').map(f => f.trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tags) {
|
||||||
|
if (Array.isArray(tags)) {
|
||||||
|
tagsArray = tags;
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
tagsArray = JSON.parse(tags);
|
||||||
|
} catch (e) {
|
||||||
|
tagsArray = tags.split(',').map(t => t.trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pricing) {
|
||||||
|
if (typeof pricing === 'object') {
|
||||||
|
pricingObj = pricing;
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
pricingObj = JSON.parse(pricing);
|
||||||
|
} catch (e) {
|
||||||
|
pricingObj = { basePrice: pricing };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (seo) {
|
||||||
|
if (typeof seo === 'object') {
|
||||||
|
seoObj = seo;
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
seoObj = JSON.parse(seo);
|
||||||
|
} catch (e) {
|
||||||
|
seoObj = {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateData = {
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
shortDescription,
|
||||||
|
icon,
|
||||||
|
category,
|
||||||
|
features: featuresArray,
|
||||||
|
pricing: pricingObj,
|
||||||
|
estimatedTime,
|
||||||
|
isActive: Boolean(isActive),
|
||||||
|
featured: Boolean(featured),
|
||||||
|
tags: tagsArray,
|
||||||
|
order: order ? parseInt(order) : 0,
|
||||||
|
seo: seoObj
|
||||||
|
};
|
||||||
|
|
||||||
|
// Remove undefined values
|
||||||
|
Object.keys(updateData).forEach(key => {
|
||||||
|
if (updateData[key] === undefined) {
|
||||||
|
delete updateData[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await service.update(updateData);
|
||||||
|
res.json({ success: true, service });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Service update error:', error);
|
||||||
|
res.status(500).json({ success: false, message: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.delete('/services/:id', requireAuth, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const service = await Service.findByPk(req.params.id);
|
||||||
|
if (!service) {
|
||||||
|
return res.status(404).json({ success: false, message: 'Service not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
await service.destroy();
|
||||||
|
res.json({ success: true });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Service deletion error:', error);
|
||||||
|
res.status(500).json({ success: false, message: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Contacts API Routes
|
||||||
|
router.patch('/contacts/:id', requireAuth, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const contact = await Contact.findByPk(req.params.id);
|
||||||
|
if (!contact) {
|
||||||
|
return res.status(404).json({ success: false, message: 'Contact not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
await contact.update(req.body);
|
||||||
|
res.json({ success: true, contact });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Contact update error:', error);
|
||||||
|
res.status(500).json({ success: false, message: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.delete('/contacts/:id', requireAuth, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const contact = await Contact.findByPk(req.params.id);
|
||||||
|
if (!contact) {
|
||||||
|
return res.status(404).json({ success: false, message: 'Contact not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
await contact.destroy();
|
||||||
|
res.json({ success: true });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Contact deletion error:', error);
|
||||||
|
res.status(500).json({ success: false, message: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Telegram notification for contact
|
||||||
|
router.post('/contacts/:id/telegram', requireAuth, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const contact = await Contact.findByPk(req.params.id);
|
||||||
|
if (!contact) {
|
||||||
|
return res.status(404).json({ success: false, message: 'Contact not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send Telegram notification
|
||||||
|
const telegramService = require('../../services/telegram');
|
||||||
|
const result = await telegramService.sendContactNotification(contact);
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
res.json({ success: true });
|
||||||
|
} else {
|
||||||
|
res.status(500).json({ success: false, message: result.message || result.error });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Telegram notification error:', error);
|
||||||
|
res.status(500).json({ success: false, message: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test Telegram connection
|
||||||
|
router.post('/telegram/test', requireAuth, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { botToken, chatId } = req.body;
|
||||||
|
|
||||||
|
// Temporarily set up telegram service with provided credentials
|
||||||
|
const axios = require('axios');
|
||||||
|
|
||||||
|
// Test bot info
|
||||||
|
const botResponse = await axios.get(`https://api.telegram.org/bot${botToken}/getMe`);
|
||||||
|
|
||||||
|
// Test sending a message
|
||||||
|
const testMessage = '✅ Telegram bot подключен успешно!\n\nЭто тестовое сообщение от SmartSolTech Admin Panel.';
|
||||||
|
await axios.post(`https://api.telegram.org/bot${botToken}/sendMessage`, {
|
||||||
|
chat_id: chatId,
|
||||||
|
text: testMessage,
|
||||||
|
parse_mode: 'Markdown'
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
bot: botResponse.data.result,
|
||||||
|
message: 'Test message sent successfully'
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Telegram test error:', error);
|
||||||
|
let message = 'Connection failed';
|
||||||
|
|
||||||
|
if (error.response?.data?.description) {
|
||||||
|
message = error.response.data.description;
|
||||||
|
} else if (error.message) {
|
||||||
|
message = error.message;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(400).json({ success: false, message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Settings API
|
||||||
|
const { SiteSettings } = require('../../models');
|
||||||
|
|
||||||
|
router.get('/settings', requireAuth, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const settings = await SiteSettings.findOne() || {};
|
||||||
|
res.json({ success: true, settings });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Settings fetch error:', error);
|
||||||
|
res.status(500).json({ success: false, message: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post('/settings', requireAuth, upload.fields([
|
||||||
|
{ name: 'logo', maxCount: 1 },
|
||||||
|
{ name: 'favicon', maxCount: 1 }
|
||||||
|
]), async (req, res) => {
|
||||||
|
try {
|
||||||
|
let settings = await SiteSettings.findOne();
|
||||||
|
if (!settings) {
|
||||||
|
settings = await SiteSettings.create({});
|
||||||
|
}
|
||||||
|
|
||||||
|
const updates = {};
|
||||||
|
|
||||||
|
// Handle nested objects
|
||||||
|
Object.keys(req.body).forEach(key => {
|
||||||
|
if (key.includes('.')) {
|
||||||
|
const [parent, child] = key.split('.');
|
||||||
|
if (!updates[parent]) updates[parent] = {};
|
||||||
|
updates[parent][child] = req.body[key];
|
||||||
|
} else {
|
||||||
|
updates[key] = req.body[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle file uploads
|
||||||
|
if (req.files.logo) {
|
||||||
|
updates.logo = `/uploads/${req.files.logo[0].filename}`;
|
||||||
|
}
|
||||||
|
if (req.files.favicon) {
|
||||||
|
updates.favicon = `/uploads/${req.files.favicon[0].filename}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update existing settings with new values
|
||||||
|
Object.keys(updates).forEach(key => {
|
||||||
|
if (typeof updates[key] === 'object' && updates[key] !== null) {
|
||||||
|
settings[key] = { ...settings[key], ...updates[key] };
|
||||||
|
} else {
|
||||||
|
settings[key] = updates[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await settings.save();
|
||||||
|
|
||||||
|
res.json({ success: true, settings });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Settings update error:', error);
|
||||||
|
res.status(500).json({ success: false, message: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
77
.history/routes/demo-adminlte_20251026211505.js
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
// Демо-роут для AdminLTE админки
|
||||||
|
router.get('/demo-adminlte', async (req, res) => {
|
||||||
|
try {
|
||||||
|
// Мок-данные для демонстрации
|
||||||
|
const stats = {
|
||||||
|
portfolioCount: 12,
|
||||||
|
servicesCount: 6,
|
||||||
|
contactsCount: 24,
|
||||||
|
usersCount: 3
|
||||||
|
};
|
||||||
|
|
||||||
|
const recentPortfolio = [
|
||||||
|
{
|
||||||
|
title: '삼성 모바일 앱 개발',
|
||||||
|
category: 'mobile-app',
|
||||||
|
status: 'completed',
|
||||||
|
createdAt: new Date('2024-10-20')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'LG 웹사이트 리뉴얼',
|
||||||
|
category: 'web-development',
|
||||||
|
status: 'in-progress',
|
||||||
|
createdAt: new Date('2024-10-18')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '현대차 UI/UX 디자인',
|
||||||
|
category: 'ui-ux-design',
|
||||||
|
status: 'planning',
|
||||||
|
createdAt: new Date('2024-10-15')
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const recentContacts = [
|
||||||
|
{
|
||||||
|
name: '김철수',
|
||||||
|
email: 'kim@example.com',
|
||||||
|
status: 'pending',
|
||||||
|
createdAt: new Date('2024-10-25')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '이영희',
|
||||||
|
email: 'lee@company.kr',
|
||||||
|
status: 'replied',
|
||||||
|
createdAt: new Date('2024-10-24')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '박민수',
|
||||||
|
email: 'park@startup.co.kr',
|
||||||
|
status: 'new',
|
||||||
|
createdAt: new Date('2024-10-23')
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const user = {
|
||||||
|
name: '관리자'
|
||||||
|
};
|
||||||
|
|
||||||
|
res.render('admin/dashboard-adminlte', {
|
||||||
|
layout: 'admin/layout-adminlte',
|
||||||
|
title: '대시보드',
|
||||||
|
currentPage: 'dashboard',
|
||||||
|
currentLanguage: 'ko',
|
||||||
|
stats,
|
||||||
|
recentPortfolio,
|
||||||
|
recentContacts,
|
||||||
|
user
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('AdminLTE Demo Error:', error);
|
||||||
|
res.status(500).send('Server Error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
77
.history/routes/demo-adminlte_20251026211507.js
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
// Демо-роут для AdminLTE админки
|
||||||
|
router.get('/demo-adminlte', async (req, res) => {
|
||||||
|
try {
|
||||||
|
// Мок-данные для демонстрации
|
||||||
|
const stats = {
|
||||||
|
portfolioCount: 12,
|
||||||
|
servicesCount: 6,
|
||||||
|
contactsCount: 24,
|
||||||
|
usersCount: 3
|
||||||
|
};
|
||||||
|
|
||||||
|
const recentPortfolio = [
|
||||||
|
{
|
||||||
|
title: '삼성 모바일 앱 개발',
|
||||||
|
category: 'mobile-app',
|
||||||
|
status: 'completed',
|
||||||
|
createdAt: new Date('2024-10-20')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'LG 웹사이트 리뉴얼',
|
||||||
|
category: 'web-development',
|
||||||
|
status: 'in-progress',
|
||||||
|
createdAt: new Date('2024-10-18')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '현대차 UI/UX 디자인',
|
||||||
|
category: 'ui-ux-design',
|
||||||
|
status: 'planning',
|
||||||
|
createdAt: new Date('2024-10-15')
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const recentContacts = [
|
||||||
|
{
|
||||||
|
name: '김철수',
|
||||||
|
email: 'kim@example.com',
|
||||||
|
status: 'pending',
|
||||||
|
createdAt: new Date('2024-10-25')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '이영희',
|
||||||
|
email: 'lee@company.kr',
|
||||||
|
status: 'replied',
|
||||||
|
createdAt: new Date('2024-10-24')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '박민수',
|
||||||
|
email: 'park@startup.co.kr',
|
||||||
|
status: 'new',
|
||||||
|
createdAt: new Date('2024-10-23')
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const user = {
|
||||||
|
name: '관리자'
|
||||||
|
};
|
||||||
|
|
||||||
|
res.render('admin/dashboard-adminlte', {
|
||||||
|
layout: 'admin/layout-adminlte',
|
||||||
|
title: '대시보드',
|
||||||
|
currentPage: 'dashboard',
|
||||||
|
currentLanguage: 'ko',
|
||||||
|
stats,
|
||||||
|
recentPortfolio,
|
||||||
|
recentContacts,
|
||||||
|
user
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('AdminLTE Demo Error:', error);
|
||||||
|
res.status(500).send('Server Error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
150
.history/routes/demo-adminlte_20251026211708.js
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
// Демо-роут для AdminLTE админки
|
||||||
|
router.get('/demo-adminlte', async (req, res) => {
|
||||||
|
try {
|
||||||
|
// Мок-данные для демонстрации
|
||||||
|
const stats = {
|
||||||
|
portfolioCount: 12,
|
||||||
|
servicesCount: 6,
|
||||||
|
contactsCount: 24,
|
||||||
|
usersCount: 3
|
||||||
|
};
|
||||||
|
|
||||||
|
const recentPortfolio = [
|
||||||
|
{
|
||||||
|
title: '삼성 모바일 앱 개발',
|
||||||
|
category: 'mobile-app',
|
||||||
|
status: 'completed',
|
||||||
|
createdAt: new Date('2024-10-20')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'LG 웹사이트 리뉴얼',
|
||||||
|
category: 'web-development',
|
||||||
|
status: 'in-progress',
|
||||||
|
createdAt: new Date('2024-10-18')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '현대차 UI/UX 디자인',
|
||||||
|
category: 'ui-ux-design',
|
||||||
|
status: 'planning',
|
||||||
|
createdAt: new Date('2024-10-15')
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const recentContacts = [
|
||||||
|
{
|
||||||
|
name: '김철수',
|
||||||
|
email: 'kim@example.com',
|
||||||
|
status: 'pending',
|
||||||
|
createdAt: new Date('2024-10-25')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '이영희',
|
||||||
|
email: 'lee@company.kr',
|
||||||
|
status: 'replied',
|
||||||
|
createdAt: new Date('2024-10-24')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '박민수',
|
||||||
|
email: 'park@startup.co.kr',
|
||||||
|
status: 'new',
|
||||||
|
createdAt: new Date('2024-10-23')
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const user = {
|
||||||
|
name: '관리자'
|
||||||
|
};
|
||||||
|
|
||||||
|
res.render('admin/dashboard-adminlte', {
|
||||||
|
layout: 'admin/layout-adminlte',
|
||||||
|
title: '대시보드',
|
||||||
|
currentPage: 'dashboard',
|
||||||
|
currentLanguage: 'ko',
|
||||||
|
stats,
|
||||||
|
recentPortfolio,
|
||||||
|
recentContacts,
|
||||||
|
user
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('AdminLTE Demo Error:', error);
|
||||||
|
res.status(500).send('Server Error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Демо-роут для Tabler админки
|
||||||
|
router.get('/demo-tabler', async (req, res) => {
|
||||||
|
try {
|
||||||
|
// Мок-данные для демонстрации
|
||||||
|
const stats = {
|
||||||
|
portfolioCount: 12,
|
||||||
|
servicesCount: 6,
|
||||||
|
contactsCount: 24,
|
||||||
|
usersCount: 3
|
||||||
|
};
|
||||||
|
|
||||||
|
const recentPortfolio = [
|
||||||
|
{
|
||||||
|
title: '삼성 모바일 앱 개발',
|
||||||
|
category: 'mobile-app',
|
||||||
|
status: 'completed',
|
||||||
|
createdAt: new Date('2024-10-20')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'LG 웹사이트 리뉴얼',
|
||||||
|
category: 'web-development',
|
||||||
|
status: 'in-progress',
|
||||||
|
createdAt: new Date('2024-10-18')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '현대차 UI/UX 디자인',
|
||||||
|
category: 'ui-ux-design',
|
||||||
|
status: 'planning',
|
||||||
|
createdAt: new Date('2024-10-15')
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const recentContacts = [
|
||||||
|
{
|
||||||
|
name: '김철수',
|
||||||
|
email: 'kim@example.com',
|
||||||
|
status: 'pending',
|
||||||
|
createdAt: new Date('2024-10-25')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '이영희',
|
||||||
|
email: 'lee@company.kr',
|
||||||
|
status: 'replied',
|
||||||
|
createdAt: new Date('2024-10-24')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '박민수',
|
||||||
|
email: 'park@startup.co.kr',
|
||||||
|
status: 'new',
|
||||||
|
createdAt: new Date('2024-10-23')
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const user = {
|
||||||
|
name: '관리자'
|
||||||
|
};
|
||||||
|
|
||||||
|
res.render('admin/dashboard-tabler', {
|
||||||
|
layout: 'admin/layout-tabler',
|
||||||
|
title: '대시보드',
|
||||||
|
currentPage: 'dashboard',
|
||||||
|
currentLanguage: 'ko',
|
||||||
|
stats,
|
||||||
|
recentPortfolio,
|
||||||
|
recentContacts,
|
||||||
|
user
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Tabler Demo Error:', error);
|
||||||
|
res.status(500).send('Server Error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
150
.history/routes/demo-adminlte_20251026211716.js
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
// Демо-роут для AdminLTE админки
|
||||||
|
router.get('/demo-adminlte', async (req, res) => {
|
||||||
|
try {
|
||||||
|
// Мок-данные для демонстрации
|
||||||
|
const stats = {
|
||||||
|
portfolioCount: 12,
|
||||||
|
servicesCount: 6,
|
||||||
|
contactsCount: 24,
|
||||||
|
usersCount: 3
|
||||||
|
};
|
||||||
|
|
||||||
|
const recentPortfolio = [
|
||||||
|
{
|
||||||
|
title: '삼성 모바일 앱 개발',
|
||||||
|
category: 'mobile-app',
|
||||||
|
status: 'completed',
|
||||||
|
createdAt: new Date('2024-10-20')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'LG 웹사이트 리뉴얼',
|
||||||
|
category: 'web-development',
|
||||||
|
status: 'in-progress',
|
||||||
|
createdAt: new Date('2024-10-18')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '현대차 UI/UX 디자인',
|
||||||
|
category: 'ui-ux-design',
|
||||||
|
status: 'planning',
|
||||||
|
createdAt: new Date('2024-10-15')
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const recentContacts = [
|
||||||
|
{
|
||||||
|
name: '김철수',
|
||||||
|
email: 'kim@example.com',
|
||||||
|
status: 'pending',
|
||||||
|
createdAt: new Date('2024-10-25')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '이영희',
|
||||||
|
email: 'lee@company.kr',
|
||||||
|
status: 'replied',
|
||||||
|
createdAt: new Date('2024-10-24')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '박민수',
|
||||||
|
email: 'park@startup.co.kr',
|
||||||
|
status: 'new',
|
||||||
|
createdAt: new Date('2024-10-23')
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const user = {
|
||||||
|
name: '관리자'
|
||||||
|
};
|
||||||
|
|
||||||
|
res.render('admin/dashboard-adminlte', {
|
||||||
|
layout: 'admin/layout-adminlte',
|
||||||
|
title: '대시보드',
|
||||||
|
currentPage: 'dashboard',
|
||||||
|
currentLanguage: 'ko',
|
||||||
|
stats,
|
||||||
|
recentPortfolio,
|
||||||
|
recentContacts,
|
||||||
|
user
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('AdminLTE Demo Error:', error);
|
||||||
|
res.status(500).send('Server Error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Демо-роут для Tabler админки
|
||||||
|
router.get('/demo-tabler', async (req, res) => {
|
||||||
|
try {
|
||||||
|
// Мок-данные для демонстрации
|
||||||
|
const stats = {
|
||||||
|
portfolioCount: 12,
|
||||||
|
servicesCount: 6,
|
||||||
|
contactsCount: 24,
|
||||||
|
usersCount: 3
|
||||||
|
};
|
||||||
|
|
||||||
|
const recentPortfolio = [
|
||||||
|
{
|
||||||
|
title: '삼성 모바일 앱 개발',
|
||||||
|
category: 'mobile-app',
|
||||||
|
status: 'completed',
|
||||||
|
createdAt: new Date('2024-10-20')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'LG 웹사이트 리뉴얼',
|
||||||
|
category: 'web-development',
|
||||||
|
status: 'in-progress',
|
||||||
|
createdAt: new Date('2024-10-18')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '현대차 UI/UX 디자인',
|
||||||
|
category: 'ui-ux-design',
|
||||||
|
status: 'planning',
|
||||||
|
createdAt: new Date('2024-10-15')
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const recentContacts = [
|
||||||
|
{
|
||||||
|
name: '김철수',
|
||||||
|
email: 'kim@example.com',
|
||||||
|
status: 'pending',
|
||||||
|
createdAt: new Date('2024-10-25')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '이영희',
|
||||||
|
email: 'lee@company.kr',
|
||||||
|
status: 'replied',
|
||||||
|
createdAt: new Date('2024-10-24')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '박민수',
|
||||||
|
email: 'park@startup.co.kr',
|
||||||
|
status: 'new',
|
||||||
|
createdAt: new Date('2024-10-23')
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const user = {
|
||||||
|
name: '관리자'
|
||||||
|
};
|
||||||
|
|
||||||
|
res.render('admin/dashboard-tabler', {
|
||||||
|
layout: 'admin/layout-tabler',
|
||||||
|
title: '대시보드',
|
||||||
|
currentPage: 'dashboard',
|
||||||
|
currentLanguage: 'ko',
|
||||||
|
stats,
|
||||||
|
recentPortfolio,
|
||||||
|
recentContacts,
|
||||||
|
user
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Tabler Demo Error:', error);
|
||||||
|
res.status(500).send('Server Error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
162
.history/routes/demo-adminlte_20251026211846.js
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
// Главная страница сравнения admin bundles
|
||||||
|
router.get('/admin-comparison', async (req, res) => {
|
||||||
|
try {
|
||||||
|
res.render('admin-bundle-comparison', {
|
||||||
|
layout: false // Не используем layout для этой страницы
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Admin Comparison Error:', error);
|
||||||
|
res.status(500).send('Server Error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Демо-роут для AdminLTE админки
|
||||||
|
router.get('/demo-adminlte', async (req, res) => {
|
||||||
|
try {
|
||||||
|
// Мок-данные для демонстрации
|
||||||
|
const stats = {
|
||||||
|
portfolioCount: 12,
|
||||||
|
servicesCount: 6,
|
||||||
|
contactsCount: 24,
|
||||||
|
usersCount: 3
|
||||||
|
};
|
||||||
|
|
||||||
|
const recentPortfolio = [
|
||||||
|
{
|
||||||
|
title: '삼성 모바일 앱 개발',
|
||||||
|
category: 'mobile-app',
|
||||||
|
status: 'completed',
|
||||||
|
createdAt: new Date('2024-10-20')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'LG 웹사이트 리뉴얼',
|
||||||
|
category: 'web-development',
|
||||||
|
status: 'in-progress',
|
||||||
|
createdAt: new Date('2024-10-18')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '현대차 UI/UX 디자인',
|
||||||
|
category: 'ui-ux-design',
|
||||||
|
status: 'planning',
|
||||||
|
createdAt: new Date('2024-10-15')
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const recentContacts = [
|
||||||
|
{
|
||||||
|
name: '김철수',
|
||||||
|
email: 'kim@example.com',
|
||||||
|
status: 'pending',
|
||||||
|
createdAt: new Date('2024-10-25')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '이영희',
|
||||||
|
email: 'lee@company.kr',
|
||||||
|
status: 'replied',
|
||||||
|
createdAt: new Date('2024-10-24')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '박민수',
|
||||||
|
email: 'park@startup.co.kr',
|
||||||
|
status: 'new',
|
||||||
|
createdAt: new Date('2024-10-23')
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const user = {
|
||||||
|
name: '관리자'
|
||||||
|
};
|
||||||
|
|
||||||
|
res.render('admin/dashboard-adminlte', {
|
||||||
|
layout: 'admin/layout-adminlte',
|
||||||
|
title: '대시보드',
|
||||||
|
currentPage: 'dashboard',
|
||||||
|
currentLanguage: 'ko',
|
||||||
|
stats,
|
||||||
|
recentPortfolio,
|
||||||
|
recentContacts,
|
||||||
|
user
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('AdminLTE Demo Error:', error);
|
||||||
|
res.status(500).send('Server Error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Демо-роут для Tabler админки
|
||||||
|
router.get('/demo-tabler', async (req, res) => {
|
||||||
|
try {
|
||||||
|
// Мок-данные для демонстрации
|
||||||
|
const stats = {
|
||||||
|
portfolioCount: 12,
|
||||||
|
servicesCount: 6,
|
||||||
|
contactsCount: 24,
|
||||||
|
usersCount: 3
|
||||||
|
};
|
||||||
|
|
||||||
|
const recentPortfolio = [
|
||||||
|
{
|
||||||
|
title: '삼성 모바일 앱 개발',
|
||||||
|
category: 'mobile-app',
|
||||||
|
status: 'completed',
|
||||||
|
createdAt: new Date('2024-10-20')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'LG 웹사이트 리뉴얼',
|
||||||
|
category: 'web-development',
|
||||||
|
status: 'in-progress',
|
||||||
|
createdAt: new Date('2024-10-18')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '현대차 UI/UX 디자인',
|
||||||
|
category: 'ui-ux-design',
|
||||||
|
status: 'planning',
|
||||||
|
createdAt: new Date('2024-10-15')
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const recentContacts = [
|
||||||
|
{
|
||||||
|
name: '김철수',
|
||||||
|
email: 'kim@example.com',
|
||||||
|
status: 'pending',
|
||||||
|
createdAt: new Date('2024-10-25')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '이영희',
|
||||||
|
email: 'lee@company.kr',
|
||||||
|
status: 'replied',
|
||||||
|
createdAt: new Date('2024-10-24')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '박민수',
|
||||||
|
email: 'park@startup.co.kr',
|
||||||
|
status: 'new',
|
||||||
|
createdAt: new Date('2024-10-23')
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const user = {
|
||||||
|
name: '관리자'
|
||||||
|
};
|
||||||
|
|
||||||
|
res.render('admin/dashboard-tabler', {
|
||||||
|
layout: 'admin/layout-tabler',
|
||||||
|
title: '대시보드',
|
||||||
|
currentPage: 'dashboard',
|
||||||
|
currentLanguage: 'ko',
|
||||||
|
stats,
|
||||||
|
recentPortfolio,
|
||||||
|
recentContacts,
|
||||||
|
user
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Tabler Demo Error:', error);
|
||||||
|
res.status(500).send('Server Error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
162
.history/routes/demo-adminlte_20251026211849.js
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
// Главная страница сравнения admin bundles
|
||||||
|
router.get('/admin-comparison', async (req, res) => {
|
||||||
|
try {
|
||||||
|
res.render('admin-bundle-comparison', {
|
||||||
|
layout: false // Не используем layout для этой страницы
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Admin Comparison Error:', error);
|
||||||
|
res.status(500).send('Server Error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Демо-роут для AdminLTE админки
|
||||||
|
router.get('/demo-adminlte', async (req, res) => {
|
||||||
|
try {
|
||||||
|
// Мок-данные для демонстрации
|
||||||
|
const stats = {
|
||||||
|
portfolioCount: 12,
|
||||||
|
servicesCount: 6,
|
||||||
|
contactsCount: 24,
|
||||||
|
usersCount: 3
|
||||||
|
};
|
||||||
|
|
||||||
|
const recentPortfolio = [
|
||||||
|
{
|
||||||
|
title: '삼성 모바일 앱 개발',
|
||||||
|
category: 'mobile-app',
|
||||||
|
status: 'completed',
|
||||||
|
createdAt: new Date('2024-10-20')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'LG 웹사이트 리뉴얼',
|
||||||
|
category: 'web-development',
|
||||||
|
status: 'in-progress',
|
||||||
|
createdAt: new Date('2024-10-18')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '현대차 UI/UX 디자인',
|
||||||
|
category: 'ui-ux-design',
|
||||||
|
status: 'planning',
|
||||||
|
createdAt: new Date('2024-10-15')
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const recentContacts = [
|
||||||
|
{
|
||||||
|
name: '김철수',
|
||||||
|
email: 'kim@example.com',
|
||||||
|
status: 'pending',
|
||||||
|
createdAt: new Date('2024-10-25')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '이영희',
|
||||||
|
email: 'lee@company.kr',
|
||||||
|
status: 'replied',
|
||||||
|
createdAt: new Date('2024-10-24')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '박민수',
|
||||||
|
email: 'park@startup.co.kr',
|
||||||
|
status: 'new',
|
||||||
|
createdAt: new Date('2024-10-23')
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const user = {
|
||||||
|
name: '관리자'
|
||||||
|
};
|
||||||
|
|
||||||
|
res.render('admin/dashboard-adminlte', {
|
||||||
|
layout: 'admin/layout-adminlte',
|
||||||
|
title: '대시보드',
|
||||||
|
currentPage: 'dashboard',
|
||||||
|
currentLanguage: 'ko',
|
||||||
|
stats,
|
||||||
|
recentPortfolio,
|
||||||
|
recentContacts,
|
||||||
|
user
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('AdminLTE Demo Error:', error);
|
||||||
|
res.status(500).send('Server Error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Демо-роут для Tabler админки
|
||||||
|
router.get('/demo-tabler', async (req, res) => {
|
||||||
|
try {
|
||||||
|
// Мок-данные для демонстрации
|
||||||
|
const stats = {
|
||||||
|
portfolioCount: 12,
|
||||||
|
servicesCount: 6,
|
||||||
|
contactsCount: 24,
|
||||||
|
usersCount: 3
|
||||||
|
};
|
||||||
|
|
||||||
|
const recentPortfolio = [
|
||||||
|
{
|
||||||
|
title: '삼성 모바일 앱 개발',
|
||||||
|
category: 'mobile-app',
|
||||||
|
status: 'completed',
|
||||||
|
createdAt: new Date('2024-10-20')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'LG 웹사이트 리뉴얼',
|
||||||
|
category: 'web-development',
|
||||||
|
status: 'in-progress',
|
||||||
|
createdAt: new Date('2024-10-18')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '현대차 UI/UX 디자인',
|
||||||
|
category: 'ui-ux-design',
|
||||||
|
status: 'planning',
|
||||||
|
createdAt: new Date('2024-10-15')
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const recentContacts = [
|
||||||
|
{
|
||||||
|
name: '김철수',
|
||||||
|
email: 'kim@example.com',
|
||||||
|
status: 'pending',
|
||||||
|
createdAt: new Date('2024-10-25')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '이영희',
|
||||||
|
email: 'lee@company.kr',
|
||||||
|
status: 'replied',
|
||||||
|
createdAt: new Date('2024-10-24')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '박민수',
|
||||||
|
email: 'park@startup.co.kr',
|
||||||
|
status: 'new',
|
||||||
|
createdAt: new Date('2024-10-23')
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const user = {
|
||||||
|
name: '관리자'
|
||||||
|
};
|
||||||
|
|
||||||
|
res.render('admin/dashboard-tabler', {
|
||||||
|
layout: 'admin/layout-tabler',
|
||||||
|
title: '대시보드',
|
||||||
|
currentPage: 'dashboard',
|
||||||
|
currentLanguage: 'ko',
|
||||||
|
stats,
|
||||||
|
recentPortfolio,
|
||||||
|
recentContacts,
|
||||||
|
user
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Tabler Demo Error:', error);
|
||||||
|
res.status(500).send('Server Error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
220
.history/server_20251026211452.js
Normal file
@@ -0,0 +1,220 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const { sequelize, testConnection } = require('./config/database');
|
||||||
|
const session = require('express-session');
|
||||||
|
const SequelizeStore = require('connect-session-sequelize')(session.Store);
|
||||||
|
const path = require('path');
|
||||||
|
const helmet = require('helmet');
|
||||||
|
const compression = require('compression');
|
||||||
|
const cors = require('cors');
|
||||||
|
const morgan = require('morgan');
|
||||||
|
const rateLimit = require('express-rate-limit');
|
||||||
|
const i18n = require('i18n');
|
||||||
|
require('dotenv').config();
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
|
||||||
|
// Настройка i18n
|
||||||
|
i18n.configure({
|
||||||
|
locales: ['ko', 'en', 'ru', 'kk'],
|
||||||
|
defaultLocale: 'ru',
|
||||||
|
directory: path.join(__dirname, 'locales'),
|
||||||
|
objectNotation: true,
|
||||||
|
updateFiles: false,
|
||||||
|
syncFiles: false
|
||||||
|
});
|
||||||
|
|
||||||
|
// i18n middleware
|
||||||
|
app.use(i18n.init);
|
||||||
|
|
||||||
|
// Security middleware
|
||||||
|
app.use(helmet({
|
||||||
|
contentSecurityPolicy: {
|
||||||
|
directives: {
|
||||||
|
defaultSrc: ["'self'"],
|
||||||
|
styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com", "https://cdnjs.cloudflare.com", "https://cdn.jsdelivr.net", "https://unpkg.com", "https://cdn.tailwindcss.com"],
|
||||||
|
fontSrc: ["'self'", "https://fonts.gstatic.com", "https://cdnjs.cloudflare.com"],
|
||||||
|
scriptSrc: ["'self'", "'unsafe-inline'", "https://cdnjs.cloudflare.com", "https://unpkg.com", "https://cdn.tailwindcss.com"],
|
||||||
|
imgSrc: ["'self'", "data:", "https:"],
|
||||||
|
connectSrc: ["'self'", "ws:", "wss:", "https://cdnjs.cloudflare.com", "https://cdn.jsdelivr.net", "https://unpkg.com", "https://fonts.googleapis.com", "https://fonts.gstatic.com", "https://cdn.tailwindcss.com"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Rate limiting
|
||||||
|
const limiter = rateLimit({
|
||||||
|
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||||
|
max: 100 // limit each IP to 100 requests per windowMs
|
||||||
|
});
|
||||||
|
app.use('/api/', limiter);
|
||||||
|
|
||||||
|
// Middleware
|
||||||
|
app.use(compression());
|
||||||
|
app.use(cors());
|
||||||
|
app.use(morgan('combined'));
|
||||||
|
app.use(express.json({ limit: '10mb' }));
|
||||||
|
app.use(express.urlencoded({ extended: true, limit: '10mb' }));
|
||||||
|
|
||||||
|
// Static files
|
||||||
|
app.use(express.static(path.join(__dirname, 'public')));
|
||||||
|
app.use('/uploads', express.static(path.join(__dirname, 'public/uploads')));
|
||||||
|
|
||||||
|
// AdminLTE static files
|
||||||
|
app.use('/node_modules', express.static(path.join(__dirname, 'node_modules')));
|
||||||
|
|
||||||
|
// View engine
|
||||||
|
app.set('view engine', 'ejs');
|
||||||
|
app.set('views', path.join(__dirname, 'views'));
|
||||||
|
|
||||||
|
// Layout engine
|
||||||
|
const expressLayouts = require('express-ejs-layouts');
|
||||||
|
app.use(expressLayouts);
|
||||||
|
app.set('layout', 'layout'); // Default layout for main site
|
||||||
|
app.set('layout extractScripts', true);
|
||||||
|
app.set('layout extractStyles', true);
|
||||||
|
|
||||||
|
// Database connection and testing
|
||||||
|
testConnection();
|
||||||
|
|
||||||
|
// Session store configuration
|
||||||
|
const sessionStore = new SequelizeStore({
|
||||||
|
db: sequelize,
|
||||||
|
tableName: 'sessions',
|
||||||
|
checkExpirationInterval: 15 * 60 * 1000, // 15 minutes
|
||||||
|
expiration: 7 * 24 * 60 * 60 * 1000 // 7 days
|
||||||
|
});
|
||||||
|
|
||||||
|
// Session configuration
|
||||||
|
app.use(session({
|
||||||
|
secret: process.env.SESSION_SECRET || 'your-secret-key',
|
||||||
|
resave: false,
|
||||||
|
saveUninitialized: false,
|
||||||
|
store: sessionStore,
|
||||||
|
cookie: {
|
||||||
|
secure: process.env.NODE_ENV === 'production',
|
||||||
|
httpOnly: true,
|
||||||
|
maxAge: 1000 * 60 * 60 * 24 * 7 // 7 days
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Middleware для передачи переменных в шаблоны
|
||||||
|
app.use((req, res, next) => {
|
||||||
|
const currentLang = req.session?.language || req.getLocale() || 'ru';
|
||||||
|
req.setLocale(currentLang);
|
||||||
|
|
||||||
|
res.locals.locale = currentLang;
|
||||||
|
res.locals.__ = res.__;
|
||||||
|
res.locals.theme = req.session?.theme || 'light';
|
||||||
|
res.locals.currentLanguage = currentLang;
|
||||||
|
res.locals.currentPage = req.path.split('/')[1] || 'home';
|
||||||
|
|
||||||
|
// Debug logging for theme
|
||||||
|
if (req.url !== '/sw.js' && !req.url.startsWith('/css/') && !req.url.startsWith('/js/') && !req.url.startsWith('/images/')) {
|
||||||
|
console.log(`Request URL: ${req.url}, Theme from session: ${req.session?.theme}, Setting theme to: ${res.locals.theme}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Устанавливаем заголовки для предотвращения кеширования языкового контента
|
||||||
|
if (!req.url.startsWith('/css/') && !req.url.startsWith('/js/') && !req.url.startsWith('/images/')) {
|
||||||
|
res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
|
||||||
|
res.setHeader('Pragma', 'no-cache');
|
||||||
|
res.setHeader('Expires', '0');
|
||||||
|
res.setHeader('Vary', 'Accept-Language');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Отладочная информация
|
||||||
|
console.log(`Request URL: ${req.url}, Current Language: ${currentLang}, Session Language: ${req.session?.language}, Locale: ${req.getLocale()}`);
|
||||||
|
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Routes
|
||||||
|
app.use('/', require('./routes/index'));
|
||||||
|
app.use('/api/auth', require('./routes/auth'));
|
||||||
|
app.use('/api/portfolio', require('./routes/portfolio'));
|
||||||
|
app.use('/api/services', require('./routes/services'));
|
||||||
|
app.use('/api/calculator', require('./routes/calculator'));
|
||||||
|
app.use('/api/contact', require('./routes/contact'));
|
||||||
|
app.use('/api/media', require('./routes/media'));
|
||||||
|
app.use('/api/admin', require('./routes/api/admin'));
|
||||||
|
app.use('/admin', require('./routes/admin'));
|
||||||
|
|
||||||
|
// Language switching routes
|
||||||
|
app.get('/lang/:language', (req, res) => {
|
||||||
|
const { language } = req.params;
|
||||||
|
const supportedLanguages = ['ko', 'en', 'ru', 'kk'];
|
||||||
|
|
||||||
|
if (supportedLanguages.includes(language)) {
|
||||||
|
req.setLocale(language);
|
||||||
|
req.session.language = language;
|
||||||
|
console.log(`Language switched to: ${language}, session language: ${req.session.language}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const referer = req.get('Referer') || '/';
|
||||||
|
res.redirect(referer);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Theme switching routes
|
||||||
|
app.get('/theme/:theme', (req, res) => {
|
||||||
|
const { theme } = req.params;
|
||||||
|
if (['light', 'dark'].includes(theme)) {
|
||||||
|
req.session.theme = theme;
|
||||||
|
}
|
||||||
|
res.json({ success: true, theme: req.session.theme });
|
||||||
|
});
|
||||||
|
|
||||||
|
// PWA Service Worker
|
||||||
|
app.get('/sw.js', (req, res) => {
|
||||||
|
res.sendFile(path.join(__dirname, 'public', 'sw.js'));
|
||||||
|
});
|
||||||
|
|
||||||
|
// PWA Manifest
|
||||||
|
app.get('/manifest.json', (req, res) => {
|
||||||
|
res.sendFile(path.join(__dirname, 'public', 'manifest.json'));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Error handling middleware
|
||||||
|
app.use((err, req, res, next) => {
|
||||||
|
console.error(err.stack);
|
||||||
|
res.status(500).render('error', {
|
||||||
|
title: 'Error',
|
||||||
|
settings: {},
|
||||||
|
message: process.env.NODE_ENV === 'production'
|
||||||
|
? 'Something went wrong!'
|
||||||
|
: err.message,
|
||||||
|
currentPage: 'error'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 404 handler
|
||||||
|
app.use((req, res) => {
|
||||||
|
res.status(404).render('error', {
|
||||||
|
title: '404 - 페이지를 찾을 수 없습니다',
|
||||||
|
settings: {},
|
||||||
|
message: '요청하신 페이지를 찾을 수 없습니다',
|
||||||
|
currentPage: 'error'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const PORT = process.env.PORT || 3000;
|
||||||
|
|
||||||
|
// Sync database and start server
|
||||||
|
async function startServer() {
|
||||||
|
try {
|
||||||
|
// Sync all models with database
|
||||||
|
await sequelize.sync({ force: false });
|
||||||
|
console.log('✓ Database synchronized');
|
||||||
|
|
||||||
|
// Create session table
|
||||||
|
await sessionStore.sync();
|
||||||
|
console.log('✓ Session store synchronized');
|
||||||
|
|
||||||
|
app.listen(PORT, () => {
|
||||||
|
console.log(`🚀 Server running on port ${PORT}`);
|
||||||
|
console.log(`🌐 Visit: http://localhost:${PORT}`);
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('✗ Failed to start server:', error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
startServer();
|
||||||
220
.history/server_20251026211459.js
Normal file
@@ -0,0 +1,220 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const { sequelize, testConnection } = require('./config/database');
|
||||||
|
const session = require('express-session');
|
||||||
|
const SequelizeStore = require('connect-session-sequelize')(session.Store);
|
||||||
|
const path = require('path');
|
||||||
|
const helmet = require('helmet');
|
||||||
|
const compression = require('compression');
|
||||||
|
const cors = require('cors');
|
||||||
|
const morgan = require('morgan');
|
||||||
|
const rateLimit = require('express-rate-limit');
|
||||||
|
const i18n = require('i18n');
|
||||||
|
require('dotenv').config();
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
|
||||||
|
// Настройка i18n
|
||||||
|
i18n.configure({
|
||||||
|
locales: ['ko', 'en', 'ru', 'kk'],
|
||||||
|
defaultLocale: 'ru',
|
||||||
|
directory: path.join(__dirname, 'locales'),
|
||||||
|
objectNotation: true,
|
||||||
|
updateFiles: false,
|
||||||
|
syncFiles: false
|
||||||
|
});
|
||||||
|
|
||||||
|
// i18n middleware
|
||||||
|
app.use(i18n.init);
|
||||||
|
|
||||||
|
// Security middleware
|
||||||
|
app.use(helmet({
|
||||||
|
contentSecurityPolicy: {
|
||||||
|
directives: {
|
||||||
|
defaultSrc: ["'self'"],
|
||||||
|
styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com", "https://cdnjs.cloudflare.com", "https://cdn.jsdelivr.net", "https://unpkg.com", "https://cdn.tailwindcss.com"],
|
||||||
|
fontSrc: ["'self'", "https://fonts.gstatic.com", "https://cdnjs.cloudflare.com"],
|
||||||
|
scriptSrc: ["'self'", "'unsafe-inline'", "https://cdnjs.cloudflare.com", "https://unpkg.com", "https://cdn.tailwindcss.com"],
|
||||||
|
imgSrc: ["'self'", "data:", "https:"],
|
||||||
|
connectSrc: ["'self'", "ws:", "wss:", "https://cdnjs.cloudflare.com", "https://cdn.jsdelivr.net", "https://unpkg.com", "https://fonts.googleapis.com", "https://fonts.gstatic.com", "https://cdn.tailwindcss.com"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Rate limiting
|
||||||
|
const limiter = rateLimit({
|
||||||
|
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||||
|
max: 100 // limit each IP to 100 requests per windowMs
|
||||||
|
});
|
||||||
|
app.use('/api/', limiter);
|
||||||
|
|
||||||
|
// Middleware
|
||||||
|
app.use(compression());
|
||||||
|
app.use(cors());
|
||||||
|
app.use(morgan('combined'));
|
||||||
|
app.use(express.json({ limit: '10mb' }));
|
||||||
|
app.use(express.urlencoded({ extended: true, limit: '10mb' }));
|
||||||
|
|
||||||
|
// Static files
|
||||||
|
app.use(express.static(path.join(__dirname, 'public')));
|
||||||
|
app.use('/uploads', express.static(path.join(__dirname, 'public/uploads')));
|
||||||
|
|
||||||
|
// AdminLTE static files
|
||||||
|
app.use('/node_modules', express.static(path.join(__dirname, 'node_modules')));
|
||||||
|
|
||||||
|
// View engine
|
||||||
|
app.set('view engine', 'ejs');
|
||||||
|
app.set('views', path.join(__dirname, 'views'));
|
||||||
|
|
||||||
|
// Layout engine
|
||||||
|
const expressLayouts = require('express-ejs-layouts');
|
||||||
|
app.use(expressLayouts);
|
||||||
|
app.set('layout', 'layout'); // Default layout for main site
|
||||||
|
app.set('layout extractScripts', true);
|
||||||
|
app.set('layout extractStyles', true);
|
||||||
|
|
||||||
|
// Database connection and testing
|
||||||
|
testConnection();
|
||||||
|
|
||||||
|
// Session store configuration
|
||||||
|
const sessionStore = new SequelizeStore({
|
||||||
|
db: sequelize,
|
||||||
|
tableName: 'sessions',
|
||||||
|
checkExpirationInterval: 15 * 60 * 1000, // 15 minutes
|
||||||
|
expiration: 7 * 24 * 60 * 60 * 1000 // 7 days
|
||||||
|
});
|
||||||
|
|
||||||
|
// Session configuration
|
||||||
|
app.use(session({
|
||||||
|
secret: process.env.SESSION_SECRET || 'your-secret-key',
|
||||||
|
resave: false,
|
||||||
|
saveUninitialized: false,
|
||||||
|
store: sessionStore,
|
||||||
|
cookie: {
|
||||||
|
secure: process.env.NODE_ENV === 'production',
|
||||||
|
httpOnly: true,
|
||||||
|
maxAge: 1000 * 60 * 60 * 24 * 7 // 7 days
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Middleware для передачи переменных в шаблоны
|
||||||
|
app.use((req, res, next) => {
|
||||||
|
const currentLang = req.session?.language || req.getLocale() || 'ru';
|
||||||
|
req.setLocale(currentLang);
|
||||||
|
|
||||||
|
res.locals.locale = currentLang;
|
||||||
|
res.locals.__ = res.__;
|
||||||
|
res.locals.theme = req.session?.theme || 'light';
|
||||||
|
res.locals.currentLanguage = currentLang;
|
||||||
|
res.locals.currentPage = req.path.split('/')[1] || 'home';
|
||||||
|
|
||||||
|
// Debug logging for theme
|
||||||
|
if (req.url !== '/sw.js' && !req.url.startsWith('/css/') && !req.url.startsWith('/js/') && !req.url.startsWith('/images/')) {
|
||||||
|
console.log(`Request URL: ${req.url}, Theme from session: ${req.session?.theme}, Setting theme to: ${res.locals.theme}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Устанавливаем заголовки для предотвращения кеширования языкового контента
|
||||||
|
if (!req.url.startsWith('/css/') && !req.url.startsWith('/js/') && !req.url.startsWith('/images/')) {
|
||||||
|
res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
|
||||||
|
res.setHeader('Pragma', 'no-cache');
|
||||||
|
res.setHeader('Expires', '0');
|
||||||
|
res.setHeader('Vary', 'Accept-Language');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Отладочная информация
|
||||||
|
console.log(`Request URL: ${req.url}, Current Language: ${currentLang}, Session Language: ${req.session?.language}, Locale: ${req.getLocale()}`);
|
||||||
|
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Routes
|
||||||
|
app.use('/', require('./routes/index'));
|
||||||
|
app.use('/api/auth', require('./routes/auth'));
|
||||||
|
app.use('/api/portfolio', require('./routes/portfolio'));
|
||||||
|
app.use('/api/services', require('./routes/services'));
|
||||||
|
app.use('/api/calculator', require('./routes/calculator'));
|
||||||
|
app.use('/api/contact', require('./routes/contact'));
|
||||||
|
app.use('/api/media', require('./routes/media'));
|
||||||
|
app.use('/api/admin', require('./routes/api/admin'));
|
||||||
|
app.use('/admin', require('./routes/admin'));
|
||||||
|
|
||||||
|
// Language switching routes
|
||||||
|
app.get('/lang/:language', (req, res) => {
|
||||||
|
const { language } = req.params;
|
||||||
|
const supportedLanguages = ['ko', 'en', 'ru', 'kk'];
|
||||||
|
|
||||||
|
if (supportedLanguages.includes(language)) {
|
||||||
|
req.setLocale(language);
|
||||||
|
req.session.language = language;
|
||||||
|
console.log(`Language switched to: ${language}, session language: ${req.session.language}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const referer = req.get('Referer') || '/';
|
||||||
|
res.redirect(referer);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Theme switching routes
|
||||||
|
app.get('/theme/:theme', (req, res) => {
|
||||||
|
const { theme } = req.params;
|
||||||
|
if (['light', 'dark'].includes(theme)) {
|
||||||
|
req.session.theme = theme;
|
||||||
|
}
|
||||||
|
res.json({ success: true, theme: req.session.theme });
|
||||||
|
});
|
||||||
|
|
||||||
|
// PWA Service Worker
|
||||||
|
app.get('/sw.js', (req, res) => {
|
||||||
|
res.sendFile(path.join(__dirname, 'public', 'sw.js'));
|
||||||
|
});
|
||||||
|
|
||||||
|
// PWA Manifest
|
||||||
|
app.get('/manifest.json', (req, res) => {
|
||||||
|
res.sendFile(path.join(__dirname, 'public', 'manifest.json'));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Error handling middleware
|
||||||
|
app.use((err, req, res, next) => {
|
||||||
|
console.error(err.stack);
|
||||||
|
res.status(500).render('error', {
|
||||||
|
title: 'Error',
|
||||||
|
settings: {},
|
||||||
|
message: process.env.NODE_ENV === 'production'
|
||||||
|
? 'Something went wrong!'
|
||||||
|
: err.message,
|
||||||
|
currentPage: 'error'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 404 handler
|
||||||
|
app.use((req, res) => {
|
||||||
|
res.status(404).render('error', {
|
||||||
|
title: '404 - 페이지를 찾을 수 없습니다',
|
||||||
|
settings: {},
|
||||||
|
message: '요청하신 페이지를 찾을 수 없습니다',
|
||||||
|
currentPage: 'error'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const PORT = process.env.PORT || 3000;
|
||||||
|
|
||||||
|
// Sync database and start server
|
||||||
|
async function startServer() {
|
||||||
|
try {
|
||||||
|
// Sync all models with database
|
||||||
|
await sequelize.sync({ force: false });
|
||||||
|
console.log('✓ Database synchronized');
|
||||||
|
|
||||||
|
// Create session table
|
||||||
|
await sessionStore.sync();
|
||||||
|
console.log('✓ Session store synchronized');
|
||||||
|
|
||||||
|
app.listen(PORT, () => {
|
||||||
|
console.log(`🚀 Server running on port ${PORT}`);
|
||||||
|
console.log(`🌐 Visit: http://localhost:${PORT}`);
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('✗ Failed to start server:', error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
startServer();
|
||||||
223
.history/server_20251026211521.js
Normal file
@@ -0,0 +1,223 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const { sequelize, testConnection } = require('./config/database');
|
||||||
|
const session = require('express-session');
|
||||||
|
const SequelizeStore = require('connect-session-sequelize')(session.Store);
|
||||||
|
const path = require('path');
|
||||||
|
const helmet = require('helmet');
|
||||||
|
const compression = require('compression');
|
||||||
|
const cors = require('cors');
|
||||||
|
const morgan = require('morgan');
|
||||||
|
const rateLimit = require('express-rate-limit');
|
||||||
|
const i18n = require('i18n');
|
||||||
|
require('dotenv').config();
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
|
||||||
|
// Настройка i18n
|
||||||
|
i18n.configure({
|
||||||
|
locales: ['ko', 'en', 'ru', 'kk'],
|
||||||
|
defaultLocale: 'ru',
|
||||||
|
directory: path.join(__dirname, 'locales'),
|
||||||
|
objectNotation: true,
|
||||||
|
updateFiles: false,
|
||||||
|
syncFiles: false
|
||||||
|
});
|
||||||
|
|
||||||
|
// i18n middleware
|
||||||
|
app.use(i18n.init);
|
||||||
|
|
||||||
|
// Security middleware
|
||||||
|
app.use(helmet({
|
||||||
|
contentSecurityPolicy: {
|
||||||
|
directives: {
|
||||||
|
defaultSrc: ["'self'"],
|
||||||
|
styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com", "https://cdnjs.cloudflare.com", "https://cdn.jsdelivr.net", "https://unpkg.com", "https://cdn.tailwindcss.com"],
|
||||||
|
fontSrc: ["'self'", "https://fonts.gstatic.com", "https://cdnjs.cloudflare.com"],
|
||||||
|
scriptSrc: ["'self'", "'unsafe-inline'", "https://cdnjs.cloudflare.com", "https://unpkg.com", "https://cdn.tailwindcss.com"],
|
||||||
|
imgSrc: ["'self'", "data:", "https:"],
|
||||||
|
connectSrc: ["'self'", "ws:", "wss:", "https://cdnjs.cloudflare.com", "https://cdn.jsdelivr.net", "https://unpkg.com", "https://fonts.googleapis.com", "https://fonts.gstatic.com", "https://cdn.tailwindcss.com"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Rate limiting
|
||||||
|
const limiter = rateLimit({
|
||||||
|
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||||
|
max: 100 // limit each IP to 100 requests per windowMs
|
||||||
|
});
|
||||||
|
app.use('/api/', limiter);
|
||||||
|
|
||||||
|
// Middleware
|
||||||
|
app.use(compression());
|
||||||
|
app.use(cors());
|
||||||
|
app.use(morgan('combined'));
|
||||||
|
app.use(express.json({ limit: '10mb' }));
|
||||||
|
app.use(express.urlencoded({ extended: true, limit: '10mb' }));
|
||||||
|
|
||||||
|
// Static files
|
||||||
|
app.use(express.static(path.join(__dirname, 'public')));
|
||||||
|
app.use('/uploads', express.static(path.join(__dirname, 'public/uploads')));
|
||||||
|
|
||||||
|
// AdminLTE static files
|
||||||
|
app.use('/node_modules', express.static(path.join(__dirname, 'node_modules')));
|
||||||
|
|
||||||
|
// View engine
|
||||||
|
app.set('view engine', 'ejs');
|
||||||
|
app.set('views', path.join(__dirname, 'views'));
|
||||||
|
|
||||||
|
// Layout engine
|
||||||
|
const expressLayouts = require('express-ejs-layouts');
|
||||||
|
app.use(expressLayouts);
|
||||||
|
app.set('layout', 'layout'); // Default layout for main site
|
||||||
|
app.set('layout extractScripts', true);
|
||||||
|
app.set('layout extractStyles', true);
|
||||||
|
|
||||||
|
// Database connection and testing
|
||||||
|
testConnection();
|
||||||
|
|
||||||
|
// Session store configuration
|
||||||
|
const sessionStore = new SequelizeStore({
|
||||||
|
db: sequelize,
|
||||||
|
tableName: 'sessions',
|
||||||
|
checkExpirationInterval: 15 * 60 * 1000, // 15 minutes
|
||||||
|
expiration: 7 * 24 * 60 * 60 * 1000 // 7 days
|
||||||
|
});
|
||||||
|
|
||||||
|
// Session configuration
|
||||||
|
app.use(session({
|
||||||
|
secret: process.env.SESSION_SECRET || 'your-secret-key',
|
||||||
|
resave: false,
|
||||||
|
saveUninitialized: false,
|
||||||
|
store: sessionStore,
|
||||||
|
cookie: {
|
||||||
|
secure: process.env.NODE_ENV === 'production',
|
||||||
|
httpOnly: true,
|
||||||
|
maxAge: 1000 * 60 * 60 * 24 * 7 // 7 days
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Middleware для передачи переменных в шаблоны
|
||||||
|
app.use((req, res, next) => {
|
||||||
|
const currentLang = req.session?.language || req.getLocale() || 'ru';
|
||||||
|
req.setLocale(currentLang);
|
||||||
|
|
||||||
|
res.locals.locale = currentLang;
|
||||||
|
res.locals.__ = res.__;
|
||||||
|
res.locals.theme = req.session?.theme || 'light';
|
||||||
|
res.locals.currentLanguage = currentLang;
|
||||||
|
res.locals.currentPage = req.path.split('/')[1] || 'home';
|
||||||
|
|
||||||
|
// Debug logging for theme
|
||||||
|
if (req.url !== '/sw.js' && !req.url.startsWith('/css/') && !req.url.startsWith('/js/') && !req.url.startsWith('/images/')) {
|
||||||
|
console.log(`Request URL: ${req.url}, Theme from session: ${req.session?.theme}, Setting theme to: ${res.locals.theme}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Устанавливаем заголовки для предотвращения кеширования языкового контента
|
||||||
|
if (!req.url.startsWith('/css/') && !req.url.startsWith('/js/') && !req.url.startsWith('/images/')) {
|
||||||
|
res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
|
||||||
|
res.setHeader('Pragma', 'no-cache');
|
||||||
|
res.setHeader('Expires', '0');
|
||||||
|
res.setHeader('Vary', 'Accept-Language');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Отладочная информация
|
||||||
|
console.log(`Request URL: ${req.url}, Current Language: ${currentLang}, Session Language: ${req.session?.language}, Locale: ${req.getLocale()}`);
|
||||||
|
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Routes
|
||||||
|
app.use('/', require('./routes/index'));
|
||||||
|
app.use('/api/auth', require('./routes/auth'));
|
||||||
|
app.use('/api/portfolio', require('./routes/portfolio'));
|
||||||
|
app.use('/api/services', require('./routes/services'));
|
||||||
|
app.use('/api/calculator', require('./routes/calculator'));
|
||||||
|
app.use('/api/contact', require('./routes/contact'));
|
||||||
|
app.use('/api/media', require('./routes/media'));
|
||||||
|
app.use('/api/admin', require('./routes/api/admin'));
|
||||||
|
app.use('/admin', require('./routes/admin'));
|
||||||
|
|
||||||
|
// AdminLTE Demo route
|
||||||
|
app.use('/demo', require('./routes/demo-adminlte'));
|
||||||
|
|
||||||
|
// Language switching routes
|
||||||
|
app.get('/lang/:language', (req, res) => {
|
||||||
|
const { language } = req.params;
|
||||||
|
const supportedLanguages = ['ko', 'en', 'ru', 'kk'];
|
||||||
|
|
||||||
|
if (supportedLanguages.includes(language)) {
|
||||||
|
req.setLocale(language);
|
||||||
|
req.session.language = language;
|
||||||
|
console.log(`Language switched to: ${language}, session language: ${req.session.language}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const referer = req.get('Referer') || '/';
|
||||||
|
res.redirect(referer);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Theme switching routes
|
||||||
|
app.get('/theme/:theme', (req, res) => {
|
||||||
|
const { theme } = req.params;
|
||||||
|
if (['light', 'dark'].includes(theme)) {
|
||||||
|
req.session.theme = theme;
|
||||||
|
}
|
||||||
|
res.json({ success: true, theme: req.session.theme });
|
||||||
|
});
|
||||||
|
|
||||||
|
// PWA Service Worker
|
||||||
|
app.get('/sw.js', (req, res) => {
|
||||||
|
res.sendFile(path.join(__dirname, 'public', 'sw.js'));
|
||||||
|
});
|
||||||
|
|
||||||
|
// PWA Manifest
|
||||||
|
app.get('/manifest.json', (req, res) => {
|
||||||
|
res.sendFile(path.join(__dirname, 'public', 'manifest.json'));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Error handling middleware
|
||||||
|
app.use((err, req, res, next) => {
|
||||||
|
console.error(err.stack);
|
||||||
|
res.status(500).render('error', {
|
||||||
|
title: 'Error',
|
||||||
|
settings: {},
|
||||||
|
message: process.env.NODE_ENV === 'production'
|
||||||
|
? 'Something went wrong!'
|
||||||
|
: err.message,
|
||||||
|
currentPage: 'error'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 404 handler
|
||||||
|
app.use((req, res) => {
|
||||||
|
res.status(404).render('error', {
|
||||||
|
title: '404 - 페이지를 찾을 수 없습니다',
|
||||||
|
settings: {},
|
||||||
|
message: '요청하신 페이지를 찾을 수 없습니다',
|
||||||
|
currentPage: 'error'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const PORT = process.env.PORT || 3000;
|
||||||
|
|
||||||
|
// Sync database and start server
|
||||||
|
async function startServer() {
|
||||||
|
try {
|
||||||
|
// Sync all models with database
|
||||||
|
await sequelize.sync({ force: false });
|
||||||
|
console.log('✓ Database synchronized');
|
||||||
|
|
||||||
|
// Create session table
|
||||||
|
await sessionStore.sync();
|
||||||
|
console.log('✓ Session store synchronized');
|
||||||
|
|
||||||
|
app.listen(PORT, () => {
|
||||||
|
console.log(`🚀 Server running on port ${PORT}`);
|
||||||
|
console.log(`🌐 Visit: http://localhost:${PORT}`);
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('✗ Failed to start server:', error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
startServer();
|
||||||
223
.history/server_20251026211716.js
Normal file
@@ -0,0 +1,223 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const { sequelize, testConnection } = require('./config/database');
|
||||||
|
const session = require('express-session');
|
||||||
|
const SequelizeStore = require('connect-session-sequelize')(session.Store);
|
||||||
|
const path = require('path');
|
||||||
|
const helmet = require('helmet');
|
||||||
|
const compression = require('compression');
|
||||||
|
const cors = require('cors');
|
||||||
|
const morgan = require('morgan');
|
||||||
|
const rateLimit = require('express-rate-limit');
|
||||||
|
const i18n = require('i18n');
|
||||||
|
require('dotenv').config();
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
|
||||||
|
// Настройка i18n
|
||||||
|
i18n.configure({
|
||||||
|
locales: ['ko', 'en', 'ru', 'kk'],
|
||||||
|
defaultLocale: 'ru',
|
||||||
|
directory: path.join(__dirname, 'locales'),
|
||||||
|
objectNotation: true,
|
||||||
|
updateFiles: false,
|
||||||
|
syncFiles: false
|
||||||
|
});
|
||||||
|
|
||||||
|
// i18n middleware
|
||||||
|
app.use(i18n.init);
|
||||||
|
|
||||||
|
// Security middleware
|
||||||
|
app.use(helmet({
|
||||||
|
contentSecurityPolicy: {
|
||||||
|
directives: {
|
||||||
|
defaultSrc: ["'self'"],
|
||||||
|
styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com", "https://cdnjs.cloudflare.com", "https://cdn.jsdelivr.net", "https://unpkg.com", "https://cdn.tailwindcss.com"],
|
||||||
|
fontSrc: ["'self'", "https://fonts.gstatic.com", "https://cdnjs.cloudflare.com"],
|
||||||
|
scriptSrc: ["'self'", "'unsafe-inline'", "https://cdnjs.cloudflare.com", "https://unpkg.com", "https://cdn.tailwindcss.com"],
|
||||||
|
imgSrc: ["'self'", "data:", "https:"],
|
||||||
|
connectSrc: ["'self'", "ws:", "wss:", "https://cdnjs.cloudflare.com", "https://cdn.jsdelivr.net", "https://unpkg.com", "https://fonts.googleapis.com", "https://fonts.gstatic.com", "https://cdn.tailwindcss.com"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Rate limiting
|
||||||
|
const limiter = rateLimit({
|
||||||
|
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||||
|
max: 100 // limit each IP to 100 requests per windowMs
|
||||||
|
});
|
||||||
|
app.use('/api/', limiter);
|
||||||
|
|
||||||
|
// Middleware
|
||||||
|
app.use(compression());
|
||||||
|
app.use(cors());
|
||||||
|
app.use(morgan('combined'));
|
||||||
|
app.use(express.json({ limit: '10mb' }));
|
||||||
|
app.use(express.urlencoded({ extended: true, limit: '10mb' }));
|
||||||
|
|
||||||
|
// Static files
|
||||||
|
app.use(express.static(path.join(__dirname, 'public')));
|
||||||
|
app.use('/uploads', express.static(path.join(__dirname, 'public/uploads')));
|
||||||
|
|
||||||
|
// AdminLTE static files
|
||||||
|
app.use('/node_modules', express.static(path.join(__dirname, 'node_modules')));
|
||||||
|
|
||||||
|
// View engine
|
||||||
|
app.set('view engine', 'ejs');
|
||||||
|
app.set('views', path.join(__dirname, 'views'));
|
||||||
|
|
||||||
|
// Layout engine
|
||||||
|
const expressLayouts = require('express-ejs-layouts');
|
||||||
|
app.use(expressLayouts);
|
||||||
|
app.set('layout', 'layout'); // Default layout for main site
|
||||||
|
app.set('layout extractScripts', true);
|
||||||
|
app.set('layout extractStyles', true);
|
||||||
|
|
||||||
|
// Database connection and testing
|
||||||
|
testConnection();
|
||||||
|
|
||||||
|
// Session store configuration
|
||||||
|
const sessionStore = new SequelizeStore({
|
||||||
|
db: sequelize,
|
||||||
|
tableName: 'sessions',
|
||||||
|
checkExpirationInterval: 15 * 60 * 1000, // 15 minutes
|
||||||
|
expiration: 7 * 24 * 60 * 60 * 1000 // 7 days
|
||||||
|
});
|
||||||
|
|
||||||
|
// Session configuration
|
||||||
|
app.use(session({
|
||||||
|
secret: process.env.SESSION_SECRET || 'your-secret-key',
|
||||||
|
resave: false,
|
||||||
|
saveUninitialized: false,
|
||||||
|
store: sessionStore,
|
||||||
|
cookie: {
|
||||||
|
secure: process.env.NODE_ENV === 'production',
|
||||||
|
httpOnly: true,
|
||||||
|
maxAge: 1000 * 60 * 60 * 24 * 7 // 7 days
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Middleware для передачи переменных в шаблоны
|
||||||
|
app.use((req, res, next) => {
|
||||||
|
const currentLang = req.session?.language || req.getLocale() || 'ru';
|
||||||
|
req.setLocale(currentLang);
|
||||||
|
|
||||||
|
res.locals.locale = currentLang;
|
||||||
|
res.locals.__ = res.__;
|
||||||
|
res.locals.theme = req.session?.theme || 'light';
|
||||||
|
res.locals.currentLanguage = currentLang;
|
||||||
|
res.locals.currentPage = req.path.split('/')[1] || 'home';
|
||||||
|
|
||||||
|
// Debug logging for theme
|
||||||
|
if (req.url !== '/sw.js' && !req.url.startsWith('/css/') && !req.url.startsWith('/js/') && !req.url.startsWith('/images/')) {
|
||||||
|
console.log(`Request URL: ${req.url}, Theme from session: ${req.session?.theme}, Setting theme to: ${res.locals.theme}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Устанавливаем заголовки для предотвращения кеширования языкового контента
|
||||||
|
if (!req.url.startsWith('/css/') && !req.url.startsWith('/js/') && !req.url.startsWith('/images/')) {
|
||||||
|
res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
|
||||||
|
res.setHeader('Pragma', 'no-cache');
|
||||||
|
res.setHeader('Expires', '0');
|
||||||
|
res.setHeader('Vary', 'Accept-Language');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Отладочная информация
|
||||||
|
console.log(`Request URL: ${req.url}, Current Language: ${currentLang}, Session Language: ${req.session?.language}, Locale: ${req.getLocale()}`);
|
||||||
|
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Routes
|
||||||
|
app.use('/', require('./routes/index'));
|
||||||
|
app.use('/api/auth', require('./routes/auth'));
|
||||||
|
app.use('/api/portfolio', require('./routes/portfolio'));
|
||||||
|
app.use('/api/services', require('./routes/services'));
|
||||||
|
app.use('/api/calculator', require('./routes/calculator'));
|
||||||
|
app.use('/api/contact', require('./routes/contact'));
|
||||||
|
app.use('/api/media', require('./routes/media'));
|
||||||
|
app.use('/api/admin', require('./routes/api/admin'));
|
||||||
|
app.use('/admin', require('./routes/admin'));
|
||||||
|
|
||||||
|
// AdminLTE Demo route
|
||||||
|
app.use('/demo', require('./routes/demo-adminlte'));
|
||||||
|
|
||||||
|
// Language switching routes
|
||||||
|
app.get('/lang/:language', (req, res) => {
|
||||||
|
const { language } = req.params;
|
||||||
|
const supportedLanguages = ['ko', 'en', 'ru', 'kk'];
|
||||||
|
|
||||||
|
if (supportedLanguages.includes(language)) {
|
||||||
|
req.setLocale(language);
|
||||||
|
req.session.language = language;
|
||||||
|
console.log(`Language switched to: ${language}, session language: ${req.session.language}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const referer = req.get('Referer') || '/';
|
||||||
|
res.redirect(referer);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Theme switching routes
|
||||||
|
app.get('/theme/:theme', (req, res) => {
|
||||||
|
const { theme } = req.params;
|
||||||
|
if (['light', 'dark'].includes(theme)) {
|
||||||
|
req.session.theme = theme;
|
||||||
|
}
|
||||||
|
res.json({ success: true, theme: req.session.theme });
|
||||||
|
});
|
||||||
|
|
||||||
|
// PWA Service Worker
|
||||||
|
app.get('/sw.js', (req, res) => {
|
||||||
|
res.sendFile(path.join(__dirname, 'public', 'sw.js'));
|
||||||
|
});
|
||||||
|
|
||||||
|
// PWA Manifest
|
||||||
|
app.get('/manifest.json', (req, res) => {
|
||||||
|
res.sendFile(path.join(__dirname, 'public', 'manifest.json'));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Error handling middleware
|
||||||
|
app.use((err, req, res, next) => {
|
||||||
|
console.error(err.stack);
|
||||||
|
res.status(500).render('error', {
|
||||||
|
title: 'Error',
|
||||||
|
settings: {},
|
||||||
|
message: process.env.NODE_ENV === 'production'
|
||||||
|
? 'Something went wrong!'
|
||||||
|
: err.message,
|
||||||
|
currentPage: 'error'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 404 handler
|
||||||
|
app.use((req, res) => {
|
||||||
|
res.status(404).render('error', {
|
||||||
|
title: '404 - 페이지를 찾을 수 없습니다',
|
||||||
|
settings: {},
|
||||||
|
message: '요청하신 페이지를 찾을 수 없습니다',
|
||||||
|
currentPage: 'error'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const PORT = process.env.PORT || 3000;
|
||||||
|
|
||||||
|
// Sync database and start server
|
||||||
|
async function startServer() {
|
||||||
|
try {
|
||||||
|
// Sync all models with database
|
||||||
|
await sequelize.sync({ force: false });
|
||||||
|
console.log('✓ Database synchronized');
|
||||||
|
|
||||||
|
// Create session table
|
||||||
|
await sessionStore.sync();
|
||||||
|
console.log('✓ Session store synchronized');
|
||||||
|
|
||||||
|
app.listen(PORT, () => {
|
||||||
|
console.log(`🚀 Server running on port ${PORT}`);
|
||||||
|
console.log(`🌐 Visit: http://localhost:${PORT}`);
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('✗ Failed to start server:', error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
startServer();
|
||||||
220
.history/server_20251026212324.js
Normal file
@@ -0,0 +1,220 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const { sequelize, testConnection } = require('./config/database');
|
||||||
|
const session = require('express-session');
|
||||||
|
const SequelizeStore = require('connect-session-sequelize')(session.Store);
|
||||||
|
const path = require('path');
|
||||||
|
const helmet = require('helmet');
|
||||||
|
const compression = require('compression');
|
||||||
|
const cors = require('cors');
|
||||||
|
const morgan = require('morgan');
|
||||||
|
const rateLimit = require('express-rate-limit');
|
||||||
|
const i18n = require('i18n');
|
||||||
|
require('dotenv').config();
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
|
||||||
|
// Настройка i18n
|
||||||
|
i18n.configure({
|
||||||
|
locales: ['ko', 'en', 'ru', 'kk'],
|
||||||
|
defaultLocale: 'ru',
|
||||||
|
directory: path.join(__dirname, 'locales'),
|
||||||
|
objectNotation: true,
|
||||||
|
updateFiles: false,
|
||||||
|
syncFiles: false
|
||||||
|
});
|
||||||
|
|
||||||
|
// i18n middleware
|
||||||
|
app.use(i18n.init);
|
||||||
|
|
||||||
|
// Security middleware
|
||||||
|
app.use(helmet({
|
||||||
|
contentSecurityPolicy: {
|
||||||
|
directives: {
|
||||||
|
defaultSrc: ["'self'"],
|
||||||
|
styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com", "https://cdnjs.cloudflare.com", "https://cdn.jsdelivr.net", "https://unpkg.com", "https://cdn.tailwindcss.com"],
|
||||||
|
fontSrc: ["'self'", "https://fonts.gstatic.com", "https://cdnjs.cloudflare.com"],
|
||||||
|
scriptSrc: ["'self'", "'unsafe-inline'", "https://cdnjs.cloudflare.com", "https://unpkg.com", "https://cdn.tailwindcss.com"],
|
||||||
|
imgSrc: ["'self'", "data:", "https:"],
|
||||||
|
connectSrc: ["'self'", "ws:", "wss:", "https://cdnjs.cloudflare.com", "https://cdn.jsdelivr.net", "https://unpkg.com", "https://fonts.googleapis.com", "https://fonts.gstatic.com", "https://cdn.tailwindcss.com"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Rate limiting
|
||||||
|
const limiter = rateLimit({
|
||||||
|
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||||
|
max: 100 // limit each IP to 100 requests per windowMs
|
||||||
|
});
|
||||||
|
app.use('/api/', limiter);
|
||||||
|
|
||||||
|
// Middleware
|
||||||
|
app.use(compression());
|
||||||
|
app.use(cors());
|
||||||
|
app.use(morgan('combined'));
|
||||||
|
app.use(express.json({ limit: '10mb' }));
|
||||||
|
app.use(express.urlencoded({ extended: true, limit: '10mb' }));
|
||||||
|
|
||||||
|
// Static files
|
||||||
|
app.use(express.static(path.join(__dirname, 'public')));
|
||||||
|
app.use('/uploads', express.static(path.join(__dirname, 'public/uploads')));
|
||||||
|
|
||||||
|
// AdminLTE static files
|
||||||
|
app.use('/node_modules', express.static(path.join(__dirname, 'node_modules')));
|
||||||
|
|
||||||
|
// View engine
|
||||||
|
app.set('view engine', 'ejs');
|
||||||
|
app.set('views', path.join(__dirname, 'views'));
|
||||||
|
|
||||||
|
// Layout engine
|
||||||
|
const expressLayouts = require('express-ejs-layouts');
|
||||||
|
app.use(expressLayouts);
|
||||||
|
app.set('layout', 'layout'); // Default layout for main site
|
||||||
|
app.set('layout extractScripts', true);
|
||||||
|
app.set('layout extractStyles', true);
|
||||||
|
|
||||||
|
// Database connection and testing
|
||||||
|
testConnection();
|
||||||
|
|
||||||
|
// Session store configuration
|
||||||
|
const sessionStore = new SequelizeStore({
|
||||||
|
db: sequelize,
|
||||||
|
tableName: 'sessions',
|
||||||
|
checkExpirationInterval: 15 * 60 * 1000, // 15 minutes
|
||||||
|
expiration: 7 * 24 * 60 * 60 * 1000 // 7 days
|
||||||
|
});
|
||||||
|
|
||||||
|
// Session configuration
|
||||||
|
app.use(session({
|
||||||
|
secret: process.env.SESSION_SECRET || 'your-secret-key',
|
||||||
|
resave: false,
|
||||||
|
saveUninitialized: false,
|
||||||
|
store: sessionStore,
|
||||||
|
cookie: {
|
||||||
|
secure: process.env.NODE_ENV === 'production',
|
||||||
|
httpOnly: true,
|
||||||
|
maxAge: 1000 * 60 * 60 * 24 * 7 // 7 days
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Middleware для передачи переменных в шаблоны
|
||||||
|
app.use((req, res, next) => {
|
||||||
|
const currentLang = req.session?.language || req.getLocale() || 'ru';
|
||||||
|
req.setLocale(currentLang);
|
||||||
|
|
||||||
|
res.locals.locale = currentLang;
|
||||||
|
res.locals.__ = res.__;
|
||||||
|
res.locals.theme = req.session?.theme || 'light';
|
||||||
|
res.locals.currentLanguage = currentLang;
|
||||||
|
res.locals.currentPage = req.path.split('/')[1] || 'home';
|
||||||
|
|
||||||
|
// Debug logging for theme
|
||||||
|
if (req.url !== '/sw.js' && !req.url.startsWith('/css/') && !req.url.startsWith('/js/') && !req.url.startsWith('/images/')) {
|
||||||
|
console.log(`Request URL: ${req.url}, Theme from session: ${req.session?.theme}, Setting theme to: ${res.locals.theme}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Устанавливаем заголовки для предотвращения кеширования языкового контента
|
||||||
|
if (!req.url.startsWith('/css/') && !req.url.startsWith('/js/') && !req.url.startsWith('/images/')) {
|
||||||
|
res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
|
||||||
|
res.setHeader('Pragma', 'no-cache');
|
||||||
|
res.setHeader('Expires', '0');
|
||||||
|
res.setHeader('Vary', 'Accept-Language');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Отладочная информация
|
||||||
|
console.log(`Request URL: ${req.url}, Current Language: ${currentLang}, Session Language: ${req.session?.language}, Locale: ${req.getLocale()}`);
|
||||||
|
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Routes
|
||||||
|
app.use('/', require('./routes/index'));
|
||||||
|
app.use('/api/auth', require('./routes/auth'));
|
||||||
|
app.use('/api/portfolio', require('./routes/portfolio'));
|
||||||
|
app.use('/api/services', require('./routes/services'));
|
||||||
|
app.use('/api/calculator', require('./routes/calculator'));
|
||||||
|
app.use('/api/contact', require('./routes/contact'));
|
||||||
|
app.use('/api/media', require('./routes/media'));
|
||||||
|
app.use('/api/admin', require('./routes/api/admin'));
|
||||||
|
app.use('/admin', require('./routes/admin'));
|
||||||
|
|
||||||
|
// Language switching routes
|
||||||
|
app.get('/lang/:language', (req, res) => {
|
||||||
|
const { language } = req.params;
|
||||||
|
const supportedLanguages = ['ko', 'en', 'ru', 'kk'];
|
||||||
|
|
||||||
|
if (supportedLanguages.includes(language)) {
|
||||||
|
req.setLocale(language);
|
||||||
|
req.session.language = language;
|
||||||
|
console.log(`Language switched to: ${language}, session language: ${req.session.language}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const referer = req.get('Referer') || '/';
|
||||||
|
res.redirect(referer);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Theme switching routes
|
||||||
|
app.get('/theme/:theme', (req, res) => {
|
||||||
|
const { theme } = req.params;
|
||||||
|
if (['light', 'dark'].includes(theme)) {
|
||||||
|
req.session.theme = theme;
|
||||||
|
}
|
||||||
|
res.json({ success: true, theme: req.session.theme });
|
||||||
|
});
|
||||||
|
|
||||||
|
// PWA Service Worker
|
||||||
|
app.get('/sw.js', (req, res) => {
|
||||||
|
res.sendFile(path.join(__dirname, 'public', 'sw.js'));
|
||||||
|
});
|
||||||
|
|
||||||
|
// PWA Manifest
|
||||||
|
app.get('/manifest.json', (req, res) => {
|
||||||
|
res.sendFile(path.join(__dirname, 'public', 'manifest.json'));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Error handling middleware
|
||||||
|
app.use((err, req, res, next) => {
|
||||||
|
console.error(err.stack);
|
||||||
|
res.status(500).render('error', {
|
||||||
|
title: 'Error',
|
||||||
|
settings: {},
|
||||||
|
message: process.env.NODE_ENV === 'production'
|
||||||
|
? 'Something went wrong!'
|
||||||
|
: err.message,
|
||||||
|
currentPage: 'error'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 404 handler
|
||||||
|
app.use((req, res) => {
|
||||||
|
res.status(404).render('error', {
|
||||||
|
title: '404 - 페이지를 찾을 수 없습니다',
|
||||||
|
settings: {},
|
||||||
|
message: '요청하신 페이지를 찾을 수 없습니다',
|
||||||
|
currentPage: 'error'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const PORT = process.env.PORT || 3000;
|
||||||
|
|
||||||
|
// Sync database and start server
|
||||||
|
async function startServer() {
|
||||||
|
try {
|
||||||
|
// Sync all models with database
|
||||||
|
await sequelize.sync({ force: false });
|
||||||
|
console.log('✓ Database synchronized');
|
||||||
|
|
||||||
|
// Create session table
|
||||||
|
await sessionStore.sync();
|
||||||
|
console.log('✓ Session store synchronized');
|
||||||
|
|
||||||
|
app.listen(PORT, () => {
|
||||||
|
console.log(`🚀 Server running on port ${PORT}`);
|
||||||
|
console.log(`🌐 Visit: http://localhost:${PORT}`);
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('✗ Failed to start server:', error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
startServer();
|
||||||
220
.history/server_20251026212427.js
Normal file
@@ -0,0 +1,220 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const { sequelize, testConnection } = require('./config/database');
|
||||||
|
const session = require('express-session');
|
||||||
|
const SequelizeStore = require('connect-session-sequelize')(session.Store);
|
||||||
|
const path = require('path');
|
||||||
|
const helmet = require('helmet');
|
||||||
|
const compression = require('compression');
|
||||||
|
const cors = require('cors');
|
||||||
|
const morgan = require('morgan');
|
||||||
|
const rateLimit = require('express-rate-limit');
|
||||||
|
const i18n = require('i18n');
|
||||||
|
require('dotenv').config();
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
|
||||||
|
// Настройка i18n
|
||||||
|
i18n.configure({
|
||||||
|
locales: ['ko', 'en', 'ru', 'kk'],
|
||||||
|
defaultLocale: 'ru',
|
||||||
|
directory: path.join(__dirname, 'locales'),
|
||||||
|
objectNotation: true,
|
||||||
|
updateFiles: false,
|
||||||
|
syncFiles: false
|
||||||
|
});
|
||||||
|
|
||||||
|
// i18n middleware
|
||||||
|
app.use(i18n.init);
|
||||||
|
|
||||||
|
// Security middleware
|
||||||
|
app.use(helmet({
|
||||||
|
contentSecurityPolicy: {
|
||||||
|
directives: {
|
||||||
|
defaultSrc: ["'self'"],
|
||||||
|
styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com", "https://cdnjs.cloudflare.com", "https://cdn.jsdelivr.net", "https://unpkg.com", "https://cdn.tailwindcss.com"],
|
||||||
|
fontSrc: ["'self'", "https://fonts.gstatic.com", "https://cdnjs.cloudflare.com"],
|
||||||
|
scriptSrc: ["'self'", "'unsafe-inline'", "https://cdnjs.cloudflare.com", "https://unpkg.com", "https://cdn.tailwindcss.com"],
|
||||||
|
imgSrc: ["'self'", "data:", "https:"],
|
||||||
|
connectSrc: ["'self'", "ws:", "wss:", "https://cdnjs.cloudflare.com", "https://cdn.jsdelivr.net", "https://unpkg.com", "https://fonts.googleapis.com", "https://fonts.gstatic.com", "https://cdn.tailwindcss.com"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Rate limiting
|
||||||
|
const limiter = rateLimit({
|
||||||
|
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||||
|
max: 100 // limit each IP to 100 requests per windowMs
|
||||||
|
});
|
||||||
|
app.use('/api/', limiter);
|
||||||
|
|
||||||
|
// Middleware
|
||||||
|
app.use(compression());
|
||||||
|
app.use(cors());
|
||||||
|
app.use(morgan('combined'));
|
||||||
|
app.use(express.json({ limit: '10mb' }));
|
||||||
|
app.use(express.urlencoded({ extended: true, limit: '10mb' }));
|
||||||
|
|
||||||
|
// Static files
|
||||||
|
app.use(express.static(path.join(__dirname, 'public')));
|
||||||
|
app.use('/uploads', express.static(path.join(__dirname, 'public/uploads')));
|
||||||
|
|
||||||
|
// AdminLTE static files
|
||||||
|
app.use('/node_modules', express.static(path.join(__dirname, 'node_modules')));
|
||||||
|
|
||||||
|
// View engine
|
||||||
|
app.set('view engine', 'ejs');
|
||||||
|
app.set('views', path.join(__dirname, 'views'));
|
||||||
|
|
||||||
|
// Layout engine
|
||||||
|
const expressLayouts = require('express-ejs-layouts');
|
||||||
|
app.use(expressLayouts);
|
||||||
|
app.set('layout', 'layout'); // Default layout for main site
|
||||||
|
app.set('layout extractScripts', true);
|
||||||
|
app.set('layout extractStyles', true);
|
||||||
|
|
||||||
|
// Database connection and testing
|
||||||
|
testConnection();
|
||||||
|
|
||||||
|
// Session store configuration
|
||||||
|
const sessionStore = new SequelizeStore({
|
||||||
|
db: sequelize,
|
||||||
|
tableName: 'sessions',
|
||||||
|
checkExpirationInterval: 15 * 60 * 1000, // 15 minutes
|
||||||
|
expiration: 7 * 24 * 60 * 60 * 1000 // 7 days
|
||||||
|
});
|
||||||
|
|
||||||
|
// Session configuration
|
||||||
|
app.use(session({
|
||||||
|
secret: process.env.SESSION_SECRET || 'your-secret-key',
|
||||||
|
resave: false,
|
||||||
|
saveUninitialized: false,
|
||||||
|
store: sessionStore,
|
||||||
|
cookie: {
|
||||||
|
secure: process.env.NODE_ENV === 'production',
|
||||||
|
httpOnly: true,
|
||||||
|
maxAge: 1000 * 60 * 60 * 24 * 7 // 7 days
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Middleware для передачи переменных в шаблоны
|
||||||
|
app.use((req, res, next) => {
|
||||||
|
const currentLang = req.session?.language || req.getLocale() || 'ru';
|
||||||
|
req.setLocale(currentLang);
|
||||||
|
|
||||||
|
res.locals.locale = currentLang;
|
||||||
|
res.locals.__ = res.__;
|
||||||
|
res.locals.theme = req.session?.theme || 'light';
|
||||||
|
res.locals.currentLanguage = currentLang;
|
||||||
|
res.locals.currentPage = req.path.split('/')[1] || 'home';
|
||||||
|
|
||||||
|
// Debug logging for theme
|
||||||
|
if (req.url !== '/sw.js' && !req.url.startsWith('/css/') && !req.url.startsWith('/js/') && !req.url.startsWith('/images/')) {
|
||||||
|
console.log(`Request URL: ${req.url}, Theme from session: ${req.session?.theme}, Setting theme to: ${res.locals.theme}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Устанавливаем заголовки для предотвращения кеширования языкового контента
|
||||||
|
if (!req.url.startsWith('/css/') && !req.url.startsWith('/js/') && !req.url.startsWith('/images/')) {
|
||||||
|
res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
|
||||||
|
res.setHeader('Pragma', 'no-cache');
|
||||||
|
res.setHeader('Expires', '0');
|
||||||
|
res.setHeader('Vary', 'Accept-Language');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Отладочная информация
|
||||||
|
console.log(`Request URL: ${req.url}, Current Language: ${currentLang}, Session Language: ${req.session?.language}, Locale: ${req.getLocale()}`);
|
||||||
|
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Routes
|
||||||
|
app.use('/', require('./routes/index'));
|
||||||
|
app.use('/api/auth', require('./routes/auth'));
|
||||||
|
app.use('/api/portfolio', require('./routes/portfolio'));
|
||||||
|
app.use('/api/services', require('./routes/services'));
|
||||||
|
app.use('/api/calculator', require('./routes/calculator'));
|
||||||
|
app.use('/api/contact', require('./routes/contact'));
|
||||||
|
app.use('/api/media', require('./routes/media'));
|
||||||
|
app.use('/api/admin', require('./routes/api/admin'));
|
||||||
|
app.use('/admin', require('./routes/admin'));
|
||||||
|
|
||||||
|
// Language switching routes
|
||||||
|
app.get('/lang/:language', (req, res) => {
|
||||||
|
const { language } = req.params;
|
||||||
|
const supportedLanguages = ['ko', 'en', 'ru', 'kk'];
|
||||||
|
|
||||||
|
if (supportedLanguages.includes(language)) {
|
||||||
|
req.setLocale(language);
|
||||||
|
req.session.language = language;
|
||||||
|
console.log(`Language switched to: ${language}, session language: ${req.session.language}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const referer = req.get('Referer') || '/';
|
||||||
|
res.redirect(referer);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Theme switching routes
|
||||||
|
app.get('/theme/:theme', (req, res) => {
|
||||||
|
const { theme } = req.params;
|
||||||
|
if (['light', 'dark'].includes(theme)) {
|
||||||
|
req.session.theme = theme;
|
||||||
|
}
|
||||||
|
res.json({ success: true, theme: req.session.theme });
|
||||||
|
});
|
||||||
|
|
||||||
|
// PWA Service Worker
|
||||||
|
app.get('/sw.js', (req, res) => {
|
||||||
|
res.sendFile(path.join(__dirname, 'public', 'sw.js'));
|
||||||
|
});
|
||||||
|
|
||||||
|
// PWA Manifest
|
||||||
|
app.get('/manifest.json', (req, res) => {
|
||||||
|
res.sendFile(path.join(__dirname, 'public', 'manifest.json'));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Error handling middleware
|
||||||
|
app.use((err, req, res, next) => {
|
||||||
|
console.error(err.stack);
|
||||||
|
res.status(500).render('error', {
|
||||||
|
title: 'Error',
|
||||||
|
settings: {},
|
||||||
|
message: process.env.NODE_ENV === 'production'
|
||||||
|
? 'Something went wrong!'
|
||||||
|
: err.message,
|
||||||
|
currentPage: 'error'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 404 handler
|
||||||
|
app.use((req, res) => {
|
||||||
|
res.status(404).render('error', {
|
||||||
|
title: '404 - 페이지를 찾을 수 없습니다',
|
||||||
|
settings: {},
|
||||||
|
message: '요청하신 페이지를 찾을 수 없습니다',
|
||||||
|
currentPage: 'error'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const PORT = process.env.PORT || 3000;
|
||||||
|
|
||||||
|
// Sync database and start server
|
||||||
|
async function startServer() {
|
||||||
|
try {
|
||||||
|
// Sync all models with database
|
||||||
|
await sequelize.sync({ force: false });
|
||||||
|
console.log('✓ Database synchronized');
|
||||||
|
|
||||||
|
// Create session table
|
||||||
|
await sessionStore.sync();
|
||||||
|
console.log('✓ Session store synchronized');
|
||||||
|
|
||||||
|
app.listen(PORT, () => {
|
||||||
|
console.log(`🚀 Server running on port ${PORT}`);
|
||||||
|
console.log(`🌐 Visit: http://localhost:${PORT}`);
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('✗ Failed to start server:', error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
startServer();
|
||||||
220
.history/server_20251026213505.js
Normal file
@@ -0,0 +1,220 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const { sequelize, testConnection } = require('./config/database');
|
||||||
|
const session = require('express-session');
|
||||||
|
const SequelizeStore = require('connect-session-sequelize')(session.Store);
|
||||||
|
const path = require('path');
|
||||||
|
const helmet = require('helmet');
|
||||||
|
const compression = require('compression');
|
||||||
|
const cors = require('cors');
|
||||||
|
const morgan = require('morgan');
|
||||||
|
const rateLimit = require('express-rate-limit');
|
||||||
|
const i18n = require('i18n');
|
||||||
|
require('dotenv').config();
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
|
||||||
|
// Настройка i18n
|
||||||
|
i18n.configure({
|
||||||
|
locales: ['ko', 'en', 'ru', 'kk'],
|
||||||
|
defaultLocale: 'ru',
|
||||||
|
directory: path.join(__dirname, 'locales'),
|
||||||
|
objectNotation: true,
|
||||||
|
updateFiles: false,
|
||||||
|
syncFiles: false
|
||||||
|
});
|
||||||
|
|
||||||
|
// i18n middleware
|
||||||
|
app.use(i18n.init);
|
||||||
|
|
||||||
|
// Security middleware
|
||||||
|
app.use(helmet({
|
||||||
|
contentSecurityPolicy: {
|
||||||
|
directives: {
|
||||||
|
defaultSrc: ["'self'"],
|
||||||
|
styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com", "https://cdnjs.cloudflare.com", "https://cdn.jsdelivr.net", "https://unpkg.com", "https://cdn.tailwindcss.com"],
|
||||||
|
fontSrc: ["'self'", "https://fonts.gstatic.com", "https://cdnjs.cloudflare.com"],
|
||||||
|
scriptSrc: ["'self'", "'unsafe-inline'", "https://cdnjs.cloudflare.com", "https://unpkg.com", "https://cdn.tailwindcss.com", "https://code.jquery.com", "https://cdn.jsdelivr.net"],
|
||||||
|
imgSrc: ["'self'", "data:", "https:"],
|
||||||
|
connectSrc: ["'self'", "ws:", "wss:", "https://cdnjs.cloudflare.com", "https://cdn.jsdelivr.net", "https://unpkg.com", "https://fonts.googleapis.com", "https://fonts.gstatic.com", "https://cdn.tailwindcss.com"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Rate limiting
|
||||||
|
const limiter = rateLimit({
|
||||||
|
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||||
|
max: 100 // limit each IP to 100 requests per windowMs
|
||||||
|
});
|
||||||
|
app.use('/api/', limiter);
|
||||||
|
|
||||||
|
// Middleware
|
||||||
|
app.use(compression());
|
||||||
|
app.use(cors());
|
||||||
|
app.use(morgan('combined'));
|
||||||
|
app.use(express.json({ limit: '10mb' }));
|
||||||
|
app.use(express.urlencoded({ extended: true, limit: '10mb' }));
|
||||||
|
|
||||||
|
// Static files
|
||||||
|
app.use(express.static(path.join(__dirname, 'public')));
|
||||||
|
app.use('/uploads', express.static(path.join(__dirname, 'public/uploads')));
|
||||||
|
|
||||||
|
// AdminLTE static files
|
||||||
|
app.use('/node_modules', express.static(path.join(__dirname, 'node_modules')));
|
||||||
|
|
||||||
|
// View engine
|
||||||
|
app.set('view engine', 'ejs');
|
||||||
|
app.set('views', path.join(__dirname, 'views'));
|
||||||
|
|
||||||
|
// Layout engine
|
||||||
|
const expressLayouts = require('express-ejs-layouts');
|
||||||
|
app.use(expressLayouts);
|
||||||
|
app.set('layout', 'layout'); // Default layout for main site
|
||||||
|
app.set('layout extractScripts', true);
|
||||||
|
app.set('layout extractStyles', true);
|
||||||
|
|
||||||
|
// Database connection and testing
|
||||||
|
testConnection();
|
||||||
|
|
||||||
|
// Session store configuration
|
||||||
|
const sessionStore = new SequelizeStore({
|
||||||
|
db: sequelize,
|
||||||
|
tableName: 'sessions',
|
||||||
|
checkExpirationInterval: 15 * 60 * 1000, // 15 minutes
|
||||||
|
expiration: 7 * 24 * 60 * 60 * 1000 // 7 days
|
||||||
|
});
|
||||||
|
|
||||||
|
// Session configuration
|
||||||
|
app.use(session({
|
||||||
|
secret: process.env.SESSION_SECRET || 'your-secret-key',
|
||||||
|
resave: false,
|
||||||
|
saveUninitialized: false,
|
||||||
|
store: sessionStore,
|
||||||
|
cookie: {
|
||||||
|
secure: process.env.NODE_ENV === 'production',
|
||||||
|
httpOnly: true,
|
||||||
|
maxAge: 1000 * 60 * 60 * 24 * 7 // 7 days
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Middleware для передачи переменных в шаблоны
|
||||||
|
app.use((req, res, next) => {
|
||||||
|
const currentLang = req.session?.language || req.getLocale() || 'ru';
|
||||||
|
req.setLocale(currentLang);
|
||||||
|
|
||||||
|
res.locals.locale = currentLang;
|
||||||
|
res.locals.__ = res.__;
|
||||||
|
res.locals.theme = req.session?.theme || 'light';
|
||||||
|
res.locals.currentLanguage = currentLang;
|
||||||
|
res.locals.currentPage = req.path.split('/')[1] || 'home';
|
||||||
|
|
||||||
|
// Debug logging for theme
|
||||||
|
if (req.url !== '/sw.js' && !req.url.startsWith('/css/') && !req.url.startsWith('/js/') && !req.url.startsWith('/images/')) {
|
||||||
|
console.log(`Request URL: ${req.url}, Theme from session: ${req.session?.theme}, Setting theme to: ${res.locals.theme}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Устанавливаем заголовки для предотвращения кеширования языкового контента
|
||||||
|
if (!req.url.startsWith('/css/') && !req.url.startsWith('/js/') && !req.url.startsWith('/images/')) {
|
||||||
|
res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
|
||||||
|
res.setHeader('Pragma', 'no-cache');
|
||||||
|
res.setHeader('Expires', '0');
|
||||||
|
res.setHeader('Vary', 'Accept-Language');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Отладочная информация
|
||||||
|
console.log(`Request URL: ${req.url}, Current Language: ${currentLang}, Session Language: ${req.session?.language}, Locale: ${req.getLocale()}`);
|
||||||
|
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Routes
|
||||||
|
app.use('/', require('./routes/index'));
|
||||||
|
app.use('/api/auth', require('./routes/auth'));
|
||||||
|
app.use('/api/portfolio', require('./routes/portfolio'));
|
||||||
|
app.use('/api/services', require('./routes/services'));
|
||||||
|
app.use('/api/calculator', require('./routes/calculator'));
|
||||||
|
app.use('/api/contact', require('./routes/contact'));
|
||||||
|
app.use('/api/media', require('./routes/media'));
|
||||||
|
app.use('/api/admin', require('./routes/api/admin'));
|
||||||
|
app.use('/admin', require('./routes/admin'));
|
||||||
|
|
||||||
|
// Language switching routes
|
||||||
|
app.get('/lang/:language', (req, res) => {
|
||||||
|
const { language } = req.params;
|
||||||
|
const supportedLanguages = ['ko', 'en', 'ru', 'kk'];
|
||||||
|
|
||||||
|
if (supportedLanguages.includes(language)) {
|
||||||
|
req.setLocale(language);
|
||||||
|
req.session.language = language;
|
||||||
|
console.log(`Language switched to: ${language}, session language: ${req.session.language}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const referer = req.get('Referer') || '/';
|
||||||
|
res.redirect(referer);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Theme switching routes
|
||||||
|
app.get('/theme/:theme', (req, res) => {
|
||||||
|
const { theme } = req.params;
|
||||||
|
if (['light', 'dark'].includes(theme)) {
|
||||||
|
req.session.theme = theme;
|
||||||
|
}
|
||||||
|
res.json({ success: true, theme: req.session.theme });
|
||||||
|
});
|
||||||
|
|
||||||
|
// PWA Service Worker
|
||||||
|
app.get('/sw.js', (req, res) => {
|
||||||
|
res.sendFile(path.join(__dirname, 'public', 'sw.js'));
|
||||||
|
});
|
||||||
|
|
||||||
|
// PWA Manifest
|
||||||
|
app.get('/manifest.json', (req, res) => {
|
||||||
|
res.sendFile(path.join(__dirname, 'public', 'manifest.json'));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Error handling middleware
|
||||||
|
app.use((err, req, res, next) => {
|
||||||
|
console.error(err.stack);
|
||||||
|
res.status(500).render('error', {
|
||||||
|
title: 'Error',
|
||||||
|
settings: {},
|
||||||
|
message: process.env.NODE_ENV === 'production'
|
||||||
|
? 'Something went wrong!'
|
||||||
|
: err.message,
|
||||||
|
currentPage: 'error'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 404 handler
|
||||||
|
app.use((req, res) => {
|
||||||
|
res.status(404).render('error', {
|
||||||
|
title: '404 - 페이지를 찾을 수 없습니다',
|
||||||
|
settings: {},
|
||||||
|
message: '요청하신 페이지를 찾을 수 없습니다',
|
||||||
|
currentPage: 'error'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const PORT = process.env.PORT || 3000;
|
||||||
|
|
||||||
|
// Sync database and start server
|
||||||
|
async function startServer() {
|
||||||
|
try {
|
||||||
|
// Sync all models with database
|
||||||
|
await sequelize.sync({ force: false });
|
||||||
|
console.log('✓ Database synchronized');
|
||||||
|
|
||||||
|
// Create session table
|
||||||
|
await sessionStore.sync();
|
||||||
|
console.log('✓ Session store synchronized');
|
||||||
|
|
||||||
|
app.listen(PORT, () => {
|
||||||
|
console.log(`🚀 Server running on port ${PORT}`);
|
||||||
|
console.log(`🌐 Visit: http://localhost:${PORT}`);
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('✗ Failed to start server:', error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
startServer();
|
||||||
220
.history/server_20251026213507.js
Normal file
@@ -0,0 +1,220 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const { sequelize, testConnection } = require('./config/database');
|
||||||
|
const session = require('express-session');
|
||||||
|
const SequelizeStore = require('connect-session-sequelize')(session.Store);
|
||||||
|
const path = require('path');
|
||||||
|
const helmet = require('helmet');
|
||||||
|
const compression = require('compression');
|
||||||
|
const cors = require('cors');
|
||||||
|
const morgan = require('morgan');
|
||||||
|
const rateLimit = require('express-rate-limit');
|
||||||
|
const i18n = require('i18n');
|
||||||
|
require('dotenv').config();
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
|
||||||
|
// Настройка i18n
|
||||||
|
i18n.configure({
|
||||||
|
locales: ['ko', 'en', 'ru', 'kk'],
|
||||||
|
defaultLocale: 'ru',
|
||||||
|
directory: path.join(__dirname, 'locales'),
|
||||||
|
objectNotation: true,
|
||||||
|
updateFiles: false,
|
||||||
|
syncFiles: false
|
||||||
|
});
|
||||||
|
|
||||||
|
// i18n middleware
|
||||||
|
app.use(i18n.init);
|
||||||
|
|
||||||
|
// Security middleware
|
||||||
|
app.use(helmet({
|
||||||
|
contentSecurityPolicy: {
|
||||||
|
directives: {
|
||||||
|
defaultSrc: ["'self'"],
|
||||||
|
styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com", "https://cdnjs.cloudflare.com", "https://cdn.jsdelivr.net", "https://unpkg.com", "https://cdn.tailwindcss.com"],
|
||||||
|
fontSrc: ["'self'", "https://fonts.gstatic.com", "https://cdnjs.cloudflare.com"],
|
||||||
|
scriptSrc: ["'self'", "'unsafe-inline'", "https://cdnjs.cloudflare.com", "https://unpkg.com", "https://cdn.tailwindcss.com", "https://code.jquery.com", "https://cdn.jsdelivr.net"],
|
||||||
|
imgSrc: ["'self'", "data:", "https:"],
|
||||||
|
connectSrc: ["'self'", "ws:", "wss:", "https://cdnjs.cloudflare.com", "https://cdn.jsdelivr.net", "https://unpkg.com", "https://fonts.googleapis.com", "https://fonts.gstatic.com", "https://cdn.tailwindcss.com"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Rate limiting
|
||||||
|
const limiter = rateLimit({
|
||||||
|
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||||
|
max: 100 // limit each IP to 100 requests per windowMs
|
||||||
|
});
|
||||||
|
app.use('/api/', limiter);
|
||||||
|
|
||||||
|
// Middleware
|
||||||
|
app.use(compression());
|
||||||
|
app.use(cors());
|
||||||
|
app.use(morgan('combined'));
|
||||||
|
app.use(express.json({ limit: '10mb' }));
|
||||||
|
app.use(express.urlencoded({ extended: true, limit: '10mb' }));
|
||||||
|
|
||||||
|
// Static files
|
||||||
|
app.use(express.static(path.join(__dirname, 'public')));
|
||||||
|
app.use('/uploads', express.static(path.join(__dirname, 'public/uploads')));
|
||||||
|
|
||||||
|
// AdminLTE static files
|
||||||
|
app.use('/node_modules', express.static(path.join(__dirname, 'node_modules')));
|
||||||
|
|
||||||
|
// View engine
|
||||||
|
app.set('view engine', 'ejs');
|
||||||
|
app.set('views', path.join(__dirname, 'views'));
|
||||||
|
|
||||||
|
// Layout engine
|
||||||
|
const expressLayouts = require('express-ejs-layouts');
|
||||||
|
app.use(expressLayouts);
|
||||||
|
app.set('layout', 'layout'); // Default layout for main site
|
||||||
|
app.set('layout extractScripts', true);
|
||||||
|
app.set('layout extractStyles', true);
|
||||||
|
|
||||||
|
// Database connection and testing
|
||||||
|
testConnection();
|
||||||
|
|
||||||
|
// Session store configuration
|
||||||
|
const sessionStore = new SequelizeStore({
|
||||||
|
db: sequelize,
|
||||||
|
tableName: 'sessions',
|
||||||
|
checkExpirationInterval: 15 * 60 * 1000, // 15 minutes
|
||||||
|
expiration: 7 * 24 * 60 * 60 * 1000 // 7 days
|
||||||
|
});
|
||||||
|
|
||||||
|
// Session configuration
|
||||||
|
app.use(session({
|
||||||
|
secret: process.env.SESSION_SECRET || 'your-secret-key',
|
||||||
|
resave: false,
|
||||||
|
saveUninitialized: false,
|
||||||
|
store: sessionStore,
|
||||||
|
cookie: {
|
||||||
|
secure: process.env.NODE_ENV === 'production',
|
||||||
|
httpOnly: true,
|
||||||
|
maxAge: 1000 * 60 * 60 * 24 * 7 // 7 days
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Middleware для передачи переменных в шаблоны
|
||||||
|
app.use((req, res, next) => {
|
||||||
|
const currentLang = req.session?.language || req.getLocale() || 'ru';
|
||||||
|
req.setLocale(currentLang);
|
||||||
|
|
||||||
|
res.locals.locale = currentLang;
|
||||||
|
res.locals.__ = res.__;
|
||||||
|
res.locals.theme = req.session?.theme || 'light';
|
||||||
|
res.locals.currentLanguage = currentLang;
|
||||||
|
res.locals.currentPage = req.path.split('/')[1] || 'home';
|
||||||
|
|
||||||
|
// Debug logging for theme
|
||||||
|
if (req.url !== '/sw.js' && !req.url.startsWith('/css/') && !req.url.startsWith('/js/') && !req.url.startsWith('/images/')) {
|
||||||
|
console.log(`Request URL: ${req.url}, Theme from session: ${req.session?.theme}, Setting theme to: ${res.locals.theme}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Устанавливаем заголовки для предотвращения кеширования языкового контента
|
||||||
|
if (!req.url.startsWith('/css/') && !req.url.startsWith('/js/') && !req.url.startsWith('/images/')) {
|
||||||
|
res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
|
||||||
|
res.setHeader('Pragma', 'no-cache');
|
||||||
|
res.setHeader('Expires', '0');
|
||||||
|
res.setHeader('Vary', 'Accept-Language');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Отладочная информация
|
||||||
|
console.log(`Request URL: ${req.url}, Current Language: ${currentLang}, Session Language: ${req.session?.language}, Locale: ${req.getLocale()}`);
|
||||||
|
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Routes
|
||||||
|
app.use('/', require('./routes/index'));
|
||||||
|
app.use('/api/auth', require('./routes/auth'));
|
||||||
|
app.use('/api/portfolio', require('./routes/portfolio'));
|
||||||
|
app.use('/api/services', require('./routes/services'));
|
||||||
|
app.use('/api/calculator', require('./routes/calculator'));
|
||||||
|
app.use('/api/contact', require('./routes/contact'));
|
||||||
|
app.use('/api/media', require('./routes/media'));
|
||||||
|
app.use('/api/admin', require('./routes/api/admin'));
|
||||||
|
app.use('/admin', require('./routes/admin'));
|
||||||
|
|
||||||
|
// Language switching routes
|
||||||
|
app.get('/lang/:language', (req, res) => {
|
||||||
|
const { language } = req.params;
|
||||||
|
const supportedLanguages = ['ko', 'en', 'ru', 'kk'];
|
||||||
|
|
||||||
|
if (supportedLanguages.includes(language)) {
|
||||||
|
req.setLocale(language);
|
||||||
|
req.session.language = language;
|
||||||
|
console.log(`Language switched to: ${language}, session language: ${req.session.language}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const referer = req.get('Referer') || '/';
|
||||||
|
res.redirect(referer);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Theme switching routes
|
||||||
|
app.get('/theme/:theme', (req, res) => {
|
||||||
|
const { theme } = req.params;
|
||||||
|
if (['light', 'dark'].includes(theme)) {
|
||||||
|
req.session.theme = theme;
|
||||||
|
}
|
||||||
|
res.json({ success: true, theme: req.session.theme });
|
||||||
|
});
|
||||||
|
|
||||||
|
// PWA Service Worker
|
||||||
|
app.get('/sw.js', (req, res) => {
|
||||||
|
res.sendFile(path.join(__dirname, 'public', 'sw.js'));
|
||||||
|
});
|
||||||
|
|
||||||
|
// PWA Manifest
|
||||||
|
app.get('/manifest.json', (req, res) => {
|
||||||
|
res.sendFile(path.join(__dirname, 'public', 'manifest.json'));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Error handling middleware
|
||||||
|
app.use((err, req, res, next) => {
|
||||||
|
console.error(err.stack);
|
||||||
|
res.status(500).render('error', {
|
||||||
|
title: 'Error',
|
||||||
|
settings: {},
|
||||||
|
message: process.env.NODE_ENV === 'production'
|
||||||
|
? 'Something went wrong!'
|
||||||
|
: err.message,
|
||||||
|
currentPage: 'error'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 404 handler
|
||||||
|
app.use((req, res) => {
|
||||||
|
res.status(404).render('error', {
|
||||||
|
title: '404 - 페이지를 찾을 수 없습니다',
|
||||||
|
settings: {},
|
||||||
|
message: '요청하신 페이지를 찾을 수 없습니다',
|
||||||
|
currentPage: 'error'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const PORT = process.env.PORT || 3000;
|
||||||
|
|
||||||
|
// Sync database and start server
|
||||||
|
async function startServer() {
|
||||||
|
try {
|
||||||
|
// Sync all models with database
|
||||||
|
await sequelize.sync({ force: false });
|
||||||
|
console.log('✓ Database synchronized');
|
||||||
|
|
||||||
|
// Create session table
|
||||||
|
await sessionStore.sync();
|
||||||
|
console.log('✓ Session store synchronized');
|
||||||
|
|
||||||
|
app.listen(PORT, () => {
|
||||||
|
console.log(`🚀 Server running on port ${PORT}`);
|
||||||
|
console.log(`🌐 Visit: http://localhost:${PORT}`);
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('✗ Failed to start server:', error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
startServer();
|
||||||
220
.history/server_20251026215908.js
Normal file
@@ -0,0 +1,220 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const { sequelize, testConnection } = require('./config/database');
|
||||||
|
const session = require('express-session');
|
||||||
|
const SequelizeStore = require('connect-session-sequelize')(session.Store);
|
||||||
|
const path = require('path');
|
||||||
|
const helmet = require('helmet');
|
||||||
|
const compression = require('compression');
|
||||||
|
const cors = require('cors');
|
||||||
|
const morgan = require('morgan');
|
||||||
|
const rateLimit = require('express-rate-limit');
|
||||||
|
const i18n = require('i18n');
|
||||||
|
require('dotenv').config();
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
|
||||||
|
// Настройка i18n
|
||||||
|
i18n.configure({
|
||||||
|
locales: ['ko', 'en', 'ru', 'kk'],
|
||||||
|
defaultLocale: 'ru',
|
||||||
|
directory: path.join(__dirname, 'locales'),
|
||||||
|
objectNotation: true,
|
||||||
|
updateFiles: false,
|
||||||
|
syncFiles: false
|
||||||
|
});
|
||||||
|
|
||||||
|
// i18n middleware
|
||||||
|
app.use(i18n.init);
|
||||||
|
|
||||||
|
// Security middleware
|
||||||
|
app.use(helmet({
|
||||||
|
contentSecurityPolicy: {
|
||||||
|
directives: {
|
||||||
|
defaultSrc: ["'self'"],
|
||||||
|
styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com", "https://cdn.tailwindcss.com"],
|
||||||
|
fontSrc: ["'self'", "https://fonts.gstatic.com"],
|
||||||
|
scriptSrc: ["'self'", "'unsafe-inline'", "https://unpkg.com", "https://cdn.tailwindcss.com"],
|
||||||
|
imgSrc: ["'self'", "data:", "https:"],
|
||||||
|
connectSrc: ["'self'", "ws:", "wss:", "https://fonts.googleapis.com", "https://fonts.gstatic.com", "https://cdn.tailwindcss.com"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Rate limiting
|
||||||
|
const limiter = rateLimit({
|
||||||
|
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||||
|
max: 100 // limit each IP to 100 requests per windowMs
|
||||||
|
});
|
||||||
|
app.use('/api/', limiter);
|
||||||
|
|
||||||
|
// Middleware
|
||||||
|
app.use(compression());
|
||||||
|
app.use(cors());
|
||||||
|
app.use(morgan('combined'));
|
||||||
|
app.use(express.json({ limit: '10mb' }));
|
||||||
|
app.use(express.urlencoded({ extended: true, limit: '10mb' }));
|
||||||
|
|
||||||
|
// Static files
|
||||||
|
app.use(express.static(path.join(__dirname, 'public')));
|
||||||
|
app.use('/uploads', express.static(path.join(__dirname, 'public/uploads')));
|
||||||
|
|
||||||
|
// AdminLTE static files
|
||||||
|
app.use('/node_modules', express.static(path.join(__dirname, 'node_modules')));
|
||||||
|
|
||||||
|
// View engine
|
||||||
|
app.set('view engine', 'ejs');
|
||||||
|
app.set('views', path.join(__dirname, 'views'));
|
||||||
|
|
||||||
|
// Layout engine
|
||||||
|
const expressLayouts = require('express-ejs-layouts');
|
||||||
|
app.use(expressLayouts);
|
||||||
|
app.set('layout', 'layout'); // Default layout for main site
|
||||||
|
app.set('layout extractScripts', true);
|
||||||
|
app.set('layout extractStyles', true);
|
||||||
|
|
||||||
|
// Database connection and testing
|
||||||
|
testConnection();
|
||||||
|
|
||||||
|
// Session store configuration
|
||||||
|
const sessionStore = new SequelizeStore({
|
||||||
|
db: sequelize,
|
||||||
|
tableName: 'sessions',
|
||||||
|
checkExpirationInterval: 15 * 60 * 1000, // 15 minutes
|
||||||
|
expiration: 7 * 24 * 60 * 60 * 1000 // 7 days
|
||||||
|
});
|
||||||
|
|
||||||
|
// Session configuration
|
||||||
|
app.use(session({
|
||||||
|
secret: process.env.SESSION_SECRET || 'your-secret-key',
|
||||||
|
resave: false,
|
||||||
|
saveUninitialized: false,
|
||||||
|
store: sessionStore,
|
||||||
|
cookie: {
|
||||||
|
secure: process.env.NODE_ENV === 'production',
|
||||||
|
httpOnly: true,
|
||||||
|
maxAge: 1000 * 60 * 60 * 24 * 7 // 7 days
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Middleware для передачи переменных в шаблоны
|
||||||
|
app.use((req, res, next) => {
|
||||||
|
const currentLang = req.session?.language || req.getLocale() || 'ru';
|
||||||
|
req.setLocale(currentLang);
|
||||||
|
|
||||||
|
res.locals.locale = currentLang;
|
||||||
|
res.locals.__ = res.__;
|
||||||
|
res.locals.theme = req.session?.theme || 'light';
|
||||||
|
res.locals.currentLanguage = currentLang;
|
||||||
|
res.locals.currentPage = req.path.split('/')[1] || 'home';
|
||||||
|
|
||||||
|
// Debug logging for theme
|
||||||
|
if (req.url !== '/sw.js' && !req.url.startsWith('/css/') && !req.url.startsWith('/js/') && !req.url.startsWith('/images/')) {
|
||||||
|
console.log(`Request URL: ${req.url}, Theme from session: ${req.session?.theme}, Setting theme to: ${res.locals.theme}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Устанавливаем заголовки для предотвращения кеширования языкового контента
|
||||||
|
if (!req.url.startsWith('/css/') && !req.url.startsWith('/js/') && !req.url.startsWith('/images/')) {
|
||||||
|
res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
|
||||||
|
res.setHeader('Pragma', 'no-cache');
|
||||||
|
res.setHeader('Expires', '0');
|
||||||
|
res.setHeader('Vary', 'Accept-Language');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Отладочная информация
|
||||||
|
console.log(`Request URL: ${req.url}, Current Language: ${currentLang}, Session Language: ${req.session?.language}, Locale: ${req.getLocale()}`);
|
||||||
|
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Routes
|
||||||
|
app.use('/', require('./routes/index'));
|
||||||
|
app.use('/api/auth', require('./routes/auth'));
|
||||||
|
app.use('/api/portfolio', require('./routes/portfolio'));
|
||||||
|
app.use('/api/services', require('./routes/services'));
|
||||||
|
app.use('/api/calculator', require('./routes/calculator'));
|
||||||
|
app.use('/api/contact', require('./routes/contact'));
|
||||||
|
app.use('/api/media', require('./routes/media'));
|
||||||
|
app.use('/api/admin', require('./routes/api/admin'));
|
||||||
|
app.use('/admin', require('./routes/admin'));
|
||||||
|
|
||||||
|
// Language switching routes
|
||||||
|
app.get('/lang/:language', (req, res) => {
|
||||||
|
const { language } = req.params;
|
||||||
|
const supportedLanguages = ['ko', 'en', 'ru', 'kk'];
|
||||||
|
|
||||||
|
if (supportedLanguages.includes(language)) {
|
||||||
|
req.setLocale(language);
|
||||||
|
req.session.language = language;
|
||||||
|
console.log(`Language switched to: ${language}, session language: ${req.session.language}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const referer = req.get('Referer') || '/';
|
||||||
|
res.redirect(referer);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Theme switching routes
|
||||||
|
app.get('/theme/:theme', (req, res) => {
|
||||||
|
const { theme } = req.params;
|
||||||
|
if (['light', 'dark'].includes(theme)) {
|
||||||
|
req.session.theme = theme;
|
||||||
|
}
|
||||||
|
res.json({ success: true, theme: req.session.theme });
|
||||||
|
});
|
||||||
|
|
||||||
|
// PWA Service Worker
|
||||||
|
app.get('/sw.js', (req, res) => {
|
||||||
|
res.sendFile(path.join(__dirname, 'public', 'sw.js'));
|
||||||
|
});
|
||||||
|
|
||||||
|
// PWA Manifest
|
||||||
|
app.get('/manifest.json', (req, res) => {
|
||||||
|
res.sendFile(path.join(__dirname, 'public', 'manifest.json'));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Error handling middleware
|
||||||
|
app.use((err, req, res, next) => {
|
||||||
|
console.error(err.stack);
|
||||||
|
res.status(500).render('error', {
|
||||||
|
title: 'Error',
|
||||||
|
settings: {},
|
||||||
|
message: process.env.NODE_ENV === 'production'
|
||||||
|
? 'Something went wrong!'
|
||||||
|
: err.message,
|
||||||
|
currentPage: 'error'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 404 handler
|
||||||
|
app.use((req, res) => {
|
||||||
|
res.status(404).render('error', {
|
||||||
|
title: '404 - 페이지를 찾을 수 없습니다',
|
||||||
|
settings: {},
|
||||||
|
message: '요청하신 페이지를 찾을 수 없습니다',
|
||||||
|
currentPage: 'error'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const PORT = process.env.PORT || 3000;
|
||||||
|
|
||||||
|
// Sync database and start server
|
||||||
|
async function startServer() {
|
||||||
|
try {
|
||||||
|
// Sync all models with database
|
||||||
|
await sequelize.sync({ force: false });
|
||||||
|
console.log('✓ Database synchronized');
|
||||||
|
|
||||||
|
// Create session table
|
||||||
|
await sessionStore.sync();
|
||||||
|
console.log('✓ Session store synchronized');
|
||||||
|
|
||||||
|
app.listen(PORT, () => {
|
||||||
|
console.log(`🚀 Server running on port ${PORT}`);
|
||||||
|
console.log(`🌐 Visit: http://localhost:${PORT}`);
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('✗ Failed to start server:', error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
startServer();
|
||||||
223
.history/server_20251026215916.js
Normal file
@@ -0,0 +1,223 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const { sequelize, testConnection } = require('./config/database');
|
||||||
|
const session = require('express-session');
|
||||||
|
const SequelizeStore = require('connect-session-sequelize')(session.Store);
|
||||||
|
const path = require('path');
|
||||||
|
const helmet = require('helmet');
|
||||||
|
const compression = require('compression');
|
||||||
|
const cors = require('cors');
|
||||||
|
const morgan = require('morgan');
|
||||||
|
const rateLimit = require('express-rate-limit');
|
||||||
|
const i18n = require('i18n');
|
||||||
|
require('dotenv').config();
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
|
||||||
|
// Настройка i18n
|
||||||
|
i18n.configure({
|
||||||
|
locales: ['ko', 'en', 'ru', 'kk'],
|
||||||
|
defaultLocale: 'ru',
|
||||||
|
directory: path.join(__dirname, 'locales'),
|
||||||
|
objectNotation: true,
|
||||||
|
updateFiles: false,
|
||||||
|
syncFiles: false
|
||||||
|
});
|
||||||
|
|
||||||
|
// i18n middleware
|
||||||
|
app.use(i18n.init);
|
||||||
|
|
||||||
|
// Security middleware
|
||||||
|
app.use(helmet({
|
||||||
|
contentSecurityPolicy: {
|
||||||
|
directives: {
|
||||||
|
defaultSrc: ["'self'"],
|
||||||
|
styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com", "https://cdn.tailwindcss.com"],
|
||||||
|
fontSrc: ["'self'", "https://fonts.gstatic.com"],
|
||||||
|
scriptSrc: ["'self'", "'unsafe-inline'", "https://unpkg.com", "https://cdn.tailwindcss.com"],
|
||||||
|
imgSrc: ["'self'", "data:", "https:"],
|
||||||
|
connectSrc: ["'self'", "ws:", "wss:", "https://fonts.googleapis.com", "https://fonts.gstatic.com", "https://cdn.tailwindcss.com"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Rate limiting
|
||||||
|
const limiter = rateLimit({
|
||||||
|
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||||
|
max: 100 // limit each IP to 100 requests per windowMs
|
||||||
|
});
|
||||||
|
app.use('/api/', limiter);
|
||||||
|
|
||||||
|
// Middleware
|
||||||
|
app.use(compression());
|
||||||
|
app.use(cors());
|
||||||
|
app.use(morgan('combined'));
|
||||||
|
app.use(express.json({ limit: '10mb' }));
|
||||||
|
app.use(express.urlencoded({ extended: true, limit: '10mb' }));
|
||||||
|
|
||||||
|
// Static files
|
||||||
|
app.use(express.static(path.join(__dirname, 'public')));
|
||||||
|
app.use('/uploads', express.static(path.join(__dirname, 'public/uploads')));
|
||||||
|
|
||||||
|
// Vendor files (local copies of external libraries)
|
||||||
|
app.use('/vendor', express.static(path.join(__dirname, 'public/vendor')));
|
||||||
|
|
||||||
|
// AdminLTE static files
|
||||||
|
app.use('/node_modules', express.static(path.join(__dirname, 'node_modules')));
|
||||||
|
|
||||||
|
// View engine
|
||||||
|
app.set('view engine', 'ejs');
|
||||||
|
app.set('views', path.join(__dirname, 'views'));
|
||||||
|
|
||||||
|
// Layout engine
|
||||||
|
const expressLayouts = require('express-ejs-layouts');
|
||||||
|
app.use(expressLayouts);
|
||||||
|
app.set('layout', 'layout'); // Default layout for main site
|
||||||
|
app.set('layout extractScripts', true);
|
||||||
|
app.set('layout extractStyles', true);
|
||||||
|
|
||||||
|
// Database connection and testing
|
||||||
|
testConnection();
|
||||||
|
|
||||||
|
// Session store configuration
|
||||||
|
const sessionStore = new SequelizeStore({
|
||||||
|
db: sequelize,
|
||||||
|
tableName: 'sessions',
|
||||||
|
checkExpirationInterval: 15 * 60 * 1000, // 15 minutes
|
||||||
|
expiration: 7 * 24 * 60 * 60 * 1000 // 7 days
|
||||||
|
});
|
||||||
|
|
||||||
|
// Session configuration
|
||||||
|
app.use(session({
|
||||||
|
secret: process.env.SESSION_SECRET || 'your-secret-key',
|
||||||
|
resave: false,
|
||||||
|
saveUninitialized: false,
|
||||||
|
store: sessionStore,
|
||||||
|
cookie: {
|
||||||
|
secure: process.env.NODE_ENV === 'production',
|
||||||
|
httpOnly: true,
|
||||||
|
maxAge: 1000 * 60 * 60 * 24 * 7 // 7 days
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Middleware для передачи переменных в шаблоны
|
||||||
|
app.use((req, res, next) => {
|
||||||
|
const currentLang = req.session?.language || req.getLocale() || 'ru';
|
||||||
|
req.setLocale(currentLang);
|
||||||
|
|
||||||
|
res.locals.locale = currentLang;
|
||||||
|
res.locals.__ = res.__;
|
||||||
|
res.locals.theme = req.session?.theme || 'light';
|
||||||
|
res.locals.currentLanguage = currentLang;
|
||||||
|
res.locals.currentPage = req.path.split('/')[1] || 'home';
|
||||||
|
|
||||||
|
// Debug logging for theme
|
||||||
|
if (req.url !== '/sw.js' && !req.url.startsWith('/css/') && !req.url.startsWith('/js/') && !req.url.startsWith('/images/')) {
|
||||||
|
console.log(`Request URL: ${req.url}, Theme from session: ${req.session?.theme}, Setting theme to: ${res.locals.theme}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Устанавливаем заголовки для предотвращения кеширования языкового контента
|
||||||
|
if (!req.url.startsWith('/css/') && !req.url.startsWith('/js/') && !req.url.startsWith('/images/')) {
|
||||||
|
res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
|
||||||
|
res.setHeader('Pragma', 'no-cache');
|
||||||
|
res.setHeader('Expires', '0');
|
||||||
|
res.setHeader('Vary', 'Accept-Language');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Отладочная информация
|
||||||
|
console.log(`Request URL: ${req.url}, Current Language: ${currentLang}, Session Language: ${req.session?.language}, Locale: ${req.getLocale()}`);
|
||||||
|
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Routes
|
||||||
|
app.use('/', require('./routes/index'));
|
||||||
|
app.use('/api/auth', require('./routes/auth'));
|
||||||
|
app.use('/api/portfolio', require('./routes/portfolio'));
|
||||||
|
app.use('/api/services', require('./routes/services'));
|
||||||
|
app.use('/api/calculator', require('./routes/calculator'));
|
||||||
|
app.use('/api/contact', require('./routes/contact'));
|
||||||
|
app.use('/api/media', require('./routes/media'));
|
||||||
|
app.use('/api/admin', require('./routes/api/admin'));
|
||||||
|
app.use('/admin', require('./routes/admin'));
|
||||||
|
|
||||||
|
// Language switching routes
|
||||||
|
app.get('/lang/:language', (req, res) => {
|
||||||
|
const { language } = req.params;
|
||||||
|
const supportedLanguages = ['ko', 'en', 'ru', 'kk'];
|
||||||
|
|
||||||
|
if (supportedLanguages.includes(language)) {
|
||||||
|
req.setLocale(language);
|
||||||
|
req.session.language = language;
|
||||||
|
console.log(`Language switched to: ${language}, session language: ${req.session.language}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const referer = req.get('Referer') || '/';
|
||||||
|
res.redirect(referer);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Theme switching routes
|
||||||
|
app.get('/theme/:theme', (req, res) => {
|
||||||
|
const { theme } = req.params;
|
||||||
|
if (['light', 'dark'].includes(theme)) {
|
||||||
|
req.session.theme = theme;
|
||||||
|
}
|
||||||
|
res.json({ success: true, theme: req.session.theme });
|
||||||
|
});
|
||||||
|
|
||||||
|
// PWA Service Worker
|
||||||
|
app.get('/sw.js', (req, res) => {
|
||||||
|
res.sendFile(path.join(__dirname, 'public', 'sw.js'));
|
||||||
|
});
|
||||||
|
|
||||||
|
// PWA Manifest
|
||||||
|
app.get('/manifest.json', (req, res) => {
|
||||||
|
res.sendFile(path.join(__dirname, 'public', 'manifest.json'));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Error handling middleware
|
||||||
|
app.use((err, req, res, next) => {
|
||||||
|
console.error(err.stack);
|
||||||
|
res.status(500).render('error', {
|
||||||
|
title: 'Error',
|
||||||
|
settings: {},
|
||||||
|
message: process.env.NODE_ENV === 'production'
|
||||||
|
? 'Something went wrong!'
|
||||||
|
: err.message,
|
||||||
|
currentPage: 'error'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 404 handler
|
||||||
|
app.use((req, res) => {
|
||||||
|
res.status(404).render('error', {
|
||||||
|
title: '404 - 페이지를 찾을 수 없습니다',
|
||||||
|
settings: {},
|
||||||
|
message: '요청하신 페이지를 찾을 수 없습니다',
|
||||||
|
currentPage: 'error'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const PORT = process.env.PORT || 3000;
|
||||||
|
|
||||||
|
// Sync database and start server
|
||||||
|
async function startServer() {
|
||||||
|
try {
|
||||||
|
// Sync all models with database
|
||||||
|
await sequelize.sync({ force: false });
|
||||||
|
console.log('✓ Database synchronized');
|
||||||
|
|
||||||
|
// Create session table
|
||||||
|
await sessionStore.sync();
|
||||||
|
console.log('✓ Session store synchronized');
|
||||||
|
|
||||||
|
app.listen(PORT, () => {
|
||||||
|
console.log(`🚀 Server running on port ${PORT}`);
|
||||||
|
console.log(`🌐 Visit: http://localhost:${PORT}`);
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('✗ Failed to start server:', error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
startServer();
|
||||||
223
.history/server_20251026215934.js
Normal file
@@ -0,0 +1,223 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const { sequelize, testConnection } = require('./config/database');
|
||||||
|
const session = require('express-session');
|
||||||
|
const SequelizeStore = require('connect-session-sequelize')(session.Store);
|
||||||
|
const path = require('path');
|
||||||
|
const helmet = require('helmet');
|
||||||
|
const compression = require('compression');
|
||||||
|
const cors = require('cors');
|
||||||
|
const morgan = require('morgan');
|
||||||
|
const rateLimit = require('express-rate-limit');
|
||||||
|
const i18n = require('i18n');
|
||||||
|
require('dotenv').config();
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
|
||||||
|
// Настройка i18n
|
||||||
|
i18n.configure({
|
||||||
|
locales: ['ko', 'en', 'ru', 'kk'],
|
||||||
|
defaultLocale: 'ru',
|
||||||
|
directory: path.join(__dirname, 'locales'),
|
||||||
|
objectNotation: true,
|
||||||
|
updateFiles: false,
|
||||||
|
syncFiles: false
|
||||||
|
});
|
||||||
|
|
||||||
|
// i18n middleware
|
||||||
|
app.use(i18n.init);
|
||||||
|
|
||||||
|
// Security middleware
|
||||||
|
app.use(helmet({
|
||||||
|
contentSecurityPolicy: {
|
||||||
|
directives: {
|
||||||
|
defaultSrc: ["'self'"],
|
||||||
|
styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com", "https://cdn.tailwindcss.com"],
|
||||||
|
fontSrc: ["'self'", "https://fonts.gstatic.com"],
|
||||||
|
scriptSrc: ["'self'", "'unsafe-inline'", "https://unpkg.com", "https://cdn.tailwindcss.com"],
|
||||||
|
imgSrc: ["'self'", "data:", "https:"],
|
||||||
|
connectSrc: ["'self'", "ws:", "wss:", "https://fonts.googleapis.com", "https://fonts.gstatic.com", "https://cdn.tailwindcss.com"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Rate limiting
|
||||||
|
const limiter = rateLimit({
|
||||||
|
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||||
|
max: 100 // limit each IP to 100 requests per windowMs
|
||||||
|
});
|
||||||
|
app.use('/api/', limiter);
|
||||||
|
|
||||||
|
// Middleware
|
||||||
|
app.use(compression());
|
||||||
|
app.use(cors());
|
||||||
|
app.use(morgan('combined'));
|
||||||
|
app.use(express.json({ limit: '10mb' }));
|
||||||
|
app.use(express.urlencoded({ extended: true, limit: '10mb' }));
|
||||||
|
|
||||||
|
// Static files
|
||||||
|
app.use(express.static(path.join(__dirname, 'public')));
|
||||||
|
app.use('/uploads', express.static(path.join(__dirname, 'public/uploads')));
|
||||||
|
|
||||||
|
// Vendor files (local copies of external libraries)
|
||||||
|
app.use('/vendor', express.static(path.join(__dirname, 'public/vendor')));
|
||||||
|
|
||||||
|
// AdminLTE static files
|
||||||
|
app.use('/node_modules', express.static(path.join(__dirname, 'node_modules')));
|
||||||
|
|
||||||
|
// View engine
|
||||||
|
app.set('view engine', 'ejs');
|
||||||
|
app.set('views', path.join(__dirname, 'views'));
|
||||||
|
|
||||||
|
// Layout engine
|
||||||
|
const expressLayouts = require('express-ejs-layouts');
|
||||||
|
app.use(expressLayouts);
|
||||||
|
app.set('layout', 'layout'); // Default layout for main site
|
||||||
|
app.set('layout extractScripts', true);
|
||||||
|
app.set('layout extractStyles', true);
|
||||||
|
|
||||||
|
// Database connection and testing
|
||||||
|
testConnection();
|
||||||
|
|
||||||
|
// Session store configuration
|
||||||
|
const sessionStore = new SequelizeStore({
|
||||||
|
db: sequelize,
|
||||||
|
tableName: 'sessions',
|
||||||
|
checkExpirationInterval: 15 * 60 * 1000, // 15 minutes
|
||||||
|
expiration: 7 * 24 * 60 * 60 * 1000 // 7 days
|
||||||
|
});
|
||||||
|
|
||||||
|
// Session configuration
|
||||||
|
app.use(session({
|
||||||
|
secret: process.env.SESSION_SECRET || 'your-secret-key',
|
||||||
|
resave: false,
|
||||||
|
saveUninitialized: false,
|
||||||
|
store: sessionStore,
|
||||||
|
cookie: {
|
||||||
|
secure: process.env.NODE_ENV === 'production',
|
||||||
|
httpOnly: true,
|
||||||
|
maxAge: 1000 * 60 * 60 * 24 * 7 // 7 days
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Middleware для передачи переменных в шаблоны
|
||||||
|
app.use((req, res, next) => {
|
||||||
|
const currentLang = req.session?.language || req.getLocale() || 'ru';
|
||||||
|
req.setLocale(currentLang);
|
||||||
|
|
||||||
|
res.locals.locale = currentLang;
|
||||||
|
res.locals.__ = res.__;
|
||||||
|
res.locals.theme = req.session?.theme || 'light';
|
||||||
|
res.locals.currentLanguage = currentLang;
|
||||||
|
res.locals.currentPage = req.path.split('/')[1] || 'home';
|
||||||
|
|
||||||
|
// Debug logging for theme
|
||||||
|
if (req.url !== '/sw.js' && !req.url.startsWith('/css/') && !req.url.startsWith('/js/') && !req.url.startsWith('/images/')) {
|
||||||
|
console.log(`Request URL: ${req.url}, Theme from session: ${req.session?.theme}, Setting theme to: ${res.locals.theme}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Устанавливаем заголовки для предотвращения кеширования языкового контента
|
||||||
|
if (!req.url.startsWith('/css/') && !req.url.startsWith('/js/') && !req.url.startsWith('/images/')) {
|
||||||
|
res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
|
||||||
|
res.setHeader('Pragma', 'no-cache');
|
||||||
|
res.setHeader('Expires', '0');
|
||||||
|
res.setHeader('Vary', 'Accept-Language');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Отладочная информация
|
||||||
|
console.log(`Request URL: ${req.url}, Current Language: ${currentLang}, Session Language: ${req.session?.language}, Locale: ${req.getLocale()}`);
|
||||||
|
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Routes
|
||||||
|
app.use('/', require('./routes/index'));
|
||||||
|
app.use('/api/auth', require('./routes/auth'));
|
||||||
|
app.use('/api/portfolio', require('./routes/portfolio'));
|
||||||
|
app.use('/api/services', require('./routes/services'));
|
||||||
|
app.use('/api/calculator', require('./routes/calculator'));
|
||||||
|
app.use('/api/contact', require('./routes/contact'));
|
||||||
|
app.use('/api/media', require('./routes/media'));
|
||||||
|
app.use('/api/admin', require('./routes/api/admin'));
|
||||||
|
app.use('/admin', require('./routes/admin'));
|
||||||
|
|
||||||
|
// Language switching routes
|
||||||
|
app.get('/lang/:language', (req, res) => {
|
||||||
|
const { language } = req.params;
|
||||||
|
const supportedLanguages = ['ko', 'en', 'ru', 'kk'];
|
||||||
|
|
||||||
|
if (supportedLanguages.includes(language)) {
|
||||||
|
req.setLocale(language);
|
||||||
|
req.session.language = language;
|
||||||
|
console.log(`Language switched to: ${language}, session language: ${req.session.language}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const referer = req.get('Referer') || '/';
|
||||||
|
res.redirect(referer);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Theme switching routes
|
||||||
|
app.get('/theme/:theme', (req, res) => {
|
||||||
|
const { theme } = req.params;
|
||||||
|
if (['light', 'dark'].includes(theme)) {
|
||||||
|
req.session.theme = theme;
|
||||||
|
}
|
||||||
|
res.json({ success: true, theme: req.session.theme });
|
||||||
|
});
|
||||||
|
|
||||||
|
// PWA Service Worker
|
||||||
|
app.get('/sw.js', (req, res) => {
|
||||||
|
res.sendFile(path.join(__dirname, 'public', 'sw.js'));
|
||||||
|
});
|
||||||
|
|
||||||
|
// PWA Manifest
|
||||||
|
app.get('/manifest.json', (req, res) => {
|
||||||
|
res.sendFile(path.join(__dirname, 'public', 'manifest.json'));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Error handling middleware
|
||||||
|
app.use((err, req, res, next) => {
|
||||||
|
console.error(err.stack);
|
||||||
|
res.status(500).render('error', {
|
||||||
|
title: 'Error',
|
||||||
|
settings: {},
|
||||||
|
message: process.env.NODE_ENV === 'production'
|
||||||
|
? 'Something went wrong!'
|
||||||
|
: err.message,
|
||||||
|
currentPage: 'error'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 404 handler
|
||||||
|
app.use((req, res) => {
|
||||||
|
res.status(404).render('error', {
|
||||||
|
title: '404 - 페이지를 찾을 수 없습니다',
|
||||||
|
settings: {},
|
||||||
|
message: '요청하신 페이지를 찾을 수 없습니다',
|
||||||
|
currentPage: 'error'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const PORT = process.env.PORT || 3000;
|
||||||
|
|
||||||
|
// Sync database and start server
|
||||||
|
async function startServer() {
|
||||||
|
try {
|
||||||
|
// Sync all models with database
|
||||||
|
await sequelize.sync({ force: false });
|
||||||
|
console.log('✓ Database synchronized');
|
||||||
|
|
||||||
|
// Create session table
|
||||||
|
await sessionStore.sync();
|
||||||
|
console.log('✓ Session store synchronized');
|
||||||
|
|
||||||
|
app.listen(PORT, () => {
|
||||||
|
console.log(`🚀 Server running on port ${PORT}`);
|
||||||
|
console.log(`🌐 Visit: http://localhost:${PORT}`);
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('✗ Failed to start server:', error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
startServer();
|
||||||
374
.history/views/LandingDemo_20251026205940.tsx
Normal file
@@ -0,0 +1,374 @@
|
|||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import {
|
||||||
|
ArrowRight, Calculator, Code2, Smartphone, Palette, LineChart, Eye, Menu, X,
|
||||||
|
Sun, Moon, ChevronDown, Phone, Mail, MessageCircle, Rocket, Users, Clock,
|
||||||
|
Headphones, Award
|
||||||
|
} from "lucide-react";
|
||||||
|
import AOS from "aos";
|
||||||
|
import "aos/dist/aos.css";
|
||||||
|
|
||||||
|
// Korean i18n strings to replace <%- __('...') %>
|
||||||
|
const t = {
|
||||||
|
navigation: {
|
||||||
|
home: "홈",
|
||||||
|
about: "회사소개",
|
||||||
|
services: "서비스",
|
||||||
|
portfolio: "포트폴리오",
|
||||||
|
contact: "연락처",
|
||||||
|
calculator: "비용계산기"
|
||||||
|
},
|
||||||
|
language: {
|
||||||
|
korean: "한국어",
|
||||||
|
english: "English",
|
||||||
|
russian: "Русский",
|
||||||
|
kazakh: "Қазақша"
|
||||||
|
},
|
||||||
|
hero: {
|
||||||
|
title: { smart: "스마트", solutions: "솔루션" },
|
||||||
|
subtitle: "혁신적인 기술로 비즈니스를 성장시키세요",
|
||||||
|
description: "비즈니스의 디지털 전환을 이끄는 혁신적인 웹개발, 모바일 앱, UI/UX 디자인",
|
||||||
|
cta: { start: "시작하기", portfolio: "포트폴리오 보기" },
|
||||||
|
},
|
||||||
|
services: {
|
||||||
|
title: { our: "우리의", services: "서비스" },
|
||||||
|
subtitle: "아이디어를 현실로 만드는 전문 개발 서비스",
|
||||||
|
description: "최첨단 기술과 창의적 아이디어로 완성하는 디지털 솔루션",
|
||||||
|
view_all: "모든 서비스 보기",
|
||||||
|
web: { title: "웹 개발", description: "반응형 웹사이트 및 웹 애플리케이션 개발", price: "500,000원부터" },
|
||||||
|
mobile: { title: "모바일 앱", description: "iOS 및 Android 네이티브 앱 개발", price: "1,000,000원부터" },
|
||||||
|
design: { title: "UI/UX 디자인", description: "사용자 중심의 인터페이스 및 경험 디자인", price: "300,000원부터" },
|
||||||
|
marketing: { title: "디지털 마케팅", description: "SEO, 소셜미디어 마케팅, 광고 관리", price: "200,000원부터" },
|
||||||
|
hero: {
|
||||||
|
title: "우리의",
|
||||||
|
title_highlight: "서비스",
|
||||||
|
subtitle: "혁신적인 기술로 비즈니스 성장 지원"
|
||||||
|
},
|
||||||
|
cards: {
|
||||||
|
starting_price: "시작 가격",
|
||||||
|
consultation: "상담",
|
||||||
|
calculate_cost: "비용 계산",
|
||||||
|
popular: "인기"
|
||||||
|
},
|
||||||
|
process: {
|
||||||
|
title: "프로젝트 수행 과정",
|
||||||
|
subtitle: "체계적이고 전문적인 프로세스로 프로젝트를 진행합니다",
|
||||||
|
step1: { title: "상담 및 기획", description: "고객의 요구사항을 정확히 파악하고 최적의 솔루션을 기획합니다" },
|
||||||
|
step2: { title: "디자인 및 설계", description: "사용자 중심의 직관적인 디자인과 견고한 시스템 아키텍처를 설계합니다" },
|
||||||
|
step3: { title: "개발 및 구현", description: "최신 기술과 모범 사례를 활용하여 효율적이고 확장 가능한 솔루션을 개발합니다" },
|
||||||
|
step4: { title: "테스트 및 배포", description: "철저한 테스트를 통해 품질을 보장하고 안정적인 배포를 진행합니다" }
|
||||||
|
},
|
||||||
|
why_choose: {
|
||||||
|
title: "왜 SmartSolTech를 선택해야 할까요?",
|
||||||
|
modern_tech: { title: "최신 기술 활용", description: "항상 최신 기술 트렌드를 파악하고, 검증된 기술 스택을 활용하여 미래 지향적인 솔루션을 제공합니다." },
|
||||||
|
expert_team: { title: "전문가 팀", description: "각 분야의 전문가들로 구성된 팀이 협력하여 최고 품질의 결과물을 보장합니다." },
|
||||||
|
fast_response: { title: "빠른 대응", description: "신속한 의사소통과 빠른 피드백을 통해 프로젝트를 효율적으로 진행합니다." },
|
||||||
|
continuous_support: { title: "지속적인 지원", description: "프로젝트 완료 후에도 지속적인 유지보수와 기술 지원을 제공합니다." },
|
||||||
|
quality_guarantee: { title: "품질 보장", subtitle: "최고 품질의 결과물을 약속드립니다" }
|
||||||
|
},
|
||||||
|
cta: {
|
||||||
|
title: "프로젝트를 시작할 준비가 되셨나요?",
|
||||||
|
subtitle: "지금 바로 무료 상담을 받아보세요",
|
||||||
|
calculate_cost: "비용 계산하기",
|
||||||
|
view_portfolio: "포트폴리오 보기"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
portfolio: {
|
||||||
|
title: { recent: "최근", projects: "프로젝트" },
|
||||||
|
subtitle: "웹, 모바일, 브랜딩 분야의 선별된 작업들",
|
||||||
|
view_all: "전체 포트폴리오 보기",
|
||||||
|
},
|
||||||
|
calculator: {
|
||||||
|
cta: { title: "프로젝트 비용 계산기", subtitle: "몇 가지 질문에 답하여 빠른 견적을 받아보세요", button: "계산기 열기" },
|
||||||
|
},
|
||||||
|
contact: {
|
||||||
|
cta: { ready: "시작할", start: "준비가", question: " 되셨나요?", subtitle: "프로젝트에 대해 알려주세요 - 하루 안에 답변드리겠습니다" },
|
||||||
|
phone: { title: "전화", number: "+82 10-1234-5678" },
|
||||||
|
email: { title: "이메일", address: "hello@smartsoltech.kr" },
|
||||||
|
telegram: { title: "텔레그램", subtitle: "@smartsoltech" },
|
||||||
|
form: {
|
||||||
|
title: "빠른 연락",
|
||||||
|
name: "성함",
|
||||||
|
email: "이메일",
|
||||||
|
phone: "전화번호 (선택사항)",
|
||||||
|
service: {
|
||||||
|
select: "어떤 서비스에 관심이 있으신가요?",
|
||||||
|
web: "웹 개발",
|
||||||
|
mobile: "모바일 앱",
|
||||||
|
design: "UI/UX 디자인",
|
||||||
|
branding: "브랜딩",
|
||||||
|
consulting: "컨설팅",
|
||||||
|
other: "기타",
|
||||||
|
},
|
||||||
|
message: "프로젝트에 대해 알려주세요",
|
||||||
|
submit: "메시지 보내기",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
common: { view_details: "자세히 보기" },
|
||||||
|
theme: { toggle: "테마 변경" }
|
||||||
|
};
|
||||||
|
|
||||||
|
const services = [
|
||||||
|
{ icon: <Code2 className="w-10 h-10" />, title: t.services.web.title, description: t.services.web.description, price: t.services.web.price, delay: 0 },
|
||||||
|
{ icon: <Smartphone className="w-10 h-10" />, title: t.services.mobile.title, description: t.services.mobile.description, price: t.services.mobile.price, delay: 100 },
|
||||||
|
{ icon: <Palette className="w-10 h-10" />, title: t.services.design.title, description: t.services.design.description, price: t.services.design.price, delay: 200 },
|
||||||
|
{ icon: <LineChart className="w-10 h-10" />, title: t.services.marketing.title, description: t.services.marketing.description, price: t.services.marketing.price, delay: 300 },
|
||||||
|
];
|
||||||
|
|
||||||
|
const demoPortfolio = [
|
||||||
|
{
|
||||||
|
_id: "p1",
|
||||||
|
title: "Fintech Dashboard",
|
||||||
|
category: "Web App",
|
||||||
|
images: [{ url: "https://picsum.photos/seed/fin/800/450", isPrimary: true }],
|
||||||
|
technologies: ["React", "Tailwind", "Node"],
|
||||||
|
description: "Realtime analytics with role‑based access.",
|
||||||
|
shortDescription: "Realtime analytics with role‑based access.",
|
||||||
|
viewCount: 423,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_id: "p2",
|
||||||
|
title: "Wellness Tracker",
|
||||||
|
category: "Mobile",
|
||||||
|
images: [{ url: "https://picsum.photos/seed/well/800/450", isPrimary: true }],
|
||||||
|
technologies: ["Kotlin", "Compose"],
|
||||||
|
description: "Personal health metrics and reminders.",
|
||||||
|
shortDescription: "Personal health metrics and reminders.",
|
||||||
|
viewCount: 1189,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_id: "p3",
|
||||||
|
title: "Brand Revamp",
|
||||||
|
category: "Branding",
|
||||||
|
images: [{ url: "https://picsum.photos/seed/brand/800/450", isPrimary: true }],
|
||||||
|
technologies: ["Figma", "Design System"],
|
||||||
|
description: "Identity, components, and marketing site.",
|
||||||
|
shortDescription: "Identity, components, and marketing site.",
|
||||||
|
viewCount: 762,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function LandingDemo() {
|
||||||
|
useEffect(() => {
|
||||||
|
AOS.init({ duration: 800, easing: "ease-in-out", once: true });
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50 dark:bg-gray-900">
|
||||||
|
{/* Global styles for blob animation */}
|
||||||
|
<style jsx global>{`
|
||||||
|
@keyframes blob { 0% { transform: translate(0px,0px) scale(1); } 33% { transform: translate(20px,-30px) scale(1.1);} 66% { transform: translate(-10px,20px) scale(0.9);} 100% { transform: translate(0px,0px) scale(1);} }
|
||||||
|
.animate-blob { animation: blob 12s infinite; }
|
||||||
|
.animation-delay-2000 { animation-delay: 2s; }
|
||||||
|
.animation-delay-4000 { animation-delay: 4s; }
|
||||||
|
.glass-effect { backdrop-filter: blur(6px); }
|
||||||
|
.hero-section::before { content: ""; position: absolute; inset: 0; background: radial-gradient(1200px 600px at 10% -10%, rgba(99,102,241,.35), transparent 60%), radial-gradient(1000px 500px at 110% 10%, rgba(168,85,247,.35), transparent 60%); pointer-events:none; }
|
||||||
|
.cta-section { background: radial-gradient(1200px 600px at 10% 10%, rgba(59,130,246,.25), transparent 60%), linear-gradient(90deg, #3730a3, #7c3aed); }
|
||||||
|
.form-input { outline: none; }
|
||||||
|
`}</style>
|
||||||
|
|
||||||
|
{/* Hero */}
|
||||||
|
<section className="relative bg-gradient-to-br from-blue-900 via-purple-900 to-indigo-900 dark:from-gray-900 dark:via-blue-900 dark:to-purple-900 min-h-screen flex items-center overflow-hidden hero-section">
|
||||||
|
<div className="absolute inset-0 bg-black opacity-50 dark:opacity-70"></div>
|
||||||
|
<div className="absolute inset-0 bg-gradient-to-r from-blue-600/20 to-purple-600/20"></div>
|
||||||
|
|
||||||
|
{/* Animated blobs */}
|
||||||
|
<div className="absolute inset-0 overflow-hidden">
|
||||||
|
<div className="absolute -top-40 -right-32 w-80 h-80 bg-purple-500 dark:bg-purple-400 rounded-full mix-blend-multiply filter blur-xl opacity-70 animate-blob"></div>
|
||||||
|
<div className="absolute -bottom-40 -left-32 w-80 h-80 bg-blue-500 dark:bg-blue-400 rounded-full mix-blend-multiply filter blur-xl opacity-70 animate-blob animation-delay-2000"></div>
|
||||||
|
<div className="absolute top-40 left-40 w-80 h-80 bg-indigo-500 dark:bg-indigo-400 rounded-full mix-blend-multiply filter blur-xl opacity-70 animate-blob animation-delay-4000"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="relative max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-24 w-full">
|
||||||
|
<div className="text-center">
|
||||||
|
<h1 className="text-3xl md:text-5xl font-bold text-white mb-6 leading-tight" data-aos="fade-up">
|
||||||
|
{t.hero.title.smart} <span className="text-transparent bg-clip-text bg-gradient-to-r from-blue-400 to-purple-400">{t.hero.title.solutions}</span>
|
||||||
|
</h1>
|
||||||
|
<p className="text-xl md:text-2xl text-gray-300 dark:text-gray-200 mb-8 max-w-3xl mx-auto" data-aos="fade-up" data-aos-delay="200">
|
||||||
|
{t.hero.subtitle}
|
||||||
|
</p>
|
||||||
|
<div className="flex flex-col sm:flex-row gap-4 justify-center items-center" data-aos="fade-up" data-aos-delay="400">
|
||||||
|
<a href="#contact" className="bg-gradient-to-r from-blue-500 to-purple-600 text-white px-8 py-4 rounded-full text-lg font-semibold hover:from-blue-600 hover:to-purple-700 transition-all duration-300 transform hover:scale-105 shadow-lg">
|
||||||
|
{t.hero.cta.start}
|
||||||
|
</a>
|
||||||
|
<a href="#portfolio" className="border-2 border-white text-white px-8 py-4 rounded-full text-lg font-semibold hover:bg-white hover:text-gray-900 transition-all duration-300 transform hover:scale-105">
|
||||||
|
{t.hero.cta.portfolio}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Scroll indicator */}
|
||||||
|
<div className="absolute bottom-20 left-1/2 -translate-x-1/2 animate-bounce">
|
||||||
|
<svg className="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 14l-7 7m0 0l-7-7m7 7V3"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Services */}
|
||||||
|
<section className="py-20 bg-white dark:bg-gray-900" id="services">
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
<div className="text-center mb-16" data-aos="fade-up">
|
||||||
|
<h2 className="text-2xl md:text-3xl font-bold text-gray-900 dark:text-white mb-4">
|
||||||
|
{t.services.title.our} <span className="text-blue-600 dark:text-blue-400">{t.services.title.services}</span>
|
||||||
|
</h2>
|
||||||
|
<p className="text-lg text-gray-600 dark:text-gray-300 text-center max-w-3xl mx-auto">
|
||||||
|
{t.services.subtitle}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8">
|
||||||
|
{services.map((s, i) => (
|
||||||
|
<div key={i} className="group p-8 bg-gray-50 dark:bg-gray-800 rounded-2xl hover:bg-white dark:hover:bg-gray-700 hover:shadow-xl transition-all duration-300 transform hover:-translate-y-2" data-aos="fade-up" data-aos-delay={s.delay}>
|
||||||
|
<div className="text-blue-600 dark:text-blue-400 mb-4 group-hover:scale-110 transition-transform duration-300">{s.icon}</div>
|
||||||
|
<h3 className="text-xl font-bold text-gray-900 dark:text-white mb-3">{s.title}</h3>
|
||||||
|
<p className="text-gray-600 dark:text-gray-300 mb-4">{s.description}</p>
|
||||||
|
<div className="text-blue-600 dark:text-blue-400 font-semibold">{s.price}</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-center mt-12" data-aos="fade-up">
|
||||||
|
<a href="#services" className="inline-flex items-center px-6 py-3 border border-blue-600 dark:border-blue-400 text-blue-600 dark:text-blue-400 font-semibold rounded-lg hover:bg-blue-600 hover:text-white dark:hover:bg-blue-400 dark:hover:text-gray-900 transition-colors">
|
||||||
|
{t.services.view_all}
|
||||||
|
<ArrowRight className="ml-2 w-5 h-5" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Portfolio */}
|
||||||
|
<section className="py-20 bg-gray-50 dark:bg-gray-800" id="portfolio">
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
<div className="text-center mb-16" data-aos="fade-up">
|
||||||
|
<h2 className="text-2xl md:text-3xl font-bold text-gray-900 dark:text-white mb-4">
|
||||||
|
{t.portfolio.title.recent} <span className="text-purple-600 dark:text-purple-400">{t.portfolio.title.projects}</span>
|
||||||
|
</h2>
|
||||||
|
<p className="text-lg text-gray-600 dark:text-gray-300 text-center max-w-3xl mx-auto">
|
||||||
|
{t.portfolio.subtitle}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||||
|
{demoPortfolio.map((project, index) => (
|
||||||
|
<div key={project._id} className="group bg-white dark:bg-gray-700 rounded-2xl overflow-hidden shadow-lg hover:shadow-2xl transition-all duration-300 transform hover:-translate-y-2" data-aos="fade-up" data-aos-delay={index * 100}>
|
||||||
|
<div className="relative overflow-hidden">
|
||||||
|
<img src={project.images?.[0]?.url} alt={project.title} className="w-full h-48 object-cover" />
|
||||||
|
<div className="absolute inset-0 bg-gradient-to-t from-black/50 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
|
||||||
|
<div className="absolute bottom-4 left-4 right-4 translate-y-4 group-hover:translate-y-0 transition-transform duration-300 opacity-0 group-hover:opacity-100">
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{project.technologies?.slice(0, 3).map((tech) => (
|
||||||
|
<span key={tech} className="px-2 py-1 bg-white/20 glass-effect text-white text-xs rounded-full">{tech}</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="p-6">
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<span className="px-3 py-1 bg-blue-100 dark:bg-blue-900 text-blue-600 dark:text-blue-300 text-sm rounded-full font-medium">{project.category}</span>
|
||||||
|
<span className="text-gray-500 dark:text-gray-400 text-sm flex items-center"><Eye className="w-4 h-4 mr-1" />{project.viewCount ?? 0}</span>
|
||||||
|
</div>
|
||||||
|
<h3 className="text-xl font-bold text-gray-900 dark:text-white mb-2 group-hover:text-blue-600 dark:group-hover:text-blue-400 transition-colors">{project.title}</h3>
|
||||||
|
<p className="text-gray-600 dark:text-gray-300 mb-4">{project.shortDescription ?? project.description}</p>
|
||||||
|
<a href="#" className="inline-flex items-center text-blue-600 dark:text-blue-400 font-semibold hover:text-blue-700 dark:hover:text-blue-300 transition-colors">
|
||||||
|
{t.common.view_details}
|
||||||
|
<ArrowRight className="ml-1 w-4 h-4" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-center mt-12" data-aos="fade-up">
|
||||||
|
<a href="#portfolio" className="inline-flex items-center px-6 py-3 bg-purple-600 dark:bg-purple-500 text-white font-semibold rounded-lg hover:bg-purple-700 dark:hover:bg-purple-400 transition-colors">
|
||||||
|
{t.portfolio.view_all}
|
||||||
|
<ArrowRight className="ml-2 w-5 h-5" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Calculator CTA */}
|
||||||
|
<section className="py-20 cta-section">
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 text-center relative z-10">
|
||||||
|
<div data-aos="fade-up">
|
||||||
|
<h2 className="text-2xl md:text-3xl font-bold text-white mb-6">{t.calculator.cta.title}</h2>
|
||||||
|
<p className="text-lg text-blue-100 mb-8 max-w-3xl mx-auto">{t.calculator.cta.subtitle}</p>
|
||||||
|
<a href="#calculator" className="inline-flex items-center px-8 py-4 bg-white text-blue-600 font-bold rounded-full text-lg hover:bg-gray-100 transition-colors transform hover:scale-105">
|
||||||
|
<Calculator className="mr-3" />
|
||||||
|
{t.calculator.cta.button}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Contact */}
|
||||||
|
<section className="py-20 bg-white dark:bg-gray-900" id="contact">
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 items-center">
|
||||||
|
<div data-aos="fade-right">
|
||||||
|
<h2 className="text-2xl md:text-3xl font-bold text-gray-900 dark:text-white mb-6">
|
||||||
|
{t.contact.cta.ready} <span className="text-blue-600 dark:text-blue-400">{t.contact.cta.start}</span>
|
||||||
|
{t.contact.cta.question}
|
||||||
|
</h2>
|
||||||
|
<p className="text-lg text-gray-600 dark:text-gray-300 mb-8">{t.contact.cta.subtitle}</p>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="w-12 h-12 bg-blue-100 dark:bg-blue-900 rounded-full flex items-center justify-center mr-4">
|
||||||
|
<svg viewBox="0 0 24 24" className="w-6 h-6 text-blue-600 dark:text-blue-400" fill="none" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M3 5a2 2 0 012-2h3.28a2 2 0 011.94 1.515l.72 2.885a2 2 0 01-.52 1.94L9.12 11.88a16.001 16.001 0 006.999 6.999l1.54-1.28a2 2 0 011.94-.52l2.885.72A2 2 0 0121 20.72V24a2 2 0 01-2 2h-1C9.163 26 2 18.837 2 10V9a2 2 0 011-1.732V5z"/></svg>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="font-semibold text-gray-900 dark:text-white">{t.contact.phone.title}</div>
|
||||||
|
<div className="text-gray-600 dark:text-gray-300">{t.contact.phone.number}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="w-12 h-12 bg-purple-100 dark:bg-purple-900 rounded-full flex items-center justify-center mr-4">
|
||||||
|
<svg viewBox="0 0 24 24" className="w-6 h-6 text-purple-600 dark:text-purple-400" fill="none" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M16 12H8m8 4H8m8-8H8M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/></svg>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="font-semibold text-gray-900 dark:text-white">{t.contact.email.title}</div>
|
||||||
|
<div className="text-gray-600 dark:text-gray-300">{t.contact.email.address}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="w-12 h-12 bg-green-100 dark:bg-green-900 rounded-full flex items-center justify-center mr-4">
|
||||||
|
<svg viewBox="0 0 24 24" className="w-6 h-6 text-green-600 dark:text-green-400" fill="none" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 11c1.657 0 3-1.343 3-3S13.657 5 12 5 9 6.343 9 8s1.343 3 3 3zm-7 9a7 7 0 1114 0H5z"/></svg>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="font-semibold text-gray-900 dark:text-white">{t.contact.telegram.title}</div>
|
||||||
|
<div className="text-gray-600 dark:text-gray-300">{t.contact.telegram.subtitle}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div data-aos="fade-left">
|
||||||
|
<div className="p-8 rounded-2xl shadow-lg bg-white dark:bg-gray-800">
|
||||||
|
<h3 className="text-2xl font-bold text-gray-900 dark:text-white mb-6">{t.contact.form.title}</h3>
|
||||||
|
<form className="space-y-4" onSubmit={(e) => e.preventDefault()}>
|
||||||
|
<input type="text" placeholder={t.contact.form.name} required className="w-full px-4 py-3 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:bg-gray-700 dark:text-white" />
|
||||||
|
<input type="email" placeholder={t.contact.form.email} required className="w-full px-4 py-3 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:bg-gray-700 dark:text-white" />
|
||||||
|
<input type="tel" placeholder={t.contact.form.phone} className="w-full px-4 py-3 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:bg-gray-700 dark:text-white" />
|
||||||
|
<select className="w-full px-4 py-3 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:bg-gray-700 dark:text-white">
|
||||||
|
<option value="">{t.contact.form.service.select}</option>
|
||||||
|
<option value="web">{t.contact.form.service.web}</option>
|
||||||
|
<option value="mobile">{t.contact.form.service.mobile}</option>
|
||||||
|
<option value="design">{t.contact.form.service.design}</option>
|
||||||
|
<option value="branding">{t.contact.form.service.branding}</option>
|
||||||
|
<option value="consulting">{t.contact.form.service.consulting}</option>
|
||||||
|
<option value="other">{t.contact.form.service.other}</option>
|
||||||
|
</select>
|
||||||
|
<textarea rows={4} placeholder={t.contact.form.message} required className="w-full px-4 py-3 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent resize-none dark:bg-gray-700 dark:text-white"></textarea>
|
||||||
|
<button type="submit" className="w-full bg-gradient-to-r from-blue-600 to-purple-600 text-white font-semibold py-3 rounded-lg hover:from-blue-700 hover:to-purple-700 transition-all duration-300 transform hover:scale-105">
|
||||||
|
{t.contact.form.submit}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
517
.history/views/LandingDemo_20251026210038.tsx
Normal file
@@ -0,0 +1,517 @@
|
|||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import {
|
||||||
|
ArrowRight, Calculator, Code2, Smartphone, Palette, LineChart, Eye, Menu, X,
|
||||||
|
Sun, Moon, ChevronDown, Phone, Mail, MessageCircle, Rocket, Users, Clock,
|
||||||
|
Headphones, Award
|
||||||
|
} from "lucide-react";
|
||||||
|
import AOS from "aos";
|
||||||
|
import "aos/dist/aos.css";
|
||||||
|
|
||||||
|
// Korean i18n strings to replace <%- __('...') %>
|
||||||
|
const t = {
|
||||||
|
navigation: {
|
||||||
|
home: "홈",
|
||||||
|
about: "회사소개",
|
||||||
|
services: "서비스",
|
||||||
|
portfolio: "포트폴리오",
|
||||||
|
contact: "연락처",
|
||||||
|
calculator: "비용계산기"
|
||||||
|
},
|
||||||
|
language: {
|
||||||
|
korean: "한국어",
|
||||||
|
english: "English",
|
||||||
|
russian: "Русский",
|
||||||
|
kazakh: "Қазақша"
|
||||||
|
},
|
||||||
|
hero: {
|
||||||
|
title: { smart: "스마트", solutions: "솔루션" },
|
||||||
|
subtitle: "혁신적인 기술로 비즈니스를 성장시키세요",
|
||||||
|
description: "비즈니스의 디지털 전환을 이끄는 혁신적인 웹개발, 모바일 앱, UI/UX 디자인",
|
||||||
|
cta: { start: "시작하기", portfolio: "포트폴리오 보기" },
|
||||||
|
},
|
||||||
|
services: {
|
||||||
|
title: { our: "우리의", services: "서비스" },
|
||||||
|
subtitle: "아이디어를 현실로 만드는 전문 개발 서비스",
|
||||||
|
description: "최첨단 기술과 창의적 아이디어로 완성하는 디지털 솔루션",
|
||||||
|
view_all: "모든 서비스 보기",
|
||||||
|
web: { title: "웹 개발", description: "반응형 웹사이트 및 웹 애플리케이션 개발", price: "500,000원부터" },
|
||||||
|
mobile: { title: "모바일 앱", description: "iOS 및 Android 네이티브 앱 개발", price: "1,000,000원부터" },
|
||||||
|
design: { title: "UI/UX 디자인", description: "사용자 중심의 인터페이스 및 경험 디자인", price: "300,000원부터" },
|
||||||
|
marketing: { title: "디지털 마케팅", description: "SEO, 소셜미디어 마케팅, 광고 관리", price: "200,000원부터" },
|
||||||
|
hero: {
|
||||||
|
title: "우리의",
|
||||||
|
title_highlight: "서비스",
|
||||||
|
subtitle: "혁신적인 기술로 비즈니스 성장 지원"
|
||||||
|
},
|
||||||
|
cards: {
|
||||||
|
starting_price: "시작 가격",
|
||||||
|
consultation: "상담",
|
||||||
|
calculate_cost: "비용 계산",
|
||||||
|
popular: "인기"
|
||||||
|
},
|
||||||
|
process: {
|
||||||
|
title: "프로젝트 수행 과정",
|
||||||
|
subtitle: "체계적이고 전문적인 프로세스로 프로젝트를 진행합니다",
|
||||||
|
step1: { title: "상담 및 기획", description: "고객의 요구사항을 정확히 파악하고 최적의 솔루션을 기획합니다" },
|
||||||
|
step2: { title: "디자인 및 설계", description: "사용자 중심의 직관적인 디자인과 견고한 시스템 아키텍처를 설계합니다" },
|
||||||
|
step3: { title: "개발 및 구현", description: "최신 기술과 모범 사례를 활용하여 효율적이고 확장 가능한 솔루션을 개발합니다" },
|
||||||
|
step4: { title: "테스트 및 배포", description: "철저한 테스트를 통해 품질을 보장하고 안정적인 배포를 진행합니다" }
|
||||||
|
},
|
||||||
|
why_choose: {
|
||||||
|
title: "왜 SmartSolTech를 선택해야 할까요?",
|
||||||
|
modern_tech: { title: "최신 기술 활용", description: "항상 최신 기술 트렌드를 파악하고, 검증된 기술 스택을 활용하여 미래 지향적인 솔루션을 제공합니다." },
|
||||||
|
expert_team: { title: "전문가 팀", description: "각 분야의 전문가들로 구성된 팀이 협력하여 최고 품질의 결과물을 보장합니다." },
|
||||||
|
fast_response: { title: "빠른 대응", description: "신속한 의사소통과 빠른 피드백을 통해 프로젝트를 효율적으로 진행합니다." },
|
||||||
|
continuous_support: { title: "지속적인 지원", description: "프로젝트 완료 후에도 지속적인 유지보수와 기술 지원을 제공합니다." },
|
||||||
|
quality_guarantee: { title: "품질 보장", subtitle: "최고 품질의 결과물을 약속드립니다" }
|
||||||
|
},
|
||||||
|
cta: {
|
||||||
|
title: "프로젝트를 시작할 준비가 되셨나요?",
|
||||||
|
subtitle: "지금 바로 무료 상담을 받아보세요",
|
||||||
|
calculate_cost: "비용 계산하기",
|
||||||
|
view_portfolio: "포트폴리오 보기"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
portfolio: {
|
||||||
|
title: { recent: "최근", projects: "프로젝트" },
|
||||||
|
subtitle: "웹, 모바일, 브랜딩 분야의 선별된 작업들",
|
||||||
|
view_all: "전체 포트폴리오 보기",
|
||||||
|
},
|
||||||
|
calculator: {
|
||||||
|
cta: { title: "프로젝트 비용 계산기", subtitle: "몇 가지 질문에 답하여 빠른 견적을 받아보세요", button: "계산기 열기" },
|
||||||
|
},
|
||||||
|
contact: {
|
||||||
|
cta: { ready: "시작할", start: "준비가", question: " 되셨나요?", subtitle: "프로젝트에 대해 알려주세요 - 하루 안에 답변드리겠습니다" },
|
||||||
|
phone: { title: "전화", number: "+82 10-1234-5678" },
|
||||||
|
email: { title: "이메일", address: "hello@smartsoltech.kr" },
|
||||||
|
telegram: { title: "텔레그램", subtitle: "@smartsoltech" },
|
||||||
|
form: {
|
||||||
|
title: "빠른 연락",
|
||||||
|
name: "성함",
|
||||||
|
email: "이메일",
|
||||||
|
phone: "전화번호 (선택사항)",
|
||||||
|
service: {
|
||||||
|
select: "어떤 서비스에 관심이 있으신가요?",
|
||||||
|
web: "웹 개발",
|
||||||
|
mobile: "모바일 앱",
|
||||||
|
design: "UI/UX 디자인",
|
||||||
|
branding: "브랜딩",
|
||||||
|
consulting: "컨설팅",
|
||||||
|
other: "기타",
|
||||||
|
},
|
||||||
|
message: "프로젝트에 대해 알려주세요",
|
||||||
|
submit: "메시지 보내기",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
common: { view_details: "자세히 보기" },
|
||||||
|
theme: { toggle: "테마 변경" }
|
||||||
|
};
|
||||||
|
|
||||||
|
const services = [
|
||||||
|
{ icon: <Code2 className="w-10 h-10" />, title: t.services.web.title, description: t.services.web.description, price: t.services.web.price, delay: 0, category: "개발" },
|
||||||
|
{ icon: <Smartphone className="w-10 h-10" />, title: t.services.mobile.title, description: t.services.mobile.description, price: t.services.mobile.price, delay: 100, category: "모바일" },
|
||||||
|
{ icon: <Palette className="w-10 h-10" />, title: t.services.design.title, description: t.services.design.description, price: t.services.design.price, delay: 200, category: "디자인" },
|
||||||
|
{ icon: <LineChart className="w-10 h-10" />, title: t.services.marketing.title, description: t.services.marketing.description, price: t.services.marketing.price, delay: 300, category: "마케팅" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const demoPortfolio = [
|
||||||
|
{
|
||||||
|
_id: "p1",
|
||||||
|
title: "핀테크 대시보드",
|
||||||
|
category: "웹앱",
|
||||||
|
images: [{ url: "https://picsum.photos/seed/fin/800/450", isPrimary: true }],
|
||||||
|
technologies: ["React", "Tailwind", "Node"],
|
||||||
|
description: "역할 기반 접근 제어를 갖춘 실시간 분석",
|
||||||
|
shortDescription: "역할 기반 접근 제어를 갖춘 실시간 분석",
|
||||||
|
viewCount: 423,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_id: "p2",
|
||||||
|
title: "웰니스 트래커",
|
||||||
|
category: "모바일",
|
||||||
|
images: [{ url: "https://picsum.photos/seed/well/800/450", isPrimary: true }],
|
||||||
|
technologies: ["Kotlin", "Compose"],
|
||||||
|
description: "개인 건강 지표 및 알림",
|
||||||
|
shortDescription: "개인 건강 지표 및 알림",
|
||||||
|
viewCount: 1189,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_id: "p3",
|
||||||
|
title: "브랜드 리뉴얼",
|
||||||
|
category: "브랜딩",
|
||||||
|
images: [{ url: "https://picsum.photos/seed/brand/800/450", isPrimary: true }],
|
||||||
|
technologies: ["Figma", "Design System"],
|
||||||
|
description: "아이덴티티, 컴포넌트 및 마케팅 사이트",
|
||||||
|
shortDescription: "아이덴티티, 컴포넌트 및 마케팅 사이트",
|
||||||
|
viewCount: 762,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function LandingDemo() {
|
||||||
|
const [darkMode, setDarkMode] = useState(false);
|
||||||
|
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
||||||
|
const [currentLanguage, setCurrentLanguage] = useState('ko');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
AOS.init({ duration: 800, easing: "ease-in-out", once: true });
|
||||||
|
|
||||||
|
// Check for saved theme preference or default to light mode
|
||||||
|
const savedTheme = localStorage.getItem('theme');
|
||||||
|
if (savedTheme === 'dark' || (!savedTheme && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
|
||||||
|
setDarkMode(true);
|
||||||
|
document.documentElement.classList.add('dark');
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const toggleTheme = () => {
|
||||||
|
setDarkMode(!darkMode);
|
||||||
|
if (!darkMode) {
|
||||||
|
document.documentElement.classList.add('dark');
|
||||||
|
localStorage.setItem('theme', 'dark');
|
||||||
|
} else {
|
||||||
|
document.documentElement.classList.remove('dark');
|
||||||
|
localStorage.setItem('theme', 'light');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleMobileMenu = () => {
|
||||||
|
setMobileMenuOpen(!mobileMenuOpen);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50 dark:bg-gray-900">
|
||||||
|
{/* Global styles for blob animation */}
|
||||||
|
<style jsx global>{`
|
||||||
|
@keyframes blob { 0% { transform: translate(0px,0px) scale(1); } 33% { transform: translate(20px,-30px) scale(1.1);} 66% { transform: translate(-10px,20px) scale(0.9);} 100% { transform: translate(0px,0px) scale(1);} }
|
||||||
|
.animate-blob { animation: blob 12s infinite; }
|
||||||
|
.animation-delay-2000 { animation-delay: 2s; }
|
||||||
|
.animation-delay-4000 { animation-delay: 4s; }
|
||||||
|
.glass-effect { backdrop-filter: blur(6px); }
|
||||||
|
.hero-section::before { content: ""; position: absolute; inset: 0; background: radial-gradient(1200px 600px at 10% -10%, rgba(99,102,241,.35), transparent 60%), radial-gradient(1000px 500px at 110% 10%, rgba(168,85,247,.35), transparent 60%); pointer-events:none; }
|
||||||
|
.cta-section { background: radial-gradient(1200px 600px at 10% 10%, rgba(59,130,246,.25), transparent 60%), linear-gradient(90deg, #3730a3, #7c3aed); }
|
||||||
|
.form-input { outline: none; }
|
||||||
|
.theme-toggle-slider { transform: translateX(0); }
|
||||||
|
.dark .theme-toggle-slider { transform: translateX(1.75rem); }
|
||||||
|
.theme-sun-icon { opacity: 1; }
|
||||||
|
.dark .theme-sun-icon { opacity: 0; transform: rotate(180deg); }
|
||||||
|
.theme-moon-icon { opacity: 0; transform: rotate(-180deg); }
|
||||||
|
.dark .theme-moon-icon { opacity: 1; transform: rotate(0deg); }
|
||||||
|
`}</style>
|
||||||
|
|
||||||
|
{/* Navigation */}
|
||||||
|
<nav className="bg-white dark:bg-gray-900 shadow-lg fixed w-full z-50 top-0 transition-colors duration-300">
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
<div className="flex justify-between h-16">
|
||||||
|
<div className="flex items-center">
|
||||||
|
{/* Logo */}
|
||||||
|
<a href="/" className="flex-shrink-0 flex items-center">
|
||||||
|
<img className="h-8 w-auto" src="/images/logo.png" alt="SmartSolTech" />
|
||||||
|
<span className="ml-2 text-xl font-bold text-gray-900 dark:text-white">SmartSolTech</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Desktop Navigation */}
|
||||||
|
<div className="hidden md:flex items-center space-x-6">
|
||||||
|
{/* Navigation Links */}
|
||||||
|
<a href="#hero" className="text-blue-600 border-b-2 border-blue-600 px-3 py-2 text-sm font-medium transition-colors nav-link">
|
||||||
|
{t.navigation.home}
|
||||||
|
</a>
|
||||||
|
<a href="#services" className="text-gray-700 dark:text-gray-300 hover:text-blue-600 px-3 py-2 text-sm font-medium transition-colors nav-link">
|
||||||
|
{t.navigation.services}
|
||||||
|
</a>
|
||||||
|
<a href="#portfolio" className="text-gray-700 dark:text-gray-300 hover:text-blue-600 px-3 py-2 text-sm font-medium transition-colors nav-link">
|
||||||
|
{t.navigation.portfolio}
|
||||||
|
</a>
|
||||||
|
<a href="#calculator" className="text-gray-700 dark:text-gray-300 hover:text-blue-600 px-3 py-2 text-sm font-medium transition-colors nav-link">
|
||||||
|
{t.navigation.calculator}
|
||||||
|
</a>
|
||||||
|
<a href="#contact" className="text-gray-700 dark:text-gray-300 hover:text-blue-600 px-3 py-2 text-sm font-medium transition-colors nav-link">
|
||||||
|
{t.navigation.contact}
|
||||||
|
</a>
|
||||||
|
|
||||||
|
{/* Language Dropdown */}
|
||||||
|
<div className="relative group">
|
||||||
|
<button className="flex items-center text-gray-700 dark:text-gray-300 hover:text-blue-600 px-3 py-2 text-sm font-medium transition-colors">
|
||||||
|
<span className="mr-2">🇰🇷</span>
|
||||||
|
{t.language.korean}
|
||||||
|
<ChevronDown className="ml-1 w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
<div className="absolute right-0 mt-2 w-40 bg-white dark:bg-gray-800 rounded-lg shadow-lg border dark:border-gray-700 opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-200">
|
||||||
|
<a href="#" className="block px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-t-lg">
|
||||||
|
🇰🇷 {t.language.korean}
|
||||||
|
</a>
|
||||||
|
<a href="#" className="block px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700">
|
||||||
|
🇺🇸 {t.language.english}
|
||||||
|
</a>
|
||||||
|
<a href="#" className="block px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700">
|
||||||
|
🇷🇺 {t.language.russian}
|
||||||
|
</a>
|
||||||
|
<a href="#" className="block px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-b-lg">
|
||||||
|
🇰🇿 {t.language.kazakh}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Animated Theme Toggle */}
|
||||||
|
<div className="relative inline-block ml-4" title={t.theme.toggle}>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="theme-toggle"
|
||||||
|
className="sr-only"
|
||||||
|
checked={darkMode}
|
||||||
|
onChange={toggleTheme}
|
||||||
|
/>
|
||||||
|
<label htmlFor="theme-toggle" className="flex items-center cursor-pointer">
|
||||||
|
<div className="relative w-14 h-7 bg-gradient-to-r from-blue-200 to-yellow-200 dark:from-gray-700 dark:to-gray-600 rounded-full border-2 border-gray-300 dark:border-gray-500 transition-all duration-300 shadow-sm">
|
||||||
|
<div className="absolute top-0.5 left-1 w-5 h-5 bg-white dark:bg-gray-200 rounded-full shadow-md transform transition-all duration-300 flex items-center justify-center theme-toggle-slider">
|
||||||
|
<div className="relative w-full h-full flex items-center justify-center">
|
||||||
|
{/* Sun Icon */}
|
||||||
|
<Sun className="absolute w-3 h-3 text-yellow-500 theme-sun-icon transition-all duration-300 transform" />
|
||||||
|
{/* Moon Icon */}
|
||||||
|
<Moon className="absolute w-3 h-3 text-blue-500 theme-moon-icon transition-all duration-300 transform" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Mobile menu button */}
|
||||||
|
<div className="md:hidden flex items-center space-x-2">
|
||||||
|
<button
|
||||||
|
onClick={toggleMobileMenu}
|
||||||
|
className="p-2 text-gray-700 dark:text-gray-300 hover:text-blue-600 transition-colors"
|
||||||
|
>
|
||||||
|
{mobileMenuOpen ? <X className="w-6 h-6" /> : <Menu className="w-6 h-6" />}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Mobile Navigation Menu */}
|
||||||
|
{mobileMenuOpen && (
|
||||||
|
<div className="md:hidden bg-white dark:bg-gray-900 border-t dark:border-gray-700">
|
||||||
|
<div className="px-2 pt-2 pb-3 space-y-1">
|
||||||
|
<a href="#hero" className="bg-blue-50 dark:bg-blue-900 text-blue-600 block px-3 py-2 rounded-md text-base font-medium">
|
||||||
|
{t.navigation.home}
|
||||||
|
</a>
|
||||||
|
<a href="#services" className="text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 block px-3 py-2 rounded-md text-base font-medium">
|
||||||
|
{t.navigation.services}
|
||||||
|
</a>
|
||||||
|
<a href="#portfolio" className="text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 block px-3 py-2 rounded-md text-base font-medium">
|
||||||
|
{t.navigation.portfolio}
|
||||||
|
</a>
|
||||||
|
<a href="#calculator" className="text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 block px-3 py-2 rounded-md text-base font-medium">
|
||||||
|
{t.navigation.calculator}
|
||||||
|
</a>
|
||||||
|
<a href="#contact" className="text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 block px-3 py-2 rounded-md text-base font-medium">
|
||||||
|
{t.navigation.contact}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</nav>
|
||||||
|
<div className="absolute inset-0 bg-black opacity-50 dark:opacity-70"></div>
|
||||||
|
<div className="absolute inset-0 bg-gradient-to-r from-blue-600/20 to-purple-600/20"></div>
|
||||||
|
|
||||||
|
{/* Animated blobs */}
|
||||||
|
<div className="absolute inset-0 overflow-hidden">
|
||||||
|
<div className="absolute -top-40 -right-32 w-80 h-80 bg-purple-500 dark:bg-purple-400 rounded-full mix-blend-multiply filter blur-xl opacity-70 animate-blob"></div>
|
||||||
|
<div className="absolute -bottom-40 -left-32 w-80 h-80 bg-blue-500 dark:bg-blue-400 rounded-full mix-blend-multiply filter blur-xl opacity-70 animate-blob animation-delay-2000"></div>
|
||||||
|
<div className="absolute top-40 left-40 w-80 h-80 bg-indigo-500 dark:bg-indigo-400 rounded-full mix-blend-multiply filter blur-xl opacity-70 animate-blob animation-delay-4000"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="relative max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-24 w-full">
|
||||||
|
<div className="text-center">
|
||||||
|
<h1 className="text-3xl md:text-5xl font-bold text-white mb-6 leading-tight" data-aos="fade-up">
|
||||||
|
{t.hero.title.smart} <span className="text-transparent bg-clip-text bg-gradient-to-r from-blue-400 to-purple-400">{t.hero.title.solutions}</span>
|
||||||
|
</h1>
|
||||||
|
<p className="text-xl md:text-2xl text-gray-300 dark:text-gray-200 mb-8 max-w-3xl mx-auto" data-aos="fade-up" data-aos-delay="200">
|
||||||
|
{t.hero.subtitle}
|
||||||
|
</p>
|
||||||
|
<div className="flex flex-col sm:flex-row gap-4 justify-center items-center" data-aos="fade-up" data-aos-delay="400">
|
||||||
|
<a href="#contact" className="bg-gradient-to-r from-blue-500 to-purple-600 text-white px-8 py-4 rounded-full text-lg font-semibold hover:from-blue-600 hover:to-purple-700 transition-all duration-300 transform hover:scale-105 shadow-lg">
|
||||||
|
{t.hero.cta.start}
|
||||||
|
</a>
|
||||||
|
<a href="#portfolio" className="border-2 border-white text-white px-8 py-4 rounded-full text-lg font-semibold hover:bg-white hover:text-gray-900 transition-all duration-300 transform hover:scale-105">
|
||||||
|
{t.hero.cta.portfolio}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Scroll indicator */}
|
||||||
|
<div className="absolute bottom-20 left-1/2 -translate-x-1/2 animate-bounce">
|
||||||
|
<svg className="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 14l-7 7m0 0l-7-7m7 7V3"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Services */}
|
||||||
|
<section className="py-20 bg-white dark:bg-gray-900" id="services">
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
<div className="text-center mb-16" data-aos="fade-up">
|
||||||
|
<h2 className="text-2xl md:text-3xl font-bold text-gray-900 dark:text-white mb-4">
|
||||||
|
{t.services.title.our} <span className="text-blue-600 dark:text-blue-400">{t.services.title.services}</span>
|
||||||
|
</h2>
|
||||||
|
<p className="text-lg text-gray-600 dark:text-gray-300 text-center max-w-3xl mx-auto">
|
||||||
|
{t.services.subtitle}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8">
|
||||||
|
{services.map((s, i) => (
|
||||||
|
<div key={i} className="group p-8 bg-gray-50 dark:bg-gray-800 rounded-2xl hover:bg-white dark:hover:bg-gray-700 hover:shadow-xl transition-all duration-300 transform hover:-translate-y-2" data-aos="fade-up" data-aos-delay={s.delay}>
|
||||||
|
<div className="text-blue-600 dark:text-blue-400 mb-4 group-hover:scale-110 transition-transform duration-300">{s.icon}</div>
|
||||||
|
<h3 className="text-xl font-bold text-gray-900 dark:text-white mb-3">{s.title}</h3>
|
||||||
|
<p className="text-gray-600 dark:text-gray-300 mb-4">{s.description}</p>
|
||||||
|
<div className="text-blue-600 dark:text-blue-400 font-semibold">{s.price}</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-center mt-12" data-aos="fade-up">
|
||||||
|
<a href="#services" className="inline-flex items-center px-6 py-3 border border-blue-600 dark:border-blue-400 text-blue-600 dark:text-blue-400 font-semibold rounded-lg hover:bg-blue-600 hover:text-white dark:hover:bg-blue-400 dark:hover:text-gray-900 transition-colors">
|
||||||
|
{t.services.view_all}
|
||||||
|
<ArrowRight className="ml-2 w-5 h-5" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Portfolio */}
|
||||||
|
<section className="py-20 bg-gray-50 dark:bg-gray-800" id="portfolio">
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
<div className="text-center mb-16" data-aos="fade-up">
|
||||||
|
<h2 className="text-2xl md:text-3xl font-bold text-gray-900 dark:text-white mb-4">
|
||||||
|
{t.portfolio.title.recent} <span className="text-purple-600 dark:text-purple-400">{t.portfolio.title.projects}</span>
|
||||||
|
</h2>
|
||||||
|
<p className="text-lg text-gray-600 dark:text-gray-300 text-center max-w-3xl mx-auto">
|
||||||
|
{t.portfolio.subtitle}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||||
|
{demoPortfolio.map((project, index) => (
|
||||||
|
<div key={project._id} className="group bg-white dark:bg-gray-700 rounded-2xl overflow-hidden shadow-lg hover:shadow-2xl transition-all duration-300 transform hover:-translate-y-2" data-aos="fade-up" data-aos-delay={index * 100}>
|
||||||
|
<div className="relative overflow-hidden">
|
||||||
|
<img src={project.images?.[0]?.url} alt={project.title} className="w-full h-48 object-cover" />
|
||||||
|
<div className="absolute inset-0 bg-gradient-to-t from-black/50 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
|
||||||
|
<div className="absolute bottom-4 left-4 right-4 translate-y-4 group-hover:translate-y-0 transition-transform duration-300 opacity-0 group-hover:opacity-100">
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{project.technologies?.slice(0, 3).map((tech) => (
|
||||||
|
<span key={tech} className="px-2 py-1 bg-white/20 glass-effect text-white text-xs rounded-full">{tech}</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="p-6">
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<span className="px-3 py-1 bg-blue-100 dark:bg-blue-900 text-blue-600 dark:text-blue-300 text-sm rounded-full font-medium">{project.category}</span>
|
||||||
|
<span className="text-gray-500 dark:text-gray-400 text-sm flex items-center"><Eye className="w-4 h-4 mr-1" />{project.viewCount ?? 0}</span>
|
||||||
|
</div>
|
||||||
|
<h3 className="text-xl font-bold text-gray-900 dark:text-white mb-2 group-hover:text-blue-600 dark:group-hover:text-blue-400 transition-colors">{project.title}</h3>
|
||||||
|
<p className="text-gray-600 dark:text-gray-300 mb-4">{project.shortDescription ?? project.description}</p>
|
||||||
|
<a href="#" className="inline-flex items-center text-blue-600 dark:text-blue-400 font-semibold hover:text-blue-700 dark:hover:text-blue-300 transition-colors">
|
||||||
|
{t.common.view_details}
|
||||||
|
<ArrowRight className="ml-1 w-4 h-4" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-center mt-12" data-aos="fade-up">
|
||||||
|
<a href="#portfolio" className="inline-flex items-center px-6 py-3 bg-purple-600 dark:bg-purple-500 text-white font-semibold rounded-lg hover:bg-purple-700 dark:hover:bg-purple-400 transition-colors">
|
||||||
|
{t.portfolio.view_all}
|
||||||
|
<ArrowRight className="ml-2 w-5 h-5" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Calculator CTA */}
|
||||||
|
<section className="py-20 cta-section">
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 text-center relative z-10">
|
||||||
|
<div data-aos="fade-up">
|
||||||
|
<h2 className="text-2xl md:text-3xl font-bold text-white mb-6">{t.calculator.cta.title}</h2>
|
||||||
|
<p className="text-lg text-blue-100 mb-8 max-w-3xl mx-auto">{t.calculator.cta.subtitle}</p>
|
||||||
|
<a href="#calculator" className="inline-flex items-center px-8 py-4 bg-white text-blue-600 font-bold rounded-full text-lg hover:bg-gray-100 transition-colors transform hover:scale-105">
|
||||||
|
<Calculator className="mr-3" />
|
||||||
|
{t.calculator.cta.button}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Contact */}
|
||||||
|
<section className="py-20 bg-white dark:bg-gray-900" id="contact">
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 items-center">
|
||||||
|
<div data-aos="fade-right">
|
||||||
|
<h2 className="text-2xl md:text-3xl font-bold text-gray-900 dark:text-white mb-6">
|
||||||
|
{t.contact.cta.ready} <span className="text-blue-600 dark:text-blue-400">{t.contact.cta.start}</span>
|
||||||
|
{t.contact.cta.question}
|
||||||
|
</h2>
|
||||||
|
<p className="text-lg text-gray-600 dark:text-gray-300 mb-8">{t.contact.cta.subtitle}</p>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="w-12 h-12 bg-blue-100 dark:bg-blue-900 rounded-full flex items-center justify-center mr-4">
|
||||||
|
<svg viewBox="0 0 24 24" className="w-6 h-6 text-blue-600 dark:text-blue-400" fill="none" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M3 5a2 2 0 012-2h3.28a2 2 0 011.94 1.515l.72 2.885a2 2 0 01-.52 1.94L9.12 11.88a16.001 16.001 0 006.999 6.999l1.54-1.28a2 2 0 011.94-.52l2.885.72A2 2 0 0121 20.72V24a2 2 0 01-2 2h-1C9.163 26 2 18.837 2 10V9a2 2 0 011-1.732V5z"/></svg>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="font-semibold text-gray-900 dark:text-white">{t.contact.phone.title}</div>
|
||||||
|
<div className="text-gray-600 dark:text-gray-300">{t.contact.phone.number}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="w-12 h-12 bg-purple-100 dark:bg-purple-900 rounded-full flex items-center justify-center mr-4">
|
||||||
|
<svg viewBox="0 0 24 24" className="w-6 h-6 text-purple-600 dark:text-purple-400" fill="none" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M16 12H8m8 4H8m8-8H8M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/></svg>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="font-semibold text-gray-900 dark:text-white">{t.contact.email.title}</div>
|
||||||
|
<div className="text-gray-600 dark:text-gray-300">{t.contact.email.address}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="w-12 h-12 bg-green-100 dark:bg-green-900 rounded-full flex items-center justify-center mr-4">
|
||||||
|
<svg viewBox="0 0 24 24" className="w-6 h-6 text-green-600 dark:text-green-400" fill="none" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 11c1.657 0 3-1.343 3-3S13.657 5 12 5 9 6.343 9 8s1.343 3 3 3zm-7 9a7 7 0 1114 0H5z"/></svg>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="font-semibold text-gray-900 dark:text-white">{t.contact.telegram.title}</div>
|
||||||
|
<div className="text-gray-600 dark:text-gray-300">{t.contact.telegram.subtitle}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div data-aos="fade-left">
|
||||||
|
<div className="p-8 rounded-2xl shadow-lg bg-white dark:bg-gray-800">
|
||||||
|
<h3 className="text-2xl font-bold text-gray-900 dark:text-white mb-6">{t.contact.form.title}</h3>
|
||||||
|
<form className="space-y-4" onSubmit={(e) => e.preventDefault()}>
|
||||||
|
<input type="text" placeholder={t.contact.form.name} required className="w-full px-4 py-3 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:bg-gray-700 dark:text-white" />
|
||||||
|
<input type="email" placeholder={t.contact.form.email} required className="w-full px-4 py-3 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:bg-gray-700 dark:text-white" />
|
||||||
|
<input type="tel" placeholder={t.contact.form.phone} className="w-full px-4 py-3 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:bg-gray-700 dark:text-white" />
|
||||||
|
<select className="w-full px-4 py-3 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:bg-gray-700 dark:text-white">
|
||||||
|
<option value="">{t.contact.form.service.select}</option>
|
||||||
|
<option value="web">{t.contact.form.service.web}</option>
|
||||||
|
<option value="mobile">{t.contact.form.service.mobile}</option>
|
||||||
|
<option value="design">{t.contact.form.service.design}</option>
|
||||||
|
<option value="branding">{t.contact.form.service.branding}</option>
|
||||||
|
<option value="consulting">{t.contact.form.service.consulting}</option>
|
||||||
|
<option value="other">{t.contact.form.service.other}</option>
|
||||||
|
</select>
|
||||||
|
<textarea rows={4} placeholder={t.contact.form.message} required className="w-full px-4 py-3 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent resize-none dark:bg-gray-700 dark:text-white"></textarea>
|
||||||
|
<button type="submit" className="w-full bg-gradient-to-r from-blue-600 to-purple-600 text-white font-semibold py-3 rounded-lg hover:from-blue-700 hover:to-purple-700 transition-all duration-300 transform hover:scale-105">
|
||||||
|
{t.contact.form.submit}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
752
.history/views/LandingDemo_20251026210146.tsx
Normal file
@@ -0,0 +1,752 @@
|
|||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import {
|
||||||
|
ArrowRight, Calculator, Code2, Smartphone, Palette, LineChart, Eye, Menu, X,
|
||||||
|
Sun, Moon, ChevronDown, Phone, Mail, MessageCircle, Rocket, Users, Clock,
|
||||||
|
Headphones, Award
|
||||||
|
} from "lucide-react";
|
||||||
|
import AOS from "aos";
|
||||||
|
import "aos/dist/aos.css";
|
||||||
|
|
||||||
|
// Korean i18n strings to replace <%- __('...') %>
|
||||||
|
const t = {
|
||||||
|
navigation: {
|
||||||
|
home: "홈",
|
||||||
|
about: "회사소개",
|
||||||
|
services: "서비스",
|
||||||
|
portfolio: "포트폴리오",
|
||||||
|
contact: "연락처",
|
||||||
|
calculator: "비용계산기"
|
||||||
|
},
|
||||||
|
language: {
|
||||||
|
korean: "한국어",
|
||||||
|
english: "English",
|
||||||
|
russian: "Русский",
|
||||||
|
kazakh: "Қазақша"
|
||||||
|
},
|
||||||
|
hero: {
|
||||||
|
title: { smart: "스마트", solutions: "솔루션" },
|
||||||
|
subtitle: "혁신적인 기술로 비즈니스를 성장시키세요",
|
||||||
|
description: "비즈니스의 디지털 전환을 이끄는 혁신적인 웹개발, 모바일 앱, UI/UX 디자인",
|
||||||
|
cta: { start: "시작하기", portfolio: "포트폴리오 보기" },
|
||||||
|
},
|
||||||
|
services: {
|
||||||
|
title: { our: "우리의", services: "서비스" },
|
||||||
|
subtitle: "아이디어를 현실로 만드는 전문 개발 서비스",
|
||||||
|
description: "최첨단 기술과 창의적 아이디어로 완성하는 디지털 솔루션",
|
||||||
|
view_all: "모든 서비스 보기",
|
||||||
|
web: { title: "웹 개발", description: "반응형 웹사이트 및 웹 애플리케이션 개발", price: "500,000원부터" },
|
||||||
|
mobile: { title: "모바일 앱", description: "iOS 및 Android 네이티브 앱 개발", price: "1,000,000원부터" },
|
||||||
|
design: { title: "UI/UX 디자인", description: "사용자 중심의 인터페이스 및 경험 디자인", price: "300,000원부터" },
|
||||||
|
marketing: { title: "디지털 마케팅", description: "SEO, 소셜미디어 마케팅, 광고 관리", price: "200,000원부터" },
|
||||||
|
hero: {
|
||||||
|
title: "우리의",
|
||||||
|
title_highlight: "서비스",
|
||||||
|
subtitle: "혁신적인 기술로 비즈니스 성장 지원"
|
||||||
|
},
|
||||||
|
cards: {
|
||||||
|
starting_price: "시작 가격",
|
||||||
|
consultation: "상담",
|
||||||
|
calculate_cost: "비용 계산",
|
||||||
|
popular: "인기"
|
||||||
|
},
|
||||||
|
process: {
|
||||||
|
title: "프로젝트 수행 과정",
|
||||||
|
subtitle: "체계적이고 전문적인 프로세스로 프로젝트를 진행합니다",
|
||||||
|
step1: { title: "상담 및 기획", description: "고객의 요구사항을 정확히 파악하고 최적의 솔루션을 기획합니다" },
|
||||||
|
step2: { title: "디자인 및 설계", description: "사용자 중심의 직관적인 디자인과 견고한 시스템 아키텍처를 설계합니다" },
|
||||||
|
step3: { title: "개발 및 구현", description: "최신 기술과 모범 사례를 활용하여 효율적이고 확장 가능한 솔루션을 개발합니다" },
|
||||||
|
step4: { title: "테스트 및 배포", description: "철저한 테스트를 통해 품질을 보장하고 안정적인 배포를 진행합니다" }
|
||||||
|
},
|
||||||
|
why_choose: {
|
||||||
|
title: "왜 SmartSolTech를 선택해야 할까요?",
|
||||||
|
modern_tech: { title: "최신 기술 활용", description: "항상 최신 기술 트렌드를 파악하고, 검증된 기술 스택을 활용하여 미래 지향적인 솔루션을 제공합니다." },
|
||||||
|
expert_team: { title: "전문가 팀", description: "각 분야의 전문가들로 구성된 팀이 협력하여 최고 품질의 결과물을 보장합니다." },
|
||||||
|
fast_response: { title: "빠른 대응", description: "신속한 의사소통과 빠른 피드백을 통해 프로젝트를 효율적으로 진행합니다." },
|
||||||
|
continuous_support: { title: "지속적인 지원", description: "프로젝트 완료 후에도 지속적인 유지보수와 기술 지원을 제공합니다." },
|
||||||
|
quality_guarantee: { title: "품질 보장", subtitle: "최고 품질의 결과물을 약속드립니다" }
|
||||||
|
},
|
||||||
|
cta: {
|
||||||
|
title: "프로젝트를 시작할 준비가 되셨나요?",
|
||||||
|
subtitle: "지금 바로 무료 상담을 받아보세요",
|
||||||
|
calculate_cost: "비용 계산하기",
|
||||||
|
view_portfolio: "포트폴리오 보기"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
portfolio: {
|
||||||
|
title: { recent: "최근", projects: "프로젝트" },
|
||||||
|
subtitle: "웹, 모바일, 브랜딩 분야의 선별된 작업들",
|
||||||
|
view_all: "전체 포트폴리오 보기",
|
||||||
|
},
|
||||||
|
calculator: {
|
||||||
|
cta: { title: "프로젝트 비용 계산기", subtitle: "몇 가지 질문에 답하여 빠른 견적을 받아보세요", button: "계산기 열기" },
|
||||||
|
},
|
||||||
|
contact: {
|
||||||
|
cta: { ready: "시작할", start: "준비가", question: " 되셨나요?", subtitle: "프로젝트에 대해 알려주세요 - 하루 안에 답변드리겠습니다" },
|
||||||
|
phone: { title: "전화", number: "+82 10-1234-5678" },
|
||||||
|
email: { title: "이메일", address: "hello@smartsoltech.kr" },
|
||||||
|
telegram: { title: "텔레그램", subtitle: "@smartsoltech" },
|
||||||
|
form: {
|
||||||
|
title: "빠른 연락",
|
||||||
|
name: "성함",
|
||||||
|
email: "이메일",
|
||||||
|
phone: "전화번호 (선택사항)",
|
||||||
|
service: {
|
||||||
|
select: "어떤 서비스에 관심이 있으신가요?",
|
||||||
|
web: "웹 개발",
|
||||||
|
mobile: "모바일 앱",
|
||||||
|
design: "UI/UX 디자인",
|
||||||
|
branding: "브랜딩",
|
||||||
|
consulting: "컨설팅",
|
||||||
|
other: "기타",
|
||||||
|
},
|
||||||
|
message: "프로젝트에 대해 알려주세요",
|
||||||
|
submit: "메시지 보내기",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
common: { view_details: "자세히 보기" },
|
||||||
|
theme: { toggle: "테마 변경" }
|
||||||
|
};
|
||||||
|
|
||||||
|
const services = [
|
||||||
|
{ icon: <Code2 className="w-10 h-10" />, title: t.services.web.title, description: t.services.web.description, price: t.services.web.price, delay: 0, category: "개발" },
|
||||||
|
{ icon: <Smartphone className="w-10 h-10" />, title: t.services.mobile.title, description: t.services.mobile.description, price: t.services.mobile.price, delay: 100, category: "모바일" },
|
||||||
|
{ icon: <Palette className="w-10 h-10" />, title: t.services.design.title, description: t.services.design.description, price: t.services.design.price, delay: 200, category: "디자인" },
|
||||||
|
{ icon: <LineChart className="w-10 h-10" />, title: t.services.marketing.title, description: t.services.marketing.description, price: t.services.marketing.price, delay: 300, category: "마케팅" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const demoPortfolio = [
|
||||||
|
{
|
||||||
|
_id: "p1",
|
||||||
|
title: "핀테크 대시보드",
|
||||||
|
category: "웹앱",
|
||||||
|
images: [{ url: "https://picsum.photos/seed/fin/800/450", isPrimary: true }],
|
||||||
|
technologies: ["React", "Tailwind", "Node"],
|
||||||
|
description: "역할 기반 접근 제어를 갖춘 실시간 분석",
|
||||||
|
shortDescription: "역할 기반 접근 제어를 갖춘 실시간 분석",
|
||||||
|
viewCount: 423,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_id: "p2",
|
||||||
|
title: "웰니스 트래커",
|
||||||
|
category: "모바일",
|
||||||
|
images: [{ url: "https://picsum.photos/seed/well/800/450", isPrimary: true }],
|
||||||
|
technologies: ["Kotlin", "Compose"],
|
||||||
|
description: "개인 건강 지표 및 알림",
|
||||||
|
shortDescription: "개인 건강 지표 및 알림",
|
||||||
|
viewCount: 1189,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_id: "p3",
|
||||||
|
title: "브랜드 리뉴얼",
|
||||||
|
category: "브랜딩",
|
||||||
|
images: [{ url: "https://picsum.photos/seed/brand/800/450", isPrimary: true }],
|
||||||
|
technologies: ["Figma", "Design System"],
|
||||||
|
description: "아이덴티티, 컴포넌트 및 마케팅 사이트",
|
||||||
|
shortDescription: "아이덴티티, 컴포넌트 및 마케팅 사이트",
|
||||||
|
viewCount: 762,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function LandingDemo() {
|
||||||
|
const [darkMode, setDarkMode] = useState(false);
|
||||||
|
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
||||||
|
const [currentLanguage, setCurrentLanguage] = useState('ko');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
AOS.init({ duration: 800, easing: "ease-in-out", once: true });
|
||||||
|
|
||||||
|
// Check for saved theme preference or default to light mode
|
||||||
|
const savedTheme = localStorage.getItem('theme');
|
||||||
|
if (savedTheme === 'dark' || (!savedTheme && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
|
||||||
|
setDarkMode(true);
|
||||||
|
document.documentElement.classList.add('dark');
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const toggleTheme = () => {
|
||||||
|
setDarkMode(!darkMode);
|
||||||
|
if (!darkMode) {
|
||||||
|
document.documentElement.classList.add('dark');
|
||||||
|
localStorage.setItem('theme', 'dark');
|
||||||
|
} else {
|
||||||
|
document.documentElement.classList.remove('dark');
|
||||||
|
localStorage.setItem('theme', 'light');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleMobileMenu = () => {
|
||||||
|
setMobileMenuOpen(!mobileMenuOpen);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50 dark:bg-gray-900">
|
||||||
|
{/* Global styles for blob animation */}
|
||||||
|
<style jsx global>{`
|
||||||
|
@keyframes blob { 0% { transform: translate(0px,0px) scale(1); } 33% { transform: translate(20px,-30px) scale(1.1);} 66% { transform: translate(-10px,20px) scale(0.9);} 100% { transform: translate(0px,0px) scale(1);} }
|
||||||
|
.animate-blob { animation: blob 12s infinite; }
|
||||||
|
.animation-delay-2000 { animation-delay: 2s; }
|
||||||
|
.animation-delay-4000 { animation-delay: 4s; }
|
||||||
|
.glass-effect { backdrop-filter: blur(6px); }
|
||||||
|
.hero-section::before { content: ""; position: absolute; inset: 0; background: radial-gradient(1200px 600px at 10% -10%, rgba(99,102,241,.35), transparent 60%), radial-gradient(1000px 500px at 110% 10%, rgba(168,85,247,.35), transparent 60%); pointer-events:none; }
|
||||||
|
.cta-section { background: radial-gradient(1200px 600px at 10% 10%, rgba(59,130,246,.25), transparent 60%), linear-gradient(90deg, #3730a3, #7c3aed); }
|
||||||
|
.form-input { outline: none; }
|
||||||
|
.theme-toggle-slider { transform: translateX(0); }
|
||||||
|
.dark .theme-toggle-slider { transform: translateX(1.75rem); }
|
||||||
|
.theme-sun-icon { opacity: 1; }
|
||||||
|
.dark .theme-sun-icon { opacity: 0; transform: rotate(180deg); }
|
||||||
|
.theme-moon-icon { opacity: 0; transform: rotate(-180deg); }
|
||||||
|
.dark .theme-moon-icon { opacity: 1; transform: rotate(0deg); }
|
||||||
|
`}</style>
|
||||||
|
|
||||||
|
{/* Navigation */}
|
||||||
|
<nav className="bg-white dark:bg-gray-900 shadow-lg fixed w-full z-50 top-0 transition-colors duration-300">
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
<div className="flex justify-between h-16">
|
||||||
|
<div className="flex items-center">
|
||||||
|
{/* Logo */}
|
||||||
|
<a href="/" className="flex-shrink-0 flex items-center">
|
||||||
|
<img className="h-8 w-auto" src="/images/logo.png" alt="SmartSolTech" />
|
||||||
|
<span className="ml-2 text-xl font-bold text-gray-900 dark:text-white">SmartSolTech</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Desktop Navigation */}
|
||||||
|
<div className="hidden md:flex items-center space-x-6">
|
||||||
|
{/* Navigation Links */}
|
||||||
|
<a href="#hero" className="text-blue-600 border-b-2 border-blue-600 px-3 py-2 text-sm font-medium transition-colors nav-link">
|
||||||
|
{t.navigation.home}
|
||||||
|
</a>
|
||||||
|
<a href="#services" className="text-gray-700 dark:text-gray-300 hover:text-blue-600 px-3 py-2 text-sm font-medium transition-colors nav-link">
|
||||||
|
{t.navigation.services}
|
||||||
|
</a>
|
||||||
|
<a href="#portfolio" className="text-gray-700 dark:text-gray-300 hover:text-blue-600 px-3 py-2 text-sm font-medium transition-colors nav-link">
|
||||||
|
{t.navigation.portfolio}
|
||||||
|
</a>
|
||||||
|
<a href="#calculator" className="text-gray-700 dark:text-gray-300 hover:text-blue-600 px-3 py-2 text-sm font-medium transition-colors nav-link">
|
||||||
|
{t.navigation.calculator}
|
||||||
|
</a>
|
||||||
|
<a href="#contact" className="text-gray-700 dark:text-gray-300 hover:text-blue-600 px-3 py-2 text-sm font-medium transition-colors nav-link">
|
||||||
|
{t.navigation.contact}
|
||||||
|
</a>
|
||||||
|
|
||||||
|
{/* Language Dropdown */}
|
||||||
|
<div className="relative group">
|
||||||
|
<button className="flex items-center text-gray-700 dark:text-gray-300 hover:text-blue-600 px-3 py-2 text-sm font-medium transition-colors">
|
||||||
|
<span className="mr-2">🇰🇷</span>
|
||||||
|
{t.language.korean}
|
||||||
|
<ChevronDown className="ml-1 w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
<div className="absolute right-0 mt-2 w-40 bg-white dark:bg-gray-800 rounded-lg shadow-lg border dark:border-gray-700 opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-200">
|
||||||
|
<a href="#" className="block px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-t-lg">
|
||||||
|
🇰🇷 {t.language.korean}
|
||||||
|
</a>
|
||||||
|
<a href="#" className="block px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700">
|
||||||
|
🇺🇸 {t.language.english}
|
||||||
|
</a>
|
||||||
|
<a href="#" className="block px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700">
|
||||||
|
🇷🇺 {t.language.russian}
|
||||||
|
</a>
|
||||||
|
<a href="#" className="block px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-b-lg">
|
||||||
|
🇰🇿 {t.language.kazakh}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Animated Theme Toggle */}
|
||||||
|
<div className="relative inline-block ml-4" title={t.theme.toggle}>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="theme-toggle"
|
||||||
|
className="sr-only"
|
||||||
|
checked={darkMode}
|
||||||
|
onChange={toggleTheme}
|
||||||
|
/>
|
||||||
|
<label htmlFor="theme-toggle" className="flex items-center cursor-pointer">
|
||||||
|
<div className="relative w-14 h-7 bg-gradient-to-r from-blue-200 to-yellow-200 dark:from-gray-700 dark:to-gray-600 rounded-full border-2 border-gray-300 dark:border-gray-500 transition-all duration-300 shadow-sm">
|
||||||
|
<div className="absolute top-0.5 left-1 w-5 h-5 bg-white dark:bg-gray-200 rounded-full shadow-md transform transition-all duration-300 flex items-center justify-center theme-toggle-slider">
|
||||||
|
<div className="relative w-full h-full flex items-center justify-center">
|
||||||
|
{/* Sun Icon */}
|
||||||
|
<Sun className="absolute w-3 h-3 text-yellow-500 theme-sun-icon transition-all duration-300 transform" />
|
||||||
|
{/* Moon Icon */}
|
||||||
|
<Moon className="absolute w-3 h-3 text-blue-500 theme-moon-icon transition-all duration-300 transform" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Mobile menu button */}
|
||||||
|
<div className="md:hidden flex items-center space-x-2">
|
||||||
|
<button
|
||||||
|
onClick={toggleMobileMenu}
|
||||||
|
className="p-2 text-gray-700 dark:text-gray-300 hover:text-blue-600 transition-colors"
|
||||||
|
>
|
||||||
|
{mobileMenuOpen ? <X className="w-6 h-6" /> : <Menu className="w-6 h-6" />}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Mobile Navigation Menu */}
|
||||||
|
{mobileMenuOpen && (
|
||||||
|
<div className="md:hidden bg-white dark:bg-gray-900 border-t dark:border-gray-700">
|
||||||
|
<div className="px-2 pt-2 pb-3 space-y-1">
|
||||||
|
<a href="#hero" className="bg-blue-50 dark:bg-blue-900 text-blue-600 block px-3 py-2 rounded-md text-base font-medium">
|
||||||
|
{t.navigation.home}
|
||||||
|
</a>
|
||||||
|
<a href="#services" className="text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 block px-3 py-2 rounded-md text-base font-medium">
|
||||||
|
{t.navigation.services}
|
||||||
|
</a>
|
||||||
|
<a href="#portfolio" className="text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 block px-3 py-2 rounded-md text-base font-medium">
|
||||||
|
{t.navigation.portfolio}
|
||||||
|
</a>
|
||||||
|
<a href="#calculator" className="text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 block px-3 py-2 rounded-md text-base font-medium">
|
||||||
|
{t.navigation.calculator}
|
||||||
|
</a>
|
||||||
|
<a href="#contact" className="text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 block px-3 py-2 rounded-md text-base font-medium">
|
||||||
|
{t.navigation.contact}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</nav>
|
||||||
|
<div className="absolute inset-0 bg-black opacity-50 dark:opacity-70"></div>
|
||||||
|
<div className="absolute inset-0 bg-gradient-to-r from-blue-600/20 to-purple-600/20"></div>
|
||||||
|
|
||||||
|
{/* Animated blobs */}
|
||||||
|
<div className="absolute inset-0 overflow-hidden">
|
||||||
|
<div className="absolute -top-40 -right-32 w-80 h-80 bg-purple-500 dark:bg-purple-400 rounded-full mix-blend-multiply filter blur-xl opacity-70 animate-blob"></div>
|
||||||
|
<div className="absolute -bottom-40 -left-32 w-80 h-80 bg-blue-500 dark:bg-blue-400 rounded-full mix-blend-multiply filter blur-xl opacity-70 animate-blob animation-delay-2000"></div>
|
||||||
|
<div className="absolute top-40 left-40 w-80 h-80 bg-indigo-500 dark:bg-indigo-400 rounded-full mix-blend-multiply filter blur-xl opacity-70 animate-blob animation-delay-4000"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="relative max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-24 w-full">
|
||||||
|
<div className="text-center">
|
||||||
|
<h1 className="text-3xl md:text-5xl font-bold text-white mb-6 leading-tight" data-aos="fade-up">
|
||||||
|
{t.hero.title.smart} <span className="text-transparent bg-clip-text bg-gradient-to-r from-blue-400 to-purple-400">{t.hero.title.solutions}</span>
|
||||||
|
</h1>
|
||||||
|
<p className="text-xl md:text-2xl text-gray-300 dark:text-gray-200 mb-8 max-w-3xl mx-auto" data-aos="fade-up" data-aos-delay="200">
|
||||||
|
{t.hero.subtitle}
|
||||||
|
</p>
|
||||||
|
<div className="flex flex-col sm:flex-row gap-4 justify-center items-center" data-aos="fade-up" data-aos-delay="400">
|
||||||
|
<a href="#contact" className="bg-gradient-to-r from-blue-500 to-purple-600 text-white px-8 py-4 rounded-full text-lg font-semibold hover:from-blue-600 hover:to-purple-700 transition-all duration-300 transform hover:scale-105 shadow-lg">
|
||||||
|
{t.hero.cta.start}
|
||||||
|
</a>
|
||||||
|
<a href="#portfolio" className="border-2 border-white text-white px-8 py-4 rounded-full text-lg font-semibold hover:bg-white hover:text-gray-900 transition-all duration-300 transform hover:scale-105">
|
||||||
|
{t.hero.cta.portfolio}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Scroll indicator */}
|
||||||
|
<div className="absolute bottom-20 left-1/2 -translate-x-1/2 animate-bounce">
|
||||||
|
<svg className="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 14l-7 7m0 0l-7-7m7 7V3"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
)}
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
{/* Hero */}
|
||||||
|
<section id="hero" className="relative bg-gradient-to-br from-blue-900 via-purple-900 to-indigo-900 dark:from-gray-900 dark:via-blue-900 dark:to-purple-900 min-h-screen flex items-center overflow-hidden hero-section">
|
||||||
|
<div className="absolute inset-0 bg-black opacity-50 dark:opacity-70"></div>
|
||||||
|
<div className="absolute inset-0 bg-gradient-to-r from-blue-600/20 to-purple-600/20"></div>
|
||||||
|
|
||||||
|
{/* Animated blobs */}
|
||||||
|
<div className="absolute inset-0 overflow-hidden">
|
||||||
|
<div className="absolute -top-40 -right-32 w-80 h-80 bg-purple-500 dark:bg-purple-400 rounded-full mix-blend-multiply filter blur-xl opacity-70 animate-blob"></div>
|
||||||
|
<div className="absolute -bottom-40 -left-32 w-80 h-80 bg-blue-500 dark:bg-blue-400 rounded-full mix-blend-multiply filter blur-xl opacity-70 animate-blob animation-delay-2000"></div>
|
||||||
|
<div className="absolute top-40 left-40 w-80 h-80 bg-indigo-500 dark:bg-indigo-400 rounded-full mix-blend-multiply filter blur-xl opacity-70 animate-blob animation-delay-4000"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="relative max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-24 w-full">
|
||||||
|
<div className="text-center">
|
||||||
|
<h1 className="text-3xl md:text-5xl font-bold text-white mb-6 leading-tight" data-aos="fade-up">
|
||||||
|
{t.hero.title.smart} <span className="text-transparent bg-clip-text bg-gradient-to-r from-blue-400 to-purple-400">{t.hero.title.solutions}</span>
|
||||||
|
</h1>
|
||||||
|
<p className="text-xl md:text-2xl text-gray-300 dark:text-gray-200 mb-8 max-w-3xl mx-auto" data-aos="fade-up" data-aos-delay="200">
|
||||||
|
{t.hero.subtitle}
|
||||||
|
</p>
|
||||||
|
<div className="flex flex-col sm:flex-row gap-4 justify-center items-center" data-aos="fade-up" data-aos-delay="400">
|
||||||
|
<a href="#contact" className="bg-gradient-to-r from-blue-500 to-purple-600 text-white px-8 py-4 rounded-full text-lg font-semibold hover:from-blue-600 hover:to-purple-700 transition-all duration-300 transform hover:scale-105 shadow-lg">
|
||||||
|
{t.hero.cta.start}
|
||||||
|
</a>
|
||||||
|
<a href="#portfolio" className="border-2 border-white text-white px-8 py-4 rounded-full text-lg font-semibold hover:bg-white hover:text-gray-900 transition-all duration-300 transform hover:scale-105">
|
||||||
|
{t.hero.cta.portfolio}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Scroll indicator */}
|
||||||
|
<div className="absolute bottom-20 left-1/2 -translate-x-1/2 animate-bounce">
|
||||||
|
<svg className="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 14l-7 7m0 0l-7-7m7 7V3"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Services Section */}
|
||||||
|
<section id="services" className="py-20 bg-white dark:bg-gray-900">
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
{/* Hero Section */}
|
||||||
|
<div className="text-center mb-16" data-aos="fade-up">
|
||||||
|
<h2 className="text-3xl md:text-4xl font-bold text-gray-900 dark:text-white mb-4">
|
||||||
|
{t.services.hero.title} <span className="text-yellow-300">{t.services.hero.title_highlight}</span>
|
||||||
|
</h2>
|
||||||
|
<p className="text-xl md:text-2xl mb-8 text-gray-700 dark:text-gray-300">
|
||||||
|
{t.services.hero.subtitle}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Services Grid */}
|
||||||
|
<div className="space-y-12">
|
||||||
|
{services.map((service, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className="relative overflow-hidden rounded-3xl shadow-2xl hover:shadow-3xl transition-all duration-500 transform hover:-translate-y-1"
|
||||||
|
data-aos="fade-up"
|
||||||
|
data-aos-delay={index * 150}
|
||||||
|
>
|
||||||
|
{/* Background with Gradient */}
|
||||||
|
<div className={`relative h-64 overflow-hidden ${
|
||||||
|
index % 4 === 0 ? 'bg-gradient-to-r from-blue-600 to-purple-600' :
|
||||||
|
index % 4 === 1 ? 'bg-gradient-to-r from-green-500 to-teal-600' :
|
||||||
|
index % 4 === 2 ? 'bg-gradient-to-r from-purple-600 to-pink-600' :
|
||||||
|
'bg-gradient-to-r from-orange-500 to-red-600'
|
||||||
|
}`}>
|
||||||
|
|
||||||
|
{/* Service Image/Icon Area */}
|
||||||
|
<div className="absolute left-0 top-0 w-2/5 h-full flex items-center justify-center px-6 py-4">
|
||||||
|
<div className="relative z-10 text-white">
|
||||||
|
{service.icon}
|
||||||
|
{/* Decorative elements */}
|
||||||
|
<div className="absolute -top-8 -left-8 w-24 h-24 bg-white bg-opacity-10 rounded-full animate-pulse"></div>
|
||||||
|
<div className="absolute -bottom-4 -right-4 w-16 h-16 bg-white bg-opacity-10 rounded-full animate-pulse" style={{animationDelay: '1s'}}></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Gradient fade to center */}
|
||||||
|
<div className="absolute right-0 top-0 w-1/2 h-full bg-gradient-to-r from-transparent to-current opacity-30"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Service Content Area */}
|
||||||
|
<div className="absolute right-0 top-0 w-3/5 h-full flex items-center pr-16 pl-6 py-4">
|
||||||
|
<div className="text-right w-full text-white">
|
||||||
|
|
||||||
|
{/* Service Category Badge */}
|
||||||
|
<div className="inline-block mb-4">
|
||||||
|
<span className="bg-white bg-opacity-90 text-gray-800 px-4 py-2 rounded-full text-sm font-medium backdrop-blur-sm shadow-sm">
|
||||||
|
{service.category}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Service Title */}
|
||||||
|
<h3 className="text-3xl font-bold mb-4 leading-tight">
|
||||||
|
{service.title}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
{/* Service Description */}
|
||||||
|
<p className="text-white text-opacity-90 mb-6 leading-relaxed text-lg">
|
||||||
|
{service.description}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* Pricing */}
|
||||||
|
<div className="mb-6">
|
||||||
|
<div className="text-white text-opacity-75 text-sm mb-1">{t.services.cards.starting_price}</div>
|
||||||
|
<div className="text-3xl font-bold text-white">
|
||||||
|
{service.price}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Action Buttons */}
|
||||||
|
<div className="flex flex-col gap-3 items-end">
|
||||||
|
<a href="#calculator"
|
||||||
|
className="border-2 border-white text-white px-6 py-3 rounded-xl font-semibold hover:bg-white hover:text-gray-900 transition-all duration-300 transform hover:scale-105">
|
||||||
|
{t.services.cards.calculate_cost}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Process Section */}
|
||||||
|
<div className="mt-32">
|
||||||
|
<div className="text-center mb-16" data-aos="fade-up">
|
||||||
|
<h2 className="text-3xl md:text-4xl font-bold text-gray-900 dark:text-white mb-4">
|
||||||
|
{t.services.process.title}
|
||||||
|
</h2>
|
||||||
|
<p className="text-lg text-gray-700 dark:text-gray-300 text-center mb-12 max-w-3xl mx-auto">
|
||||||
|
{t.services.process.subtitle}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8">
|
||||||
|
{/* Step 1 */}
|
||||||
|
<div className="text-center" data-aos="fade-up" data-aos-delay="100">
|
||||||
|
<div className="w-20 h-20 bg-blue-600 rounded-full flex items-center justify-center mx-auto mb-6">
|
||||||
|
<span className="text-white text-2xl font-bold">1</span>
|
||||||
|
</div>
|
||||||
|
<h3 className="text-xl font-bold text-gray-900 dark:text-white mb-4">{t.services.process.step1.title}</h3>
|
||||||
|
<p className="text-gray-700 dark:text-gray-300 text-center">
|
||||||
|
{t.services.process.step1.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Step 2 */}
|
||||||
|
<div className="text-center" data-aos="fade-up" data-aos-delay="200">
|
||||||
|
<div className="w-20 h-20 bg-purple-600 rounded-full flex items-center justify-center mx-auto mb-6">
|
||||||
|
<span className="text-white text-2xl font-bold">2</span>
|
||||||
|
</div>
|
||||||
|
<h3 className="text-xl font-bold text-gray-900 dark:text-white mb-4">{t.services.process.step2.title}</h3>
|
||||||
|
<p className="text-gray-700 dark:text-gray-300 text-center">
|
||||||
|
{t.services.process.step2.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Step 3 */}
|
||||||
|
<div className="text-center" data-aos="fade-up" data-aos-delay="300">
|
||||||
|
<div className="w-20 h-20 bg-green-600 rounded-full flex items-center justify-center mx-auto mb-6">
|
||||||
|
<span className="text-white text-2xl font-bold">3</span>
|
||||||
|
</div>
|
||||||
|
<h3 className="text-xl font-bold text-gray-900 dark:text-white mb-4">{t.services.process.step3.title}</h3>
|
||||||
|
<p className="text-gray-700 dark:text-gray-300 text-center">
|
||||||
|
{t.services.process.step3.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Step 4 */}
|
||||||
|
<div className="text-center" data-aos="fade-up" data-aos-delay="400">
|
||||||
|
<div className="w-20 h-20 bg-orange-600 rounded-full flex items-center justify-center mx-auto mb-6">
|
||||||
|
<span className="text-white text-2xl font-bold">4</span>
|
||||||
|
</div>
|
||||||
|
<h3 className="text-xl font-bold text-gray-900 dark:text-white mb-4">{t.services.process.step4.title}</h3>
|
||||||
|
<p className="text-gray-700 dark:text-gray-300 text-center">
|
||||||
|
{t.services.process.step4.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Why Choose Us Section */}
|
||||||
|
<div className="mt-32">
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 items-center">
|
||||||
|
{/* Content */}
|
||||||
|
<div data-aos="fade-right">
|
||||||
|
<h2 className="text-3xl md:text-4xl font-bold text-gray-900 dark:text-white mb-6">
|
||||||
|
{t.services.why_choose.title}
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Feature 1 */}
|
||||||
|
<div className="flex items-start">
|
||||||
|
<div className="w-12 h-12 bg-blue-100 dark:bg-blue-900 rounded-full flex items-center justify-center mr-4 flex-shrink-0">
|
||||||
|
<Rocket className="w-6 h-6 text-blue-600 dark:text-blue-400" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-xl font-semibold text-gray-900 dark:text-white mb-2">{t.services.why_choose.modern_tech.title}</h3>
|
||||||
|
<p className="text-gray-600 dark:text-gray-300">
|
||||||
|
{t.services.why_choose.modern_tech.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Feature 2 */}
|
||||||
|
<div className="flex items-start">
|
||||||
|
<div className="w-12 h-12 bg-green-100 dark:bg-green-900 rounded-full flex items-center justify-center mr-4 flex-shrink-0">
|
||||||
|
<Users className="w-6 h-6 text-green-600 dark:text-green-400" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-xl font-semibold text-gray-900 dark:text-white mb-2">{t.services.why_choose.expert_team.title}</h3>
|
||||||
|
<p className="text-gray-600 dark:text-gray-300">
|
||||||
|
{t.services.why_choose.expert_team.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Feature 3 */}
|
||||||
|
<div className="flex items-start">
|
||||||
|
<div className="w-12 h-12 bg-purple-100 dark:bg-purple-900 rounded-full flex items-center justify-center mr-4 flex-shrink-0">
|
||||||
|
<Clock className="w-6 h-6 text-purple-600 dark:text-purple-400" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-xl font-semibold text-gray-900 dark:text-white mb-2">{t.services.why_choose.fast_response.title}</h3>
|
||||||
|
<p className="text-gray-600 dark:text-gray-300">
|
||||||
|
{t.services.why_choose.fast_response.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Feature 4 */}
|
||||||
|
<div className="flex items-start">
|
||||||
|
<div className="w-12 h-12 bg-orange-100 dark:bg-orange-900 rounded-full flex items-center justify-center mr-4 flex-shrink-0">
|
||||||
|
<Headphones className="w-6 h-6 text-orange-600 dark:text-orange-400" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-xl font-semibold text-gray-900 dark:text-white mb-2">{t.services.why_choose.continuous_support.title}</h3>
|
||||||
|
<p className="text-gray-600 dark:text-gray-300">
|
||||||
|
{t.services.why_choose.continuous_support.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Image/Visual */}
|
||||||
|
<div className="relative" data-aos="fade-left">
|
||||||
|
<div className="bg-gradient-to-br from-blue-100 to-purple-100 dark:from-blue-900 dark:to-purple-900 rounded-2xl p-8 h-96 flex items-center justify-center">
|
||||||
|
<div className="text-center">
|
||||||
|
<Award className="w-24 h-24 text-blue-600 dark:text-blue-400 mx-auto mb-4" />
|
||||||
|
<h3 className="text-2xl font-bold text-gray-900 dark:text-white mb-2">{t.services.why_choose.quality_guarantee.title}</h3>
|
||||||
|
<p className="text-gray-600 dark:text-gray-300">
|
||||||
|
{t.services.why_choose.quality_guarantee.subtitle}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Portfolio */}
|
||||||
|
<section className="py-20 bg-gray-50 dark:bg-gray-800" id="portfolio">
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
<div className="text-center mb-16" data-aos="fade-up">
|
||||||
|
<h2 className="text-2xl md:text-3xl font-bold text-gray-900 dark:text-white mb-4">
|
||||||
|
{t.portfolio.title.recent} <span className="text-purple-600 dark:text-purple-400">{t.portfolio.title.projects}</span>
|
||||||
|
</h2>
|
||||||
|
<p className="text-lg text-gray-600 dark:text-gray-300 text-center max-w-3xl mx-auto">
|
||||||
|
{t.portfolio.subtitle}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||||
|
{demoPortfolio.map((project, index) => (
|
||||||
|
<div key={project._id} className="group bg-white dark:bg-gray-700 rounded-2xl overflow-hidden shadow-lg hover:shadow-2xl transition-all duration-300 transform hover:-translate-y-2" data-aos="fade-up" data-aos-delay={index * 100}>
|
||||||
|
<div className="relative overflow-hidden">
|
||||||
|
<img src={project.images?.[0]?.url} alt={project.title} className="w-full h-48 object-cover" />
|
||||||
|
<div className="absolute inset-0 bg-gradient-to-t from-black/50 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
|
||||||
|
<div className="absolute bottom-4 left-4 right-4 translate-y-4 group-hover:translate-y-0 transition-transform duration-300 opacity-0 group-hover:opacity-100">
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{project.technologies?.slice(0, 3).map((tech) => (
|
||||||
|
<span key={tech} className="px-2 py-1 bg-white/20 glass-effect text-white text-xs rounded-full">{tech}</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="p-6">
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<span className="px-3 py-1 bg-blue-100 dark:bg-blue-900 text-blue-600 dark:text-blue-300 text-sm rounded-full font-medium">{project.category}</span>
|
||||||
|
<span className="text-gray-500 dark:text-gray-400 text-sm flex items-center"><Eye className="w-4 h-4 mr-1" />{project.viewCount ?? 0}</span>
|
||||||
|
</div>
|
||||||
|
<h3 className="text-xl font-bold text-gray-900 dark:text-white mb-2 group-hover:text-blue-600 dark:group-hover:text-blue-400 transition-colors">{project.title}</h3>
|
||||||
|
<p className="text-gray-600 dark:text-gray-300 mb-4">{project.shortDescription ?? project.description}</p>
|
||||||
|
<a href="#" className="inline-flex items-center text-blue-600 dark:text-blue-400 font-semibold hover:text-blue-700 dark:hover:text-blue-300 transition-colors">
|
||||||
|
{t.common.view_details}
|
||||||
|
<ArrowRight className="ml-1 w-4 h-4" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-center mt-12" data-aos="fade-up">
|
||||||
|
<a href="#portfolio" className="inline-flex items-center px-6 py-3 bg-purple-600 dark:bg-purple-500 text-white font-semibold rounded-lg hover:bg-purple-700 dark:hover:bg-purple-400 transition-colors">
|
||||||
|
{t.portfolio.view_all}
|
||||||
|
<ArrowRight className="ml-2 w-5 h-5" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Calculator CTA */}
|
||||||
|
<section className="py-20 cta-section">
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 text-center relative z-10">
|
||||||
|
<div data-aos="fade-up">
|
||||||
|
<h2 className="text-2xl md:text-3xl font-bold text-white mb-6">{t.calculator.cta.title}</h2>
|
||||||
|
<p className="text-lg text-blue-100 mb-8 max-w-3xl mx-auto">{t.calculator.cta.subtitle}</p>
|
||||||
|
<a href="#calculator" className="inline-flex items-center px-8 py-4 bg-white text-blue-600 font-bold rounded-full text-lg hover:bg-gray-100 transition-colors transform hover:scale-105">
|
||||||
|
<Calculator className="mr-3" />
|
||||||
|
{t.calculator.cta.button}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Contact */}
|
||||||
|
<section className="py-20 bg-white dark:bg-gray-900" id="contact">
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 items-center">
|
||||||
|
<div data-aos="fade-right">
|
||||||
|
<h2 className="text-2xl md:text-3xl font-bold text-gray-900 dark:text-white mb-6">
|
||||||
|
{t.contact.cta.ready} <span className="text-blue-600 dark:text-blue-400">{t.contact.cta.start}</span>
|
||||||
|
{t.contact.cta.question}
|
||||||
|
</h2>
|
||||||
|
<p className="text-lg text-gray-600 dark:text-gray-300 mb-8">{t.contact.cta.subtitle}</p>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="w-12 h-12 bg-blue-100 dark:bg-blue-900 rounded-full flex items-center justify-center mr-4">
|
||||||
|
<svg viewBox="0 0 24 24" className="w-6 h-6 text-blue-600 dark:text-blue-400" fill="none" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M3 5a2 2 0 012-2h3.28a2 2 0 011.94 1.515l.72 2.885a2 2 0 01-.52 1.94L9.12 11.88a16.001 16.001 0 006.999 6.999l1.54-1.28a2 2 0 011.94-.52l2.885.72A2 2 0 0121 20.72V24a2 2 0 01-2 2h-1C9.163 26 2 18.837 2 10V9a2 2 0 011-1.732V5z"/></svg>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="font-semibold text-gray-900 dark:text-white">{t.contact.phone.title}</div>
|
||||||
|
<div className="text-gray-600 dark:text-gray-300">{t.contact.phone.number}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="w-12 h-12 bg-purple-100 dark:bg-purple-900 rounded-full flex items-center justify-center mr-4">
|
||||||
|
<svg viewBox="0 0 24 24" className="w-6 h-6 text-purple-600 dark:text-purple-400" fill="none" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M16 12H8m8 4H8m8-8H8M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/></svg>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="font-semibold text-gray-900 dark:text-white">{t.contact.email.title}</div>
|
||||||
|
<div className="text-gray-600 dark:text-gray-300">{t.contact.email.address}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="w-12 h-12 bg-green-100 dark:bg-green-900 rounded-full flex items-center justify-center mr-4">
|
||||||
|
<svg viewBox="0 0 24 24" className="w-6 h-6 text-green-600 dark:text-green-400" fill="none" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 11c1.657 0 3-1.343 3-3S13.657 5 12 5 9 6.343 9 8s1.343 3 3 3zm-7 9a7 7 0 1114 0H5z"/></svg>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="font-semibold text-gray-900 dark:text-white">{t.contact.telegram.title}</div>
|
||||||
|
<div className="text-gray-600 dark:text-gray-300">{t.contact.telegram.subtitle}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div data-aos="fade-left">
|
||||||
|
<div className="p-8 rounded-2xl shadow-lg bg-white dark:bg-gray-800">
|
||||||
|
<h3 className="text-2xl font-bold text-gray-900 dark:text-white mb-6">{t.contact.form.title}</h3>
|
||||||
|
<form className="space-y-4" onSubmit={(e) => e.preventDefault()}>
|
||||||
|
<input type="text" placeholder={t.contact.form.name} required className="w-full px-4 py-3 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:bg-gray-700 dark:text-white" />
|
||||||
|
<input type="email" placeholder={t.contact.form.email} required className="w-full px-4 py-3 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:bg-gray-700 dark:text-white" />
|
||||||
|
<input type="tel" placeholder={t.contact.form.phone} className="w-full px-4 py-3 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:bg-gray-700 dark:text-white" />
|
||||||
|
<select className="w-full px-4 py-3 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:bg-gray-700 dark:text-white">
|
||||||
|
<option value="">{t.contact.form.service.select}</option>
|
||||||
|
<option value="web">{t.contact.form.service.web}</option>
|
||||||
|
<option value="mobile">{t.contact.form.service.mobile}</option>
|
||||||
|
<option value="design">{t.contact.form.service.design}</option>
|
||||||
|
<option value="branding">{t.contact.form.service.branding}</option>
|
||||||
|
<option value="consulting">{t.contact.form.service.consulting}</option>
|
||||||
|
<option value="other">{t.contact.form.service.other}</option>
|
||||||
|
</select>
|
||||||
|
<textarea rows={4} placeholder={t.contact.form.message} required className="w-full px-4 py-3 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent resize-none dark:bg-gray-700 dark:text-white"></textarea>
|
||||||
|
<button type="submit" className="w-full bg-gradient-to-r from-blue-600 to-purple-600 text-white font-semibold py-3 rounded-lg hover:from-blue-700 hover:to-purple-700 transition-all duration-300 transform hover:scale-105">
|
||||||
|
{t.contact.form.submit}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
797
.history/views/LandingDemo_20251026210257.tsx
Normal file
@@ -0,0 +1,797 @@
|
|||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import {
|
||||||
|
ArrowRight, Calculator, Code2, Smartphone, Palette, LineChart, Eye, Menu, X,
|
||||||
|
Sun, Moon, ChevronDown, Phone, Mail, MessageCircle, Rocket, Users, Clock,
|
||||||
|
Headphones, Award
|
||||||
|
} from "lucide-react";
|
||||||
|
import AOS from "aos";
|
||||||
|
import "aos/dist/aos.css";
|
||||||
|
|
||||||
|
// Korean i18n strings to replace <%- __('...') %>
|
||||||
|
const t = {
|
||||||
|
navigation: {
|
||||||
|
home: "홈",
|
||||||
|
about: "회사소개",
|
||||||
|
services: "서비스",
|
||||||
|
portfolio: "포트폴리오",
|
||||||
|
contact: "연락처",
|
||||||
|
calculator: "비용계산기"
|
||||||
|
},
|
||||||
|
language: {
|
||||||
|
korean: "한국어",
|
||||||
|
english: "English",
|
||||||
|
russian: "Русский",
|
||||||
|
kazakh: "Қазақша"
|
||||||
|
},
|
||||||
|
hero: {
|
||||||
|
title: { smart: "스마트", solutions: "솔루션" },
|
||||||
|
subtitle: "혁신적인 기술로 비즈니스를 성장시키세요",
|
||||||
|
description: "비즈니스의 디지털 전환을 이끄는 혁신적인 웹개발, 모바일 앱, UI/UX 디자인",
|
||||||
|
cta: { start: "시작하기", portfolio: "포트폴리오 보기" },
|
||||||
|
},
|
||||||
|
services: {
|
||||||
|
title: { our: "우리의", services: "서비스" },
|
||||||
|
subtitle: "아이디어를 현실로 만드는 전문 개발 서비스",
|
||||||
|
description: "최첨단 기술과 창의적 아이디어로 완성하는 디지털 솔루션",
|
||||||
|
view_all: "모든 서비스 보기",
|
||||||
|
web: { title: "웹 개발", description: "반응형 웹사이트 및 웹 애플리케이션 개발", price: "500,000원부터" },
|
||||||
|
mobile: { title: "모바일 앱", description: "iOS 및 Android 네이티브 앱 개발", price: "1,000,000원부터" },
|
||||||
|
design: { title: "UI/UX 디자인", description: "사용자 중심의 인터페이스 및 경험 디자인", price: "300,000원부터" },
|
||||||
|
marketing: { title: "디지털 마케팅", description: "SEO, 소셜미디어 마케팅, 광고 관리", price: "200,000원부터" },
|
||||||
|
hero: {
|
||||||
|
title: "우리의",
|
||||||
|
title_highlight: "서비스",
|
||||||
|
subtitle: "혁신적인 기술로 비즈니스 성장 지원"
|
||||||
|
},
|
||||||
|
cards: {
|
||||||
|
starting_price: "시작 가격",
|
||||||
|
consultation: "상담",
|
||||||
|
calculate_cost: "비용 계산",
|
||||||
|
popular: "인기"
|
||||||
|
},
|
||||||
|
process: {
|
||||||
|
title: "프로젝트 수행 과정",
|
||||||
|
subtitle: "체계적이고 전문적인 프로세스로 프로젝트를 진행합니다",
|
||||||
|
step1: { title: "상담 및 기획", description: "고객의 요구사항을 정확히 파악하고 최적의 솔루션을 기획합니다" },
|
||||||
|
step2: { title: "디자인 및 설계", description: "사용자 중심의 직관적인 디자인과 견고한 시스템 아키텍처를 설계합니다" },
|
||||||
|
step3: { title: "개발 및 구현", description: "최신 기술과 모범 사례를 활용하여 효율적이고 확장 가능한 솔루션을 개발합니다" },
|
||||||
|
step4: { title: "테스트 및 배포", description: "철저한 테스트를 통해 품질을 보장하고 안정적인 배포를 진행합니다" }
|
||||||
|
},
|
||||||
|
why_choose: {
|
||||||
|
title: "왜 SmartSolTech를 선택해야 할까요?",
|
||||||
|
modern_tech: { title: "최신 기술 활용", description: "항상 최신 기술 트렌드를 파악하고, 검증된 기술 스택을 활용하여 미래 지향적인 솔루션을 제공합니다." },
|
||||||
|
expert_team: { title: "전문가 팀", description: "각 분야의 전문가들로 구성된 팀이 협력하여 최고 품질의 결과물을 보장합니다." },
|
||||||
|
fast_response: { title: "빠른 대응", description: "신속한 의사소통과 빠른 피드백을 통해 프로젝트를 효율적으로 진행합니다." },
|
||||||
|
continuous_support: { title: "지속적인 지원", description: "프로젝트 완료 후에도 지속적인 유지보수와 기술 지원을 제공합니다." },
|
||||||
|
quality_guarantee: { title: "품질 보장", subtitle: "최고 품질의 결과물을 약속드립니다" }
|
||||||
|
},
|
||||||
|
cta: {
|
||||||
|
title: "프로젝트를 시작할 준비가 되셨나요?",
|
||||||
|
subtitle: "지금 바로 무료 상담을 받아보세요",
|
||||||
|
calculate_cost: "비용 계산하기",
|
||||||
|
view_portfolio: "포트폴리오 보기"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
portfolio: {
|
||||||
|
title: { recent: "최근", projects: "프로젝트" },
|
||||||
|
subtitle: "웹, 모바일, 브랜딩 분야의 선별된 작업들",
|
||||||
|
view_all: "전체 포트폴리오 보기",
|
||||||
|
},
|
||||||
|
calculator: {
|
||||||
|
cta: { title: "프로젝트 비용 계산기", subtitle: "몇 가지 질문에 답하여 빠른 견적을 받아보세요", button: "계산기 열기" },
|
||||||
|
},
|
||||||
|
contact: {
|
||||||
|
cta: { ready: "시작할", start: "준비가", question: " 되셨나요?", subtitle: "프로젝트에 대해 알려주세요 - 하루 안에 답변드리겠습니다" },
|
||||||
|
phone: { title: "전화", number: "+82 10-1234-5678" },
|
||||||
|
email: { title: "이메일", address: "hello@smartsoltech.kr" },
|
||||||
|
telegram: { title: "텔레그램", subtitle: "@smartsoltech" },
|
||||||
|
form: {
|
||||||
|
title: "빠른 연락",
|
||||||
|
name: "성함",
|
||||||
|
email: "이메일",
|
||||||
|
phone: "전화번호 (선택사항)",
|
||||||
|
service: {
|
||||||
|
select: "어떤 서비스에 관심이 있으신가요?",
|
||||||
|
web: "웹 개발",
|
||||||
|
mobile: "모바일 앱",
|
||||||
|
design: "UI/UX 디자인",
|
||||||
|
branding: "브랜딩",
|
||||||
|
consulting: "컨설팅",
|
||||||
|
other: "기타",
|
||||||
|
},
|
||||||
|
message: "프로젝트에 대해 알려주세요",
|
||||||
|
submit: "메시지 보내기",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
common: { view_details: "자세히 보기" },
|
||||||
|
theme: { toggle: "테마 변경" }
|
||||||
|
};
|
||||||
|
|
||||||
|
const services = [
|
||||||
|
{ icon: <Code2 className="w-10 h-10" />, title: t.services.web.title, description: t.services.web.description, price: t.services.web.price, delay: 0, category: "개발" },
|
||||||
|
{ icon: <Smartphone className="w-10 h-10" />, title: t.services.mobile.title, description: t.services.mobile.description, price: t.services.mobile.price, delay: 100, category: "모바일" },
|
||||||
|
{ icon: <Palette className="w-10 h-10" />, title: t.services.design.title, description: t.services.design.description, price: t.services.design.price, delay: 200, category: "디자인" },
|
||||||
|
{ icon: <LineChart className="w-10 h-10" />, title: t.services.marketing.title, description: t.services.marketing.description, price: t.services.marketing.price, delay: 300, category: "마케팅" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const demoPortfolio = [
|
||||||
|
{
|
||||||
|
_id: "p1",
|
||||||
|
title: "핀테크 대시보드",
|
||||||
|
category: "웹앱",
|
||||||
|
images: [{ url: "https://picsum.photos/seed/fin/800/450", isPrimary: true }],
|
||||||
|
technologies: ["React", "Tailwind", "Node"],
|
||||||
|
description: "역할 기반 접근 제어를 갖춘 실시간 분석",
|
||||||
|
shortDescription: "역할 기반 접근 제어를 갖춘 실시간 분석",
|
||||||
|
viewCount: 423,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_id: "p2",
|
||||||
|
title: "웰니스 트래커",
|
||||||
|
category: "모바일",
|
||||||
|
images: [{ url: "https://picsum.photos/seed/well/800/450", isPrimary: true }],
|
||||||
|
technologies: ["Kotlin", "Compose"],
|
||||||
|
description: "개인 건강 지표 및 알림",
|
||||||
|
shortDescription: "개인 건강 지표 및 알림",
|
||||||
|
viewCount: 1189,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_id: "p3",
|
||||||
|
title: "브랜드 리뉴얼",
|
||||||
|
category: "브랜딩",
|
||||||
|
images: [{ url: "https://picsum.photos/seed/brand/800/450", isPrimary: true }],
|
||||||
|
technologies: ["Figma", "Design System"],
|
||||||
|
description: "아이덴티티, 컴포넌트 및 마케팅 사이트",
|
||||||
|
shortDescription: "아이덴티티, 컴포넌트 및 마케팅 사이트",
|
||||||
|
viewCount: 762,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function LandingDemo() {
|
||||||
|
const [darkMode, setDarkMode] = useState(false);
|
||||||
|
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
||||||
|
const [currentLanguage, setCurrentLanguage] = useState('ko');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
AOS.init({ duration: 800, easing: "ease-in-out", once: true });
|
||||||
|
|
||||||
|
// Check for saved theme preference or default to light mode
|
||||||
|
const savedTheme = localStorage.getItem('theme');
|
||||||
|
if (savedTheme === 'dark' || (!savedTheme && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
|
||||||
|
setDarkMode(true);
|
||||||
|
document.documentElement.classList.add('dark');
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const toggleTheme = () => {
|
||||||
|
setDarkMode(!darkMode);
|
||||||
|
if (!darkMode) {
|
||||||
|
document.documentElement.classList.add('dark');
|
||||||
|
localStorage.setItem('theme', 'dark');
|
||||||
|
} else {
|
||||||
|
document.documentElement.classList.remove('dark');
|
||||||
|
localStorage.setItem('theme', 'light');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleMobileMenu = () => {
|
||||||
|
setMobileMenuOpen(!mobileMenuOpen);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50 dark:bg-gray-900">
|
||||||
|
{/* Global styles for blob animation */}
|
||||||
|
<style jsx global>{`
|
||||||
|
@keyframes blob { 0% { transform: translate(0px,0px) scale(1); } 33% { transform: translate(20px,-30px) scale(1.1);} 66% { transform: translate(-10px,20px) scale(0.9);} 100% { transform: translate(0px,0px) scale(1);} }
|
||||||
|
.animate-blob { animation: blob 12s infinite; }
|
||||||
|
.animation-delay-2000 { animation-delay: 2s; }
|
||||||
|
.animation-delay-4000 { animation-delay: 4s; }
|
||||||
|
.glass-effect { backdrop-filter: blur(6px); }
|
||||||
|
.hero-section::before { content: ""; position: absolute; inset: 0; background: radial-gradient(1200px 600px at 10% -10%, rgba(99,102,241,.35), transparent 60%), radial-gradient(1000px 500px at 110% 10%, rgba(168,85,247,.35), transparent 60%); pointer-events:none; }
|
||||||
|
.cta-section { background: radial-gradient(1200px 600px at 10% 10%, rgba(59,130,246,.25), transparent 60%), linear-gradient(90deg, #3730a3, #7c3aed); }
|
||||||
|
.form-input { outline: none; }
|
||||||
|
.theme-toggle-slider { transform: translateX(0); }
|
||||||
|
.dark .theme-toggle-slider { transform: translateX(1.75rem); }
|
||||||
|
.theme-sun-icon { opacity: 1; }
|
||||||
|
.dark .theme-sun-icon { opacity: 0; transform: rotate(180deg); }
|
||||||
|
.theme-moon-icon { opacity: 0; transform: rotate(-180deg); }
|
||||||
|
.dark .theme-moon-icon { opacity: 1; transform: rotate(0deg); }
|
||||||
|
`}</style>
|
||||||
|
|
||||||
|
{/* Navigation */}
|
||||||
|
<nav className="bg-white dark:bg-gray-900 shadow-lg fixed w-full z-50 top-0 transition-colors duration-300">
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
<div className="flex justify-between h-16">
|
||||||
|
<div className="flex items-center">
|
||||||
|
{/* Logo */}
|
||||||
|
<a href="/" className="flex-shrink-0 flex items-center">
|
||||||
|
<img className="h-8 w-auto" src="/images/logo.png" alt="SmartSolTech" />
|
||||||
|
<span className="ml-2 text-xl font-bold text-gray-900 dark:text-white">SmartSolTech</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Desktop Navigation */}
|
||||||
|
<div className="hidden md:flex items-center space-x-6">
|
||||||
|
{/* Navigation Links */}
|
||||||
|
<a href="#hero" className="text-blue-600 border-b-2 border-blue-600 px-3 py-2 text-sm font-medium transition-colors nav-link">
|
||||||
|
{t.navigation.home}
|
||||||
|
</a>
|
||||||
|
<a href="#services" className="text-gray-700 dark:text-gray-300 hover:text-blue-600 px-3 py-2 text-sm font-medium transition-colors nav-link">
|
||||||
|
{t.navigation.services}
|
||||||
|
</a>
|
||||||
|
<a href="#portfolio" className="text-gray-700 dark:text-gray-300 hover:text-blue-600 px-3 py-2 text-sm font-medium transition-colors nav-link">
|
||||||
|
{t.navigation.portfolio}
|
||||||
|
</a>
|
||||||
|
<a href="#calculator" className="text-gray-700 dark:text-gray-300 hover:text-blue-600 px-3 py-2 text-sm font-medium transition-colors nav-link">
|
||||||
|
{t.navigation.calculator}
|
||||||
|
</a>
|
||||||
|
<a href="#contact" className="text-gray-700 dark:text-gray-300 hover:text-blue-600 px-3 py-2 text-sm font-medium transition-colors nav-link">
|
||||||
|
{t.navigation.contact}
|
||||||
|
</a>
|
||||||
|
|
||||||
|
{/* Language Dropdown */}
|
||||||
|
<div className="relative group">
|
||||||
|
<button className="flex items-center text-gray-700 dark:text-gray-300 hover:text-blue-600 px-3 py-2 text-sm font-medium transition-colors">
|
||||||
|
<span className="mr-2">🇰🇷</span>
|
||||||
|
{t.language.korean}
|
||||||
|
<ChevronDown className="ml-1 w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
<div className="absolute right-0 mt-2 w-40 bg-white dark:bg-gray-800 rounded-lg shadow-lg border dark:border-gray-700 opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-200">
|
||||||
|
<a href="#" className="block px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-t-lg">
|
||||||
|
🇰🇷 {t.language.korean}
|
||||||
|
</a>
|
||||||
|
<a href="#" className="block px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700">
|
||||||
|
🇺🇸 {t.language.english}
|
||||||
|
</a>
|
||||||
|
<a href="#" className="block px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700">
|
||||||
|
🇷🇺 {t.language.russian}
|
||||||
|
</a>
|
||||||
|
<a href="#" className="block px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-b-lg">
|
||||||
|
🇰🇿 {t.language.kazakh}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Animated Theme Toggle */}
|
||||||
|
<div className="relative inline-block ml-4" title={t.theme.toggle}>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="theme-toggle"
|
||||||
|
className="sr-only"
|
||||||
|
checked={darkMode}
|
||||||
|
onChange={toggleTheme}
|
||||||
|
/>
|
||||||
|
<label htmlFor="theme-toggle" className="flex items-center cursor-pointer">
|
||||||
|
<div className="relative w-14 h-7 bg-gradient-to-r from-blue-200 to-yellow-200 dark:from-gray-700 dark:to-gray-600 rounded-full border-2 border-gray-300 dark:border-gray-500 transition-all duration-300 shadow-sm">
|
||||||
|
<div className="absolute top-0.5 left-1 w-5 h-5 bg-white dark:bg-gray-200 rounded-full shadow-md transform transition-all duration-300 flex items-center justify-center theme-toggle-slider">
|
||||||
|
<div className="relative w-full h-full flex items-center justify-center">
|
||||||
|
{/* Sun Icon */}
|
||||||
|
<Sun className="absolute w-3 h-3 text-yellow-500 theme-sun-icon transition-all duration-300 transform" />
|
||||||
|
{/* Moon Icon */}
|
||||||
|
<Moon className="absolute w-3 h-3 text-blue-500 theme-moon-icon transition-all duration-300 transform" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Mobile menu button */}
|
||||||
|
<div className="md:hidden flex items-center space-x-2">
|
||||||
|
<button
|
||||||
|
onClick={toggleMobileMenu}
|
||||||
|
className="p-2 text-gray-700 dark:text-gray-300 hover:text-blue-600 transition-colors"
|
||||||
|
>
|
||||||
|
{mobileMenuOpen ? <X className="w-6 h-6" /> : <Menu className="w-6 h-6" />}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Mobile Navigation Menu */}
|
||||||
|
{mobileMenuOpen && (
|
||||||
|
<div className="md:hidden bg-white dark:bg-gray-900 border-t dark:border-gray-700">
|
||||||
|
<div className="px-2 pt-2 pb-3 space-y-1">
|
||||||
|
<a href="#hero" className="bg-blue-50 dark:bg-blue-900 text-blue-600 block px-3 py-2 rounded-md text-base font-medium">
|
||||||
|
{t.navigation.home}
|
||||||
|
</a>
|
||||||
|
<a href="#services" className="text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 block px-3 py-2 rounded-md text-base font-medium">
|
||||||
|
{t.navigation.services}
|
||||||
|
</a>
|
||||||
|
<a href="#portfolio" className="text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 block px-3 py-2 rounded-md text-base font-medium">
|
||||||
|
{t.navigation.portfolio}
|
||||||
|
</a>
|
||||||
|
<a href="#calculator" className="text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 block px-3 py-2 rounded-md text-base font-medium">
|
||||||
|
{t.navigation.calculator}
|
||||||
|
</a>
|
||||||
|
<a href="#contact" className="text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 block px-3 py-2 rounded-md text-base font-medium">
|
||||||
|
{t.navigation.contact}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</nav>
|
||||||
|
<div className="absolute inset-0 bg-black opacity-50 dark:opacity-70"></div>
|
||||||
|
<div className="absolute inset-0 bg-gradient-to-r from-blue-600/20 to-purple-600/20"></div>
|
||||||
|
|
||||||
|
{/* Animated blobs */}
|
||||||
|
<div className="absolute inset-0 overflow-hidden">
|
||||||
|
<div className="absolute -top-40 -right-32 w-80 h-80 bg-purple-500 dark:bg-purple-400 rounded-full mix-blend-multiply filter blur-xl opacity-70 animate-blob"></div>
|
||||||
|
<div className="absolute -bottom-40 -left-32 w-80 h-80 bg-blue-500 dark:bg-blue-400 rounded-full mix-blend-multiply filter blur-xl opacity-70 animate-blob animation-delay-2000"></div>
|
||||||
|
<div className="absolute top-40 left-40 w-80 h-80 bg-indigo-500 dark:bg-indigo-400 rounded-full mix-blend-multiply filter blur-xl opacity-70 animate-blob animation-delay-4000"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="relative max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-24 w-full">
|
||||||
|
<div className="text-center">
|
||||||
|
<h1 className="text-3xl md:text-5xl font-bold text-white mb-6 leading-tight" data-aos="fade-up">
|
||||||
|
{t.hero.title.smart} <span className="text-transparent bg-clip-text bg-gradient-to-r from-blue-400 to-purple-400">{t.hero.title.solutions}</span>
|
||||||
|
</h1>
|
||||||
|
<p className="text-xl md:text-2xl text-gray-300 dark:text-gray-200 mb-8 max-w-3xl mx-auto" data-aos="fade-up" data-aos-delay="200">
|
||||||
|
{t.hero.subtitle}
|
||||||
|
</p>
|
||||||
|
<div className="flex flex-col sm:flex-row gap-4 justify-center items-center" data-aos="fade-up" data-aos-delay="400">
|
||||||
|
<a href="#contact" className="bg-gradient-to-r from-blue-500 to-purple-600 text-white px-8 py-4 rounded-full text-lg font-semibold hover:from-blue-600 hover:to-purple-700 transition-all duration-300 transform hover:scale-105 shadow-lg">
|
||||||
|
{t.hero.cta.start}
|
||||||
|
</a>
|
||||||
|
<a href="#portfolio" className="border-2 border-white text-white px-8 py-4 rounded-full text-lg font-semibold hover:bg-white hover:text-gray-900 transition-all duration-300 transform hover:scale-105">
|
||||||
|
{t.hero.cta.portfolio}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Scroll indicator */}
|
||||||
|
<div className="absolute bottom-20 left-1/2 -translate-x-1/2 animate-bounce">
|
||||||
|
<svg className="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 14l-7 7m0 0l-7-7m7 7V3"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
)}
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
{/* Hero */}
|
||||||
|
<section id="hero" className="relative bg-gradient-to-br from-blue-900 via-purple-900 to-indigo-900 dark:from-gray-900 dark:via-blue-900 dark:to-purple-900 min-h-screen flex items-center overflow-hidden hero-section">
|
||||||
|
<div className="absolute inset-0 bg-black opacity-50 dark:opacity-70"></div>
|
||||||
|
<div className="absolute inset-0 bg-gradient-to-r from-blue-600/20 to-purple-600/20"></div>
|
||||||
|
|
||||||
|
{/* Animated blobs */}
|
||||||
|
<div className="absolute inset-0 overflow-hidden">
|
||||||
|
<div className="absolute -top-40 -right-32 w-80 h-80 bg-purple-500 dark:bg-purple-400 rounded-full mix-blend-multiply filter blur-xl opacity-70 animate-blob"></div>
|
||||||
|
<div className="absolute -bottom-40 -left-32 w-80 h-80 bg-blue-500 dark:bg-blue-400 rounded-full mix-blend-multiply filter blur-xl opacity-70 animate-blob animation-delay-2000"></div>
|
||||||
|
<div className="absolute top-40 left-40 w-80 h-80 bg-indigo-500 dark:bg-indigo-400 rounded-full mix-blend-multiply filter blur-xl opacity-70 animate-blob animation-delay-4000"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="relative max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-24 w-full">
|
||||||
|
<div className="text-center">
|
||||||
|
<h1 className="text-3xl md:text-5xl font-bold text-white mb-6 leading-tight" data-aos="fade-up">
|
||||||
|
{t.hero.title.smart} <span className="text-transparent bg-clip-text bg-gradient-to-r from-blue-400 to-purple-400">{t.hero.title.solutions}</span>
|
||||||
|
</h1>
|
||||||
|
<p className="text-xl md:text-2xl text-gray-300 dark:text-gray-200 mb-8 max-w-3xl mx-auto" data-aos="fade-up" data-aos-delay="200">
|
||||||
|
{t.hero.subtitle}
|
||||||
|
</p>
|
||||||
|
<div className="flex flex-col sm:flex-row gap-4 justify-center items-center" data-aos="fade-up" data-aos-delay="400">
|
||||||
|
<a href="#contact" className="bg-gradient-to-r from-blue-500 to-purple-600 text-white px-8 py-4 rounded-full text-lg font-semibold hover:from-blue-600 hover:to-purple-700 transition-all duration-300 transform hover:scale-105 shadow-lg">
|
||||||
|
{t.hero.cta.start}
|
||||||
|
</a>
|
||||||
|
<a href="#portfolio" className="border-2 border-white text-white px-8 py-4 rounded-full text-lg font-semibold hover:bg-white hover:text-gray-900 transition-all duration-300 transform hover:scale-105">
|
||||||
|
{t.hero.cta.portfolio}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Scroll indicator */}
|
||||||
|
<div className="absolute bottom-20 left-1/2 -translate-x-1/2 animate-bounce">
|
||||||
|
<svg className="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 14l-7 7m0 0l-7-7m7 7V3"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Services Section */}
|
||||||
|
<section id="services" className="py-20 bg-white dark:bg-gray-900">
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
{/* Hero Section */}
|
||||||
|
<div className="text-center mb-16" data-aos="fade-up">
|
||||||
|
<h2 className="text-3xl md:text-4xl font-bold text-gray-900 dark:text-white mb-4">
|
||||||
|
{t.services.hero.title} <span className="text-yellow-300">{t.services.hero.title_highlight}</span>
|
||||||
|
</h2>
|
||||||
|
<p className="text-xl md:text-2xl mb-8 text-gray-700 dark:text-gray-300">
|
||||||
|
{t.services.hero.subtitle}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Services Grid */}
|
||||||
|
<div className="space-y-12">
|
||||||
|
{services.map((service, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className="relative overflow-hidden rounded-3xl shadow-2xl hover:shadow-3xl transition-all duration-500 transform hover:-translate-y-1"
|
||||||
|
data-aos="fade-up"
|
||||||
|
data-aos-delay={index * 150}
|
||||||
|
>
|
||||||
|
{/* Background with Gradient */}
|
||||||
|
<div className={`relative h-64 overflow-hidden ${
|
||||||
|
index % 4 === 0 ? 'bg-gradient-to-r from-blue-600 to-purple-600' :
|
||||||
|
index % 4 === 1 ? 'bg-gradient-to-r from-green-500 to-teal-600' :
|
||||||
|
index % 4 === 2 ? 'bg-gradient-to-r from-purple-600 to-pink-600' :
|
||||||
|
'bg-gradient-to-r from-orange-500 to-red-600'
|
||||||
|
}`}>
|
||||||
|
|
||||||
|
{/* Service Image/Icon Area */}
|
||||||
|
<div className="absolute left-0 top-0 w-2/5 h-full flex items-center justify-center px-6 py-4">
|
||||||
|
<div className="relative z-10 text-white">
|
||||||
|
{service.icon}
|
||||||
|
{/* Decorative elements */}
|
||||||
|
<div className="absolute -top-8 -left-8 w-24 h-24 bg-white bg-opacity-10 rounded-full animate-pulse"></div>
|
||||||
|
<div className="absolute -bottom-4 -right-4 w-16 h-16 bg-white bg-opacity-10 rounded-full animate-pulse" style={{animationDelay: '1s'}}></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Gradient fade to center */}
|
||||||
|
<div className="absolute right-0 top-0 w-1/2 h-full bg-gradient-to-r from-transparent to-current opacity-30"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Service Content Area */}
|
||||||
|
<div className="absolute right-0 top-0 w-3/5 h-full flex items-center pr-16 pl-6 py-4">
|
||||||
|
<div className="text-right w-full text-white">
|
||||||
|
|
||||||
|
{/* Service Category Badge */}
|
||||||
|
<div className="inline-block mb-4">
|
||||||
|
<span className="bg-white bg-opacity-90 text-gray-800 px-4 py-2 rounded-full text-sm font-medium backdrop-blur-sm shadow-sm">
|
||||||
|
{service.category}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Service Title */}
|
||||||
|
<h3 className="text-3xl font-bold mb-4 leading-tight">
|
||||||
|
{service.title}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
{/* Service Description */}
|
||||||
|
<p className="text-white text-opacity-90 mb-6 leading-relaxed text-lg">
|
||||||
|
{service.description}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* Pricing */}
|
||||||
|
<div className="mb-6">
|
||||||
|
<div className="text-white text-opacity-75 text-sm mb-1">{t.services.cards.starting_price}</div>
|
||||||
|
<div className="text-3xl font-bold text-white">
|
||||||
|
{service.price}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Action Buttons */}
|
||||||
|
<div className="flex flex-col gap-3 items-end">
|
||||||
|
<a href="#calculator"
|
||||||
|
className="border-2 border-white text-white px-6 py-3 rounded-xl font-semibold hover:bg-white hover:text-gray-900 transition-all duration-300 transform hover:scale-105">
|
||||||
|
{t.services.cards.calculate_cost}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Process Section */}
|
||||||
|
<div className="mt-32">
|
||||||
|
<div className="text-center mb-16" data-aos="fade-up">
|
||||||
|
<h2 className="text-3xl md:text-4xl font-bold text-gray-900 dark:text-white mb-4">
|
||||||
|
{t.services.process.title}
|
||||||
|
</h2>
|
||||||
|
<p className="text-lg text-gray-700 dark:text-gray-300 text-center mb-12 max-w-3xl mx-auto">
|
||||||
|
{t.services.process.subtitle}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8">
|
||||||
|
{/* Step 1 */}
|
||||||
|
<div className="text-center" data-aos="fade-up" data-aos-delay="100">
|
||||||
|
<div className="w-20 h-20 bg-blue-600 rounded-full flex items-center justify-center mx-auto mb-6">
|
||||||
|
<span className="text-white text-2xl font-bold">1</span>
|
||||||
|
</div>
|
||||||
|
<h3 className="text-xl font-bold text-gray-900 dark:text-white mb-4">{t.services.process.step1.title}</h3>
|
||||||
|
<p className="text-gray-700 dark:text-gray-300 text-center">
|
||||||
|
{t.services.process.step1.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Step 2 */}
|
||||||
|
<div className="text-center" data-aos="fade-up" data-aos-delay="200">
|
||||||
|
<div className="w-20 h-20 bg-purple-600 rounded-full flex items-center justify-center mx-auto mb-6">
|
||||||
|
<span className="text-white text-2xl font-bold">2</span>
|
||||||
|
</div>
|
||||||
|
<h3 className="text-xl font-bold text-gray-900 dark:text-white mb-4">{t.services.process.step2.title}</h3>
|
||||||
|
<p className="text-gray-700 dark:text-gray-300 text-center">
|
||||||
|
{t.services.process.step2.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Step 3 */}
|
||||||
|
<div className="text-center" data-aos="fade-up" data-aos-delay="300">
|
||||||
|
<div className="w-20 h-20 bg-green-600 rounded-full flex items-center justify-center mx-auto mb-6">
|
||||||
|
<span className="text-white text-2xl font-bold">3</span>
|
||||||
|
</div>
|
||||||
|
<h3 className="text-xl font-bold text-gray-900 dark:text-white mb-4">{t.services.process.step3.title}</h3>
|
||||||
|
<p className="text-gray-700 dark:text-gray-300 text-center">
|
||||||
|
{t.services.process.step3.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Step 4 */}
|
||||||
|
<div className="text-center" data-aos="fade-up" data-aos-delay="400">
|
||||||
|
<div className="w-20 h-20 bg-orange-600 rounded-full flex items-center justify-center mx-auto mb-6">
|
||||||
|
<span className="text-white text-2xl font-bold">4</span>
|
||||||
|
</div>
|
||||||
|
<h3 className="text-xl font-bold text-gray-900 dark:text-white mb-4">{t.services.process.step4.title}</h3>
|
||||||
|
<p className="text-gray-700 dark:text-gray-300 text-center">
|
||||||
|
{t.services.process.step4.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Why Choose Us Section */}
|
||||||
|
<div className="mt-32">
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 items-center">
|
||||||
|
{/* Content */}
|
||||||
|
<div data-aos="fade-right">
|
||||||
|
<h2 className="text-3xl md:text-4xl font-bold text-gray-900 dark:text-white mb-6">
|
||||||
|
{t.services.why_choose.title}
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Feature 1 */}
|
||||||
|
<div className="flex items-start">
|
||||||
|
<div className="w-12 h-12 bg-blue-100 dark:bg-blue-900 rounded-full flex items-center justify-center mr-4 flex-shrink-0">
|
||||||
|
<Rocket className="w-6 h-6 text-blue-600 dark:text-blue-400" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-xl font-semibold text-gray-900 dark:text-white mb-2">{t.services.why_choose.modern_tech.title}</h3>
|
||||||
|
<p className="text-gray-600 dark:text-gray-300">
|
||||||
|
{t.services.why_choose.modern_tech.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Feature 2 */}
|
||||||
|
<div className="flex items-start">
|
||||||
|
<div className="w-12 h-12 bg-green-100 dark:bg-green-900 rounded-full flex items-center justify-center mr-4 flex-shrink-0">
|
||||||
|
<Users className="w-6 h-6 text-green-600 dark:text-green-400" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-xl font-semibold text-gray-900 dark:text-white mb-2">{t.services.why_choose.expert_team.title}</h3>
|
||||||
|
<p className="text-gray-600 dark:text-gray-300">
|
||||||
|
{t.services.why_choose.expert_team.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Feature 3 */}
|
||||||
|
<div className="flex items-start">
|
||||||
|
<div className="w-12 h-12 bg-purple-100 dark:bg-purple-900 rounded-full flex items-center justify-center mr-4 flex-shrink-0">
|
||||||
|
<Clock className="w-6 h-6 text-purple-600 dark:text-purple-400" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-xl font-semibold text-gray-900 dark:text-white mb-2">{t.services.why_choose.fast_response.title}</h3>
|
||||||
|
<p className="text-gray-600 dark:text-gray-300">
|
||||||
|
{t.services.why_choose.fast_response.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Feature 4 */}
|
||||||
|
<div className="flex items-start">
|
||||||
|
<div className="w-12 h-12 bg-orange-100 dark:bg-orange-900 rounded-full flex items-center justify-center mr-4 flex-shrink-0">
|
||||||
|
<Headphones className="w-6 h-6 text-orange-600 dark:text-orange-400" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-xl font-semibold text-gray-900 dark:text-white mb-2">{t.services.why_choose.continuous_support.title}</h3>
|
||||||
|
<p className="text-gray-600 dark:text-gray-300">
|
||||||
|
{t.services.why_choose.continuous_support.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Image/Visual */}
|
||||||
|
<div className="relative" data-aos="fade-left">
|
||||||
|
<div className="bg-gradient-to-br from-blue-100 to-purple-100 dark:from-blue-900 dark:to-purple-900 rounded-2xl p-8 h-96 flex items-center justify-center">
|
||||||
|
<div className="text-center">
|
||||||
|
<Award className="w-24 h-24 text-blue-600 dark:text-blue-400 mx-auto mb-4" />
|
||||||
|
<h3 className="text-2xl font-bold text-gray-900 dark:text-white mb-2">{t.services.why_choose.quality_guarantee.title}</h3>
|
||||||
|
<p className="text-gray-600 dark:text-gray-300">
|
||||||
|
{t.services.why_choose.quality_guarantee.subtitle}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Portfolio */}
|
||||||
|
<section id="portfolio" className="py-20 bg-gray-50 dark:bg-gray-800">
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
<div className="text-center mb-16" data-aos="fade-up">
|
||||||
|
<h2 className="text-2xl md:text-3xl font-bold text-gray-900 dark:text-white mb-4">
|
||||||
|
{t.portfolio.title.recent} <span className="text-purple-600 dark:text-purple-400">{t.portfolio.title.projects}</span>
|
||||||
|
</h2>
|
||||||
|
<p className="text-lg text-gray-600 dark:text-gray-300 text-center max-w-3xl mx-auto">
|
||||||
|
{t.portfolio.subtitle}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||||
|
{demoPortfolio.map((project, index) => (
|
||||||
|
<div key={project._id} className="group bg-white dark:bg-gray-700 rounded-2xl overflow-hidden shadow-lg hover:shadow-2xl transition-all duration-300 transform hover:-translate-y-2" data-aos="fade-up" data-aos-delay={index * 100}>
|
||||||
|
<div className="relative overflow-hidden">
|
||||||
|
<img src={project.images?.[0]?.url} alt={project.title} className="w-full h-48 object-cover" />
|
||||||
|
<div className="absolute inset-0 bg-gradient-to-t from-black/50 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
|
||||||
|
<div className="absolute bottom-4 left-4 right-4 translate-y-4 group-hover:translate-y-0 transition-transform duration-300 opacity-0 group-hover:opacity-100">
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{project.technologies?.slice(0, 3).map((tech) => (
|
||||||
|
<span key={tech} className="px-2 py-1 bg-white/20 glass-effect text-white text-xs rounded-full">{tech}</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="p-6">
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<span className="px-3 py-1 bg-blue-100 dark:bg-blue-900 text-blue-600 dark:text-blue-300 text-sm rounded-full font-medium">{project.category}</span>
|
||||||
|
<span className="text-gray-500 dark:text-gray-400 text-sm flex items-center"><Eye className="w-4 h-4 mr-1" />{project.viewCount ?? 0}</span>
|
||||||
|
</div>
|
||||||
|
<h3 className="text-xl font-bold text-gray-900 dark:text-white mb-2 group-hover:text-blue-600 dark:group-hover:text-blue-400 transition-colors">{project.title}</h3>
|
||||||
|
<p className="text-gray-600 dark:text-gray-300 mb-4">{project.shortDescription ?? project.description}</p>
|
||||||
|
<a href="#" className="inline-flex items-center text-blue-600 dark:text-blue-400 font-semibold hover:text-blue-700 dark:hover:text-blue-300 transition-colors">
|
||||||
|
{t.common.view_details}
|
||||||
|
<ArrowRight className="ml-1 w-4 h-4" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-center mt-12" data-aos="fade-up">
|
||||||
|
<a href="#portfolio" className="inline-flex items-center px-6 py-3 bg-purple-600 dark:bg-purple-500 text-white font-semibold rounded-lg hover:bg-purple-700 dark:hover:bg-purple-400 transition-colors">
|
||||||
|
{t.portfolio.view_all}
|
||||||
|
<ArrowRight className="ml-2 w-5 h-5" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Calculator CTA */}
|
||||||
|
<section id="calculator" className="py-20 cta-section">
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 text-center relative z-10">
|
||||||
|
<div data-aos="fade-up">
|
||||||
|
<h2 className="text-2xl md:text-3xl font-bold text-white mb-6">{t.calculator.cta.title}</h2>
|
||||||
|
<p className="text-lg text-blue-100 mb-8 max-w-3xl mx-auto">{t.calculator.cta.subtitle}</p>
|
||||||
|
<a href="#calculator" className="inline-flex items-center px-8 py-4 bg-white text-blue-600 font-bold rounded-full text-lg hover:bg-gray-100 transition-colors transform hover:scale-105">
|
||||||
|
<Calculator className="mr-3" />
|
||||||
|
{t.calculator.cta.button}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Services CTA */}
|
||||||
|
<section className="py-20 bg-gradient-to-r from-blue-600 to-purple-600 text-white">
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
|
||||||
|
<h2 className="text-3xl md:text-4xl font-bold mb-6" data-aos="fade-up">
|
||||||
|
{t.services.cta.title}
|
||||||
|
</h2>
|
||||||
|
<p className="text-xl mb-8 opacity-90" data-aos="fade-up" data-aos-delay="200">
|
||||||
|
{t.services.cta.subtitle}
|
||||||
|
</p>
|
||||||
|
<div className="flex flex-col sm:flex-row gap-4 justify-center items-center" data-aos="fade-up" data-aos-delay="400">
|
||||||
|
<a href="#calculator" className="border-2 border-white text-white px-8 py-3 rounded-full hover:bg-white hover:text-blue-600 transition-colors font-semibold">
|
||||||
|
{t.services.cta.calculate_cost}
|
||||||
|
</a>
|
||||||
|
<a href="#portfolio" className="border-2 border-white text-white px-8 py-3 rounded-full hover:bg-white hover:text-blue-600 transition-colors font-semibold">
|
||||||
|
{t.services.cta.view_portfolio}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Contact */}
|
||||||
|
<section id="contact" className="py-20 bg-white dark:bg-gray-900">
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 items-center">
|
||||||
|
<div data-aos="fade-right">
|
||||||
|
<h2 className="text-2xl md:text-3xl font-bold text-gray-900 dark:text-white mb-6">
|
||||||
|
{t.contact.cta.ready} <span className="text-blue-600 dark:text-blue-400">{t.contact.cta.start}</span>
|
||||||
|
{t.contact.cta.question}
|
||||||
|
</h2>
|
||||||
|
<p className="text-lg text-gray-600 dark:text-gray-300 mb-8">{t.contact.cta.subtitle}</p>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="w-12 h-12 bg-blue-100 dark:bg-blue-900 rounded-full flex items-center justify-center mr-4">
|
||||||
|
<Phone className="w-6 h-6 text-blue-600 dark:text-blue-400" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="font-semibold text-gray-900 dark:text-white">{t.contact.phone.title}</div>
|
||||||
|
<div className="text-gray-600 dark:text-gray-300">{t.contact.phone.number}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="w-12 h-12 bg-purple-100 dark:bg-purple-900 rounded-full flex items-center justify-center mr-4">
|
||||||
|
<Mail className="w-6 h-6 text-purple-600 dark:text-purple-400" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="font-semibold text-gray-900 dark:text-white">{t.contact.email.title}</div>
|
||||||
|
<div className="text-gray-600 dark:text-gray-300">{t.contact.email.address}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="w-12 h-12 bg-green-100 dark:bg-green-900 rounded-full flex items-center justify-center mr-4">
|
||||||
|
<MessageCircle className="w-6 h-6 text-green-600 dark:text-green-400" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="font-semibold text-gray-900 dark:text-white">{t.contact.telegram.title}</div>
|
||||||
|
<div className="text-gray-600 dark:text-gray-300">{t.contact.telegram.subtitle}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div data-aos="fade-left">
|
||||||
|
<div className="p-8 rounded-2xl shadow-lg bg-white dark:bg-gray-800">
|
||||||
|
<h3 className="text-2xl font-bold text-gray-900 dark:text-white mb-6">{t.contact.form.title}</h3>
|
||||||
|
<form className="space-y-4" onSubmit={(e) => e.preventDefault()}>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder={t.contact.form.name}
|
||||||
|
required
|
||||||
|
className="w-full px-4 py-3 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:bg-gray-700 dark:text-white"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
placeholder={t.contact.form.email}
|
||||||
|
required
|
||||||
|
className="w-full px-4 py-3 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:bg-gray-700 dark:text-white"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="tel"
|
||||||
|
placeholder={t.contact.form.phone}
|
||||||
|
className="w-full px-4 py-3 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:bg-gray-700 dark:text-white"
|
||||||
|
/>
|
||||||
|
<select
|
||||||
|
className="w-full px-4 py-3 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:bg-gray-700 dark:text-white"
|
||||||
|
title={t.contact.form.service.select}
|
||||||
|
>
|
||||||
|
<option value="">{t.contact.form.service.select}</option>
|
||||||
|
<option value="web">{t.contact.form.service.web}</option>
|
||||||
|
<option value="mobile">{t.contact.form.service.mobile}</option>
|
||||||
|
<option value="design">{t.contact.form.service.design}</option>
|
||||||
|
<option value="branding">{t.contact.form.service.branding}</option>
|
||||||
|
<option value="consulting">{t.contact.form.service.consulting}</option>
|
||||||
|
<option value="other">{t.contact.form.service.other}</option>
|
||||||
|
</select>
|
||||||
|
<textarea
|
||||||
|
rows={4}
|
||||||
|
placeholder={t.contact.form.message}
|
||||||
|
required
|
||||||
|
className="w-full px-4 py-3 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent resize-none dark:bg-gray-700 dark:text-white"
|
||||||
|
></textarea>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="w-full bg-gradient-to-r from-blue-600 to-purple-600 text-white font-semibold py-3 rounded-lg hover:from-blue-700 hover:to-purple-700 transition-all duration-300 transform hover:scale-105"
|
||||||
|
>
|
||||||
|
{t.contact.form.submit}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
797
.history/views/LandingDemo_20251026210326.tsx
Normal file
@@ -0,0 +1,797 @@
|
|||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import {
|
||||||
|
ArrowRight, Calculator, Code2, Smartphone, Palette, LineChart, Eye, Menu, X,
|
||||||
|
Sun, Moon, ChevronDown, Phone, Mail, MessageCircle, Rocket, Users, Clock,
|
||||||
|
Headphones, Award
|
||||||
|
} from "lucide-react";
|
||||||
|
import AOS from "aos";
|
||||||
|
import "aos/dist/aos.css";
|
||||||
|
|
||||||
|
// Korean i18n strings to replace <%- __('...') %>
|
||||||
|
const t = {
|
||||||
|
navigation: {
|
||||||
|
home: "홈",
|
||||||
|
about: "회사소개",
|
||||||
|
services: "서비스",
|
||||||
|
portfolio: "포트폴리오",
|
||||||
|
contact: "연락처",
|
||||||
|
calculator: "비용계산기"
|
||||||
|
},
|
||||||
|
language: {
|
||||||
|
korean: "한국어",
|
||||||
|
english: "English",
|
||||||
|
russian: "Русский",
|
||||||
|
kazakh: "Қазақша"
|
||||||
|
},
|
||||||
|
hero: {
|
||||||
|
title: { smart: "스마트", solutions: "솔루션" },
|
||||||
|
subtitle: "혁신적인 기술로 비즈니스를 성장시키세요",
|
||||||
|
description: "비즈니스의 디지털 전환을 이끄는 혁신적인 웹개발, 모바일 앱, UI/UX 디자인",
|
||||||
|
cta: { start: "시작하기", portfolio: "포트폴리오 보기" },
|
||||||
|
},
|
||||||
|
services: {
|
||||||
|
title: { our: "우리의", services: "서비스" },
|
||||||
|
subtitle: "아이디어를 현실로 만드는 전문 개발 서비스",
|
||||||
|
description: "최첨단 기술과 창의적 아이디어로 완성하는 디지털 솔루션",
|
||||||
|
view_all: "모든 서비스 보기",
|
||||||
|
web: { title: "웹 개발", description: "반응형 웹사이트 및 웹 애플리케이션 개발", price: "500,000원부터" },
|
||||||
|
mobile: { title: "모바일 앱", description: "iOS 및 Android 네이티브 앱 개발", price: "1,000,000원부터" },
|
||||||
|
design: { title: "UI/UX 디자인", description: "사용자 중심의 인터페이스 및 경험 디자인", price: "300,000원부터" },
|
||||||
|
marketing: { title: "디지털 마케팅", description: "SEO, 소셜미디어 마케팅, 광고 관리", price: "200,000원부터" },
|
||||||
|
hero: {
|
||||||
|
title: "우리의",
|
||||||
|
title_highlight: "서비스",
|
||||||
|
subtitle: "혁신적인 기술로 비즈니스 성장 지원"
|
||||||
|
},
|
||||||
|
cards: {
|
||||||
|
starting_price: "시작 가격",
|
||||||
|
consultation: "상담",
|
||||||
|
calculate_cost: "비용 계산",
|
||||||
|
popular: "인기"
|
||||||
|
},
|
||||||
|
process: {
|
||||||
|
title: "프로젝트 수행 과정",
|
||||||
|
subtitle: "체계적이고 전문적인 프로세스로 프로젝트를 진행합니다",
|
||||||
|
step1: { title: "상담 및 기획", description: "고객의 요구사항을 정확히 파악하고 최적의 솔루션을 기획합니다" },
|
||||||
|
step2: { title: "디자인 및 설계", description: "사용자 중심의 직관적인 디자인과 견고한 시스템 아키텍처를 설계합니다" },
|
||||||
|
step3: { title: "개발 및 구현", description: "최신 기술과 모범 사례를 활용하여 효율적이고 확장 가능한 솔루션을 개발합니다" },
|
||||||
|
step4: { title: "테스트 및 배포", description: "철저한 테스트를 통해 품질을 보장하고 안정적인 배포를 진행합니다" }
|
||||||
|
},
|
||||||
|
why_choose: {
|
||||||
|
title: "왜 SmartSolTech를 선택해야 할까요?",
|
||||||
|
modern_tech: { title: "최신 기술 활용", description: "항상 최신 기술 트렌드를 파악하고, 검증된 기술 스택을 활용하여 미래 지향적인 솔루션을 제공합니다." },
|
||||||
|
expert_team: { title: "전문가 팀", description: "각 분야의 전문가들로 구성된 팀이 협력하여 최고 품질의 결과물을 보장합니다." },
|
||||||
|
fast_response: { title: "빠른 대응", description: "신속한 의사소통과 빠른 피드백을 통해 프로젝트를 효율적으로 진행합니다." },
|
||||||
|
continuous_support: { title: "지속적인 지원", description: "프로젝트 완료 후에도 지속적인 유지보수와 기술 지원을 제공합니다." },
|
||||||
|
quality_guarantee: { title: "품질 보장", subtitle: "최고 품질의 결과물을 약속드립니다" }
|
||||||
|
},
|
||||||
|
cta: {
|
||||||
|
title: "프로젝트를 시작할 준비가 되셨나요?",
|
||||||
|
subtitle: "지금 바로 무료 상담을 받아보세요",
|
||||||
|
calculate_cost: "비용 계산하기",
|
||||||
|
view_portfolio: "포트폴리오 보기"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
portfolio: {
|
||||||
|
title: { recent: "최근", projects: "프로젝트" },
|
||||||
|
subtitle: "웹, 모바일, 브랜딩 분야의 선별된 작업들",
|
||||||
|
view_all: "전체 포트폴리오 보기",
|
||||||
|
},
|
||||||
|
calculator: {
|
||||||
|
cta: { title: "프로젝트 비용 계산기", subtitle: "몇 가지 질문에 답하여 빠른 견적을 받아보세요", button: "계산기 열기" },
|
||||||
|
},
|
||||||
|
contact: {
|
||||||
|
cta: { ready: "시작할", start: "준비가", question: " 되셨나요?", subtitle: "프로젝트에 대해 알려주세요 - 하루 안에 답변드리겠습니다" },
|
||||||
|
phone: { title: "전화", number: "+82 10-1234-5678" },
|
||||||
|
email: { title: "이메일", address: "hello@smartsoltech.kr" },
|
||||||
|
telegram: { title: "텔레그램", subtitle: "@smartsoltech" },
|
||||||
|
form: {
|
||||||
|
title: "빠른 연락",
|
||||||
|
name: "성함",
|
||||||
|
email: "이메일",
|
||||||
|
phone: "전화번호 (선택사항)",
|
||||||
|
service: {
|
||||||
|
select: "어떤 서비스에 관심이 있으신가요?",
|
||||||
|
web: "웹 개발",
|
||||||
|
mobile: "모바일 앱",
|
||||||
|
design: "UI/UX 디자인",
|
||||||
|
branding: "브랜딩",
|
||||||
|
consulting: "컨설팅",
|
||||||
|
other: "기타",
|
||||||
|
},
|
||||||
|
message: "프로젝트에 대해 알려주세요",
|
||||||
|
submit: "메시지 보내기",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
common: { view_details: "자세히 보기" },
|
||||||
|
theme: { toggle: "테마 변경" }
|
||||||
|
};
|
||||||
|
|
||||||
|
const services = [
|
||||||
|
{ icon: <Code2 className="w-10 h-10" />, title: t.services.web.title, description: t.services.web.description, price: t.services.web.price, delay: 0, category: "개발" },
|
||||||
|
{ icon: <Smartphone className="w-10 h-10" />, title: t.services.mobile.title, description: t.services.mobile.description, price: t.services.mobile.price, delay: 100, category: "모바일" },
|
||||||
|
{ icon: <Palette className="w-10 h-10" />, title: t.services.design.title, description: t.services.design.description, price: t.services.design.price, delay: 200, category: "디자인" },
|
||||||
|
{ icon: <LineChart className="w-10 h-10" />, title: t.services.marketing.title, description: t.services.marketing.description, price: t.services.marketing.price, delay: 300, category: "마케팅" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const demoPortfolio = [
|
||||||
|
{
|
||||||
|
_id: "p1",
|
||||||
|
title: "핀테크 대시보드",
|
||||||
|
category: "웹앱",
|
||||||
|
images: [{ url: "https://picsum.photos/seed/fin/800/450", isPrimary: true }],
|
||||||
|
technologies: ["React", "Tailwind", "Node"],
|
||||||
|
description: "역할 기반 접근 제어를 갖춘 실시간 분석",
|
||||||
|
shortDescription: "역할 기반 접근 제어를 갖춘 실시간 분석",
|
||||||
|
viewCount: 423,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_id: "p2",
|
||||||
|
title: "웰니스 트래커",
|
||||||
|
category: "모바일",
|
||||||
|
images: [{ url: "https://picsum.photos/seed/well/800/450", isPrimary: true }],
|
||||||
|
technologies: ["Kotlin", "Compose"],
|
||||||
|
description: "개인 건강 지표 및 알림",
|
||||||
|
shortDescription: "개인 건강 지표 및 알림",
|
||||||
|
viewCount: 1189,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_id: "p3",
|
||||||
|
title: "브랜드 리뉴얼",
|
||||||
|
category: "브랜딩",
|
||||||
|
images: [{ url: "https://picsum.photos/seed/brand/800/450", isPrimary: true }],
|
||||||
|
technologies: ["Figma", "Design System"],
|
||||||
|
description: "아이덴티티, 컴포넌트 및 마케팅 사이트",
|
||||||
|
shortDescription: "아이덴티티, 컴포넌트 및 마케팅 사이트",
|
||||||
|
viewCount: 762,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function LandingDemo() {
|
||||||
|
const [darkMode, setDarkMode] = useState(false);
|
||||||
|
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
||||||
|
const [currentLanguage, setCurrentLanguage] = useState('ko');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
AOS.init({ duration: 800, easing: "ease-in-out", once: true });
|
||||||
|
|
||||||
|
// Check for saved theme preference or default to light mode
|
||||||
|
const savedTheme = localStorage.getItem('theme');
|
||||||
|
if (savedTheme === 'dark' || (!savedTheme && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
|
||||||
|
setDarkMode(true);
|
||||||
|
document.documentElement.classList.add('dark');
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const toggleTheme = () => {
|
||||||
|
setDarkMode(!darkMode);
|
||||||
|
if (!darkMode) {
|
||||||
|
document.documentElement.classList.add('dark');
|
||||||
|
localStorage.setItem('theme', 'dark');
|
||||||
|
} else {
|
||||||
|
document.documentElement.classList.remove('dark');
|
||||||
|
localStorage.setItem('theme', 'light');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleMobileMenu = () => {
|
||||||
|
setMobileMenuOpen(!mobileMenuOpen);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50 dark:bg-gray-900">
|
||||||
|
{/* Global styles for blob animation */}
|
||||||
|
<style jsx global>{`
|
||||||
|
@keyframes blob { 0% { transform: translate(0px,0px) scale(1); } 33% { transform: translate(20px,-30px) scale(1.1);} 66% { transform: translate(-10px,20px) scale(0.9);} 100% { transform: translate(0px,0px) scale(1);} }
|
||||||
|
.animate-blob { animation: blob 12s infinite; }
|
||||||
|
.animation-delay-2000 { animation-delay: 2s; }
|
||||||
|
.animation-delay-4000 { animation-delay: 4s; }
|
||||||
|
.glass-effect { backdrop-filter: blur(6px); }
|
||||||
|
.hero-section::before { content: ""; position: absolute; inset: 0; background: radial-gradient(1200px 600px at 10% -10%, rgba(99,102,241,.35), transparent 60%), radial-gradient(1000px 500px at 110% 10%, rgba(168,85,247,.35), transparent 60%); pointer-events:none; }
|
||||||
|
.cta-section { background: radial-gradient(1200px 600px at 10% 10%, rgba(59,130,246,.25), transparent 60%), linear-gradient(90deg, #3730a3, #7c3aed); }
|
||||||
|
.form-input { outline: none; }
|
||||||
|
.theme-toggle-slider { transform: translateX(0); }
|
||||||
|
.dark .theme-toggle-slider { transform: translateX(1.75rem); }
|
||||||
|
.theme-sun-icon { opacity: 1; }
|
||||||
|
.dark .theme-sun-icon { opacity: 0; transform: rotate(180deg); }
|
||||||
|
.theme-moon-icon { opacity: 0; transform: rotate(-180deg); }
|
||||||
|
.dark .theme-moon-icon { opacity: 1; transform: rotate(0deg); }
|
||||||
|
`}</style>
|
||||||
|
|
||||||
|
{/* Navigation */}
|
||||||
|
<nav className="bg-white dark:bg-gray-900 shadow-lg fixed w-full z-50 top-0 transition-colors duration-300">
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
<div className="flex justify-between h-16">
|
||||||
|
<div className="flex items-center">
|
||||||
|
{/* Logo */}
|
||||||
|
<a href="/" className="flex-shrink-0 flex items-center">
|
||||||
|
<img className="h-8 w-auto" src="/images/logo.png" alt="SmartSolTech" />
|
||||||
|
<span className="ml-2 text-xl font-bold text-gray-900 dark:text-white">SmartSolTech</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Desktop Navigation */}
|
||||||
|
<div className="hidden md:flex items-center space-x-6">
|
||||||
|
{/* Navigation Links */}
|
||||||
|
<a href="#hero" className="text-blue-600 border-b-2 border-blue-600 px-3 py-2 text-sm font-medium transition-colors nav-link">
|
||||||
|
{t.navigation.home}
|
||||||
|
</a>
|
||||||
|
<a href="#services" className="text-gray-700 dark:text-gray-300 hover:text-blue-600 px-3 py-2 text-sm font-medium transition-colors nav-link">
|
||||||
|
{t.navigation.services}
|
||||||
|
</a>
|
||||||
|
<a href="#portfolio" className="text-gray-700 dark:text-gray-300 hover:text-blue-600 px-3 py-2 text-sm font-medium transition-colors nav-link">
|
||||||
|
{t.navigation.portfolio}
|
||||||
|
</a>
|
||||||
|
<a href="#calculator" className="text-gray-700 dark:text-gray-300 hover:text-blue-600 px-3 py-2 text-sm font-medium transition-colors nav-link">
|
||||||
|
{t.navigation.calculator}
|
||||||
|
</a>
|
||||||
|
<a href="#contact" className="text-gray-700 dark:text-gray-300 hover:text-blue-600 px-3 py-2 text-sm font-medium transition-colors nav-link">
|
||||||
|
{t.navigation.contact}
|
||||||
|
</a>
|
||||||
|
|
||||||
|
{/* Language Dropdown */}
|
||||||
|
<div className="relative group">
|
||||||
|
<button className="flex items-center text-gray-700 dark:text-gray-300 hover:text-blue-600 px-3 py-2 text-sm font-medium transition-colors">
|
||||||
|
<span className="mr-2">🇰🇷</span>
|
||||||
|
{t.language.korean}
|
||||||
|
<ChevronDown className="ml-1 w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
<div className="absolute right-0 mt-2 w-40 bg-white dark:bg-gray-800 rounded-lg shadow-lg border dark:border-gray-700 opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-200">
|
||||||
|
<a href="#" className="block px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-t-lg">
|
||||||
|
🇰🇷 {t.language.korean}
|
||||||
|
</a>
|
||||||
|
<a href="#" className="block px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700">
|
||||||
|
🇺🇸 {t.language.english}
|
||||||
|
</a>
|
||||||
|
<a href="#" className="block px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700">
|
||||||
|
🇷🇺 {t.language.russian}
|
||||||
|
</a>
|
||||||
|
<a href="#" className="block px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-b-lg">
|
||||||
|
🇰🇿 {t.language.kazakh}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Animated Theme Toggle */}
|
||||||
|
<div className="relative inline-block ml-4" title={t.theme.toggle}>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="theme-toggle"
|
||||||
|
className="sr-only"
|
||||||
|
checked={darkMode}
|
||||||
|
onChange={toggleTheme}
|
||||||
|
/>
|
||||||
|
<label htmlFor="theme-toggle" className="flex items-center cursor-pointer">
|
||||||
|
<div className="relative w-14 h-7 bg-gradient-to-r from-blue-200 to-yellow-200 dark:from-gray-700 dark:to-gray-600 rounded-full border-2 border-gray-300 dark:border-gray-500 transition-all duration-300 shadow-sm">
|
||||||
|
<div className="absolute top-0.5 left-1 w-5 h-5 bg-white dark:bg-gray-200 rounded-full shadow-md transform transition-all duration-300 flex items-center justify-center theme-toggle-slider">
|
||||||
|
<div className="relative w-full h-full flex items-center justify-center">
|
||||||
|
{/* Sun Icon */}
|
||||||
|
<Sun className="absolute w-3 h-3 text-yellow-500 theme-sun-icon transition-all duration-300 transform" />
|
||||||
|
{/* Moon Icon */}
|
||||||
|
<Moon className="absolute w-3 h-3 text-blue-500 theme-moon-icon transition-all duration-300 transform" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Mobile menu button */}
|
||||||
|
<div className="md:hidden flex items-center space-x-2">
|
||||||
|
<button
|
||||||
|
onClick={toggleMobileMenu}
|
||||||
|
className="p-2 text-gray-700 dark:text-gray-300 hover:text-blue-600 transition-colors"
|
||||||
|
>
|
||||||
|
{mobileMenuOpen ? <X className="w-6 h-6" /> : <Menu className="w-6 h-6" />}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Mobile Navigation Menu */}
|
||||||
|
{mobileMenuOpen && (
|
||||||
|
<div className="md:hidden bg-white dark:bg-gray-900 border-t dark:border-gray-700">
|
||||||
|
<div className="px-2 pt-2 pb-3 space-y-1">
|
||||||
|
<a href="#hero" className="bg-blue-50 dark:bg-blue-900 text-blue-600 block px-3 py-2 rounded-md text-base font-medium">
|
||||||
|
{t.navigation.home}
|
||||||
|
</a>
|
||||||
|
<a href="#services" className="text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 block px-3 py-2 rounded-md text-base font-medium">
|
||||||
|
{t.navigation.services}
|
||||||
|
</a>
|
||||||
|
<a href="#portfolio" className="text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 block px-3 py-2 rounded-md text-base font-medium">
|
||||||
|
{t.navigation.portfolio}
|
||||||
|
</a>
|
||||||
|
<a href="#calculator" className="text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 block px-3 py-2 rounded-md text-base font-medium">
|
||||||
|
{t.navigation.calculator}
|
||||||
|
</a>
|
||||||
|
<a href="#contact" className="text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 block px-3 py-2 rounded-md text-base font-medium">
|
||||||
|
{t.navigation.contact}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</nav>
|
||||||
|
<div className="absolute inset-0 bg-black opacity-50 dark:opacity-70"></div>
|
||||||
|
<div className="absolute inset-0 bg-gradient-to-r from-blue-600/20 to-purple-600/20"></div>
|
||||||
|
|
||||||
|
{/* Animated blobs */}
|
||||||
|
<div className="absolute inset-0 overflow-hidden">
|
||||||
|
<div className="absolute -top-40 -right-32 w-80 h-80 bg-purple-500 dark:bg-purple-400 rounded-full mix-blend-multiply filter blur-xl opacity-70 animate-blob"></div>
|
||||||
|
<div className="absolute -bottom-40 -left-32 w-80 h-80 bg-blue-500 dark:bg-blue-400 rounded-full mix-blend-multiply filter blur-xl opacity-70 animate-blob animation-delay-2000"></div>
|
||||||
|
<div className="absolute top-40 left-40 w-80 h-80 bg-indigo-500 dark:bg-indigo-400 rounded-full mix-blend-multiply filter blur-xl opacity-70 animate-blob animation-delay-4000"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="relative max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-24 w-full">
|
||||||
|
<div className="text-center">
|
||||||
|
<h1 className="text-3xl md:text-5xl font-bold text-white mb-6 leading-tight" data-aos="fade-up">
|
||||||
|
{t.hero.title.smart} <span className="text-transparent bg-clip-text bg-gradient-to-r from-blue-400 to-purple-400">{t.hero.title.solutions}</span>
|
||||||
|
</h1>
|
||||||
|
<p className="text-xl md:text-2xl text-gray-300 dark:text-gray-200 mb-8 max-w-3xl mx-auto" data-aos="fade-up" data-aos-delay="200">
|
||||||
|
{t.hero.subtitle}
|
||||||
|
</p>
|
||||||
|
<div className="flex flex-col sm:flex-row gap-4 justify-center items-center" data-aos="fade-up" data-aos-delay="400">
|
||||||
|
<a href="#contact" className="bg-gradient-to-r from-blue-500 to-purple-600 text-white px-8 py-4 rounded-full text-lg font-semibold hover:from-blue-600 hover:to-purple-700 transition-all duration-300 transform hover:scale-105 shadow-lg">
|
||||||
|
{t.hero.cta.start}
|
||||||
|
</a>
|
||||||
|
<a href="#portfolio" className="border-2 border-white text-white px-8 py-4 rounded-full text-lg font-semibold hover:bg-white hover:text-gray-900 transition-all duration-300 transform hover:scale-105">
|
||||||
|
{t.hero.cta.portfolio}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Scroll indicator */}
|
||||||
|
<div className="absolute bottom-20 left-1/2 -translate-x-1/2 animate-bounce">
|
||||||
|
<svg className="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 14l-7 7m0 0l-7-7m7 7V3"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
)}
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
{/* Hero */}
|
||||||
|
<section id="hero" className="relative bg-gradient-to-br from-blue-900 via-purple-900 to-indigo-900 dark:from-gray-900 dark:via-blue-900 dark:to-purple-900 min-h-screen flex items-center overflow-hidden hero-section">
|
||||||
|
<div className="absolute inset-0 bg-black opacity-50 dark:opacity-70"></div>
|
||||||
|
<div className="absolute inset-0 bg-gradient-to-r from-blue-600/20 to-purple-600/20"></div>
|
||||||
|
|
||||||
|
{/* Animated blobs */}
|
||||||
|
<div className="absolute inset-0 overflow-hidden">
|
||||||
|
<div className="absolute -top-40 -right-32 w-80 h-80 bg-purple-500 dark:bg-purple-400 rounded-full mix-blend-multiply filter blur-xl opacity-70 animate-blob"></div>
|
||||||
|
<div className="absolute -bottom-40 -left-32 w-80 h-80 bg-blue-500 dark:bg-blue-400 rounded-full mix-blend-multiply filter blur-xl opacity-70 animate-blob animation-delay-2000"></div>
|
||||||
|
<div className="absolute top-40 left-40 w-80 h-80 bg-indigo-500 dark:bg-indigo-400 rounded-full mix-blend-multiply filter blur-xl opacity-70 animate-blob animation-delay-4000"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="relative max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-24 w-full">
|
||||||
|
<div className="text-center">
|
||||||
|
<h1 className="text-3xl md:text-5xl font-bold text-white mb-6 leading-tight" data-aos="fade-up">
|
||||||
|
{t.hero.title.smart} <span className="text-transparent bg-clip-text bg-gradient-to-r from-blue-400 to-purple-400">{t.hero.title.solutions}</span>
|
||||||
|
</h1>
|
||||||
|
<p className="text-xl md:text-2xl text-gray-300 dark:text-gray-200 mb-8 max-w-3xl mx-auto" data-aos="fade-up" data-aos-delay="200">
|
||||||
|
{t.hero.subtitle}
|
||||||
|
</p>
|
||||||
|
<div className="flex flex-col sm:flex-row gap-4 justify-center items-center" data-aos="fade-up" data-aos-delay="400">
|
||||||
|
<a href="#contact" className="bg-gradient-to-r from-blue-500 to-purple-600 text-white px-8 py-4 rounded-full text-lg font-semibold hover:from-blue-600 hover:to-purple-700 transition-all duration-300 transform hover:scale-105 shadow-lg">
|
||||||
|
{t.hero.cta.start}
|
||||||
|
</a>
|
||||||
|
<a href="#portfolio" className="border-2 border-white text-white px-8 py-4 rounded-full text-lg font-semibold hover:bg-white hover:text-gray-900 transition-all duration-300 transform hover:scale-105">
|
||||||
|
{t.hero.cta.portfolio}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Scroll indicator */}
|
||||||
|
<div className="absolute bottom-20 left-1/2 -translate-x-1/2 animate-bounce">
|
||||||
|
<svg className="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 14l-7 7m0 0l-7-7m7 7V3"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Services Section */}
|
||||||
|
<section id="services" className="py-20 bg-white dark:bg-gray-900">
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
{/* Hero Section */}
|
||||||
|
<div className="text-center mb-16" data-aos="fade-up">
|
||||||
|
<h2 className="text-3xl md:text-4xl font-bold text-gray-900 dark:text-white mb-4">
|
||||||
|
{t.services.hero.title} <span className="text-yellow-300">{t.services.hero.title_highlight}</span>
|
||||||
|
</h2>
|
||||||
|
<p className="text-xl md:text-2xl mb-8 text-gray-700 dark:text-gray-300">
|
||||||
|
{t.services.hero.subtitle}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Services Grid */}
|
||||||
|
<div className="space-y-12">
|
||||||
|
{services.map((service, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className="relative overflow-hidden rounded-3xl shadow-2xl hover:shadow-3xl transition-all duration-500 transform hover:-translate-y-1"
|
||||||
|
data-aos="fade-up"
|
||||||
|
data-aos-delay={index * 150}
|
||||||
|
>
|
||||||
|
{/* Background with Gradient */}
|
||||||
|
<div className={`relative h-64 overflow-hidden ${
|
||||||
|
index % 4 === 0 ? 'bg-gradient-to-r from-blue-600 to-purple-600' :
|
||||||
|
index % 4 === 1 ? 'bg-gradient-to-r from-green-500 to-teal-600' :
|
||||||
|
index % 4 === 2 ? 'bg-gradient-to-r from-purple-600 to-pink-600' :
|
||||||
|
'bg-gradient-to-r from-orange-500 to-red-600'
|
||||||
|
}`}>
|
||||||
|
|
||||||
|
{/* Service Image/Icon Area */}
|
||||||
|
<div className="absolute left-0 top-0 w-2/5 h-full flex items-center justify-center px-6 py-4">
|
||||||
|
<div className="relative z-10 text-white">
|
||||||
|
{service.icon}
|
||||||
|
{/* Decorative elements */}
|
||||||
|
<div className="absolute -top-8 -left-8 w-24 h-24 bg-white bg-opacity-10 rounded-full animate-pulse"></div>
|
||||||
|
<div className="absolute -bottom-4 -right-4 w-16 h-16 bg-white bg-opacity-10 rounded-full animate-pulse" style={{animationDelay: '1s'}}></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Gradient fade to center */}
|
||||||
|
<div className="absolute right-0 top-0 w-1/2 h-full bg-gradient-to-r from-transparent to-current opacity-30"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Service Content Area */}
|
||||||
|
<div className="absolute right-0 top-0 w-3/5 h-full flex items-center pr-16 pl-6 py-4">
|
||||||
|
<div className="text-right w-full text-white">
|
||||||
|
|
||||||
|
{/* Service Category Badge */}
|
||||||
|
<div className="inline-block mb-4">
|
||||||
|
<span className="bg-white bg-opacity-90 text-gray-800 px-4 py-2 rounded-full text-sm font-medium backdrop-blur-sm shadow-sm">
|
||||||
|
{service.category}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Service Title */}
|
||||||
|
<h3 className="text-3xl font-bold mb-4 leading-tight">
|
||||||
|
{service.title}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
{/* Service Description */}
|
||||||
|
<p className="text-white text-opacity-90 mb-6 leading-relaxed text-lg">
|
||||||
|
{service.description}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* Pricing */}
|
||||||
|
<div className="mb-6">
|
||||||
|
<div className="text-white text-opacity-75 text-sm mb-1">{t.services.cards.starting_price}</div>
|
||||||
|
<div className="text-3xl font-bold text-white">
|
||||||
|
{service.price}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Action Buttons */}
|
||||||
|
<div className="flex flex-col gap-3 items-end">
|
||||||
|
<a href="#calculator"
|
||||||
|
className="border-2 border-white text-white px-6 py-3 rounded-xl font-semibold hover:bg-white hover:text-gray-900 transition-all duration-300 transform hover:scale-105">
|
||||||
|
{t.services.cards.calculate_cost}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Process Section */}
|
||||||
|
<div className="mt-32">
|
||||||
|
<div className="text-center mb-16" data-aos="fade-up">
|
||||||
|
<h2 className="text-3xl md:text-4xl font-bold text-gray-900 dark:text-white mb-4">
|
||||||
|
{t.services.process.title}
|
||||||
|
</h2>
|
||||||
|
<p className="text-lg text-gray-700 dark:text-gray-300 text-center mb-12 max-w-3xl mx-auto">
|
||||||
|
{t.services.process.subtitle}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8">
|
||||||
|
{/* Step 1 */}
|
||||||
|
<div className="text-center" data-aos="fade-up" data-aos-delay="100">
|
||||||
|
<div className="w-20 h-20 bg-blue-600 rounded-full flex items-center justify-center mx-auto mb-6">
|
||||||
|
<span className="text-white text-2xl font-bold">1</span>
|
||||||
|
</div>
|
||||||
|
<h3 className="text-xl font-bold text-gray-900 dark:text-white mb-4">{t.services.process.step1.title}</h3>
|
||||||
|
<p className="text-gray-700 dark:text-gray-300 text-center">
|
||||||
|
{t.services.process.step1.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Step 2 */}
|
||||||
|
<div className="text-center" data-aos="fade-up" data-aos-delay="200">
|
||||||
|
<div className="w-20 h-20 bg-purple-600 rounded-full flex items-center justify-center mx-auto mb-6">
|
||||||
|
<span className="text-white text-2xl font-bold">2</span>
|
||||||
|
</div>
|
||||||
|
<h3 className="text-xl font-bold text-gray-900 dark:text-white mb-4">{t.services.process.step2.title}</h3>
|
||||||
|
<p className="text-gray-700 dark:text-gray-300 text-center">
|
||||||
|
{t.services.process.step2.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Step 3 */}
|
||||||
|
<div className="text-center" data-aos="fade-up" data-aos-delay="300">
|
||||||
|
<div className="w-20 h-20 bg-green-600 rounded-full flex items-center justify-center mx-auto mb-6">
|
||||||
|
<span className="text-white text-2xl font-bold">3</span>
|
||||||
|
</div>
|
||||||
|
<h3 className="text-xl font-bold text-gray-900 dark:text-white mb-4">{t.services.process.step3.title}</h3>
|
||||||
|
<p className="text-gray-700 dark:text-gray-300 text-center">
|
||||||
|
{t.services.process.step3.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Step 4 */}
|
||||||
|
<div className="text-center" data-aos="fade-up" data-aos-delay="400">
|
||||||
|
<div className="w-20 h-20 bg-orange-600 rounded-full flex items-center justify-center mx-auto mb-6">
|
||||||
|
<span className="text-white text-2xl font-bold">4</span>
|
||||||
|
</div>
|
||||||
|
<h3 className="text-xl font-bold text-gray-900 dark:text-white mb-4">{t.services.process.step4.title}</h3>
|
||||||
|
<p className="text-gray-700 dark:text-gray-300 text-center">
|
||||||
|
{t.services.process.step4.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Why Choose Us Section */}
|
||||||
|
<div className="mt-32">
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 items-center">
|
||||||
|
{/* Content */}
|
||||||
|
<div data-aos="fade-right">
|
||||||
|
<h2 className="text-3xl md:text-4xl font-bold text-gray-900 dark:text-white mb-6">
|
||||||
|
{t.services.why_choose.title}
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Feature 1 */}
|
||||||
|
<div className="flex items-start">
|
||||||
|
<div className="w-12 h-12 bg-blue-100 dark:bg-blue-900 rounded-full flex items-center justify-center mr-4 flex-shrink-0">
|
||||||
|
<Rocket className="w-6 h-6 text-blue-600 dark:text-blue-400" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-xl font-semibold text-gray-900 dark:text-white mb-2">{t.services.why_choose.modern_tech.title}</h3>
|
||||||
|
<p className="text-gray-600 dark:text-gray-300">
|
||||||
|
{t.services.why_choose.modern_tech.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Feature 2 */}
|
||||||
|
<div className="flex items-start">
|
||||||
|
<div className="w-12 h-12 bg-green-100 dark:bg-green-900 rounded-full flex items-center justify-center mr-4 flex-shrink-0">
|
||||||
|
<Users className="w-6 h-6 text-green-600 dark:text-green-400" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-xl font-semibold text-gray-900 dark:text-white mb-2">{t.services.why_choose.expert_team.title}</h3>
|
||||||
|
<p className="text-gray-600 dark:text-gray-300">
|
||||||
|
{t.services.why_choose.expert_team.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Feature 3 */}
|
||||||
|
<div className="flex items-start">
|
||||||
|
<div className="w-12 h-12 bg-purple-100 dark:bg-purple-900 rounded-full flex items-center justify-center mr-4 flex-shrink-0">
|
||||||
|
<Clock className="w-6 h-6 text-purple-600 dark:text-purple-400" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-xl font-semibold text-gray-900 dark:text-white mb-2">{t.services.why_choose.fast_response.title}</h3>
|
||||||
|
<p className="text-gray-600 dark:text-gray-300">
|
||||||
|
{t.services.why_choose.fast_response.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Feature 4 */}
|
||||||
|
<div className="flex items-start">
|
||||||
|
<div className="w-12 h-12 bg-orange-100 dark:bg-orange-900 rounded-full flex items-center justify-center mr-4 flex-shrink-0">
|
||||||
|
<Headphones className="w-6 h-6 text-orange-600 dark:text-orange-400" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-xl font-semibold text-gray-900 dark:text-white mb-2">{t.services.why_choose.continuous_support.title}</h3>
|
||||||
|
<p className="text-gray-600 dark:text-gray-300">
|
||||||
|
{t.services.why_choose.continuous_support.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Image/Visual */}
|
||||||
|
<div className="relative" data-aos="fade-left">
|
||||||
|
<div className="bg-gradient-to-br from-blue-100 to-purple-100 dark:from-blue-900 dark:to-purple-900 rounded-2xl p-8 h-96 flex items-center justify-center">
|
||||||
|
<div className="text-center">
|
||||||
|
<Award className="w-24 h-24 text-blue-600 dark:text-blue-400 mx-auto mb-4" />
|
||||||
|
<h3 className="text-2xl font-bold text-gray-900 dark:text-white mb-2">{t.services.why_choose.quality_guarantee.title}</h3>
|
||||||
|
<p className="text-gray-600 dark:text-gray-300">
|
||||||
|
{t.services.why_choose.quality_guarantee.subtitle}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Portfolio */}
|
||||||
|
<section id="portfolio" className="py-20 bg-gray-50 dark:bg-gray-800">
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
<div className="text-center mb-16" data-aos="fade-up">
|
||||||
|
<h2 className="text-2xl md:text-3xl font-bold text-gray-900 dark:text-white mb-4">
|
||||||
|
{t.portfolio.title.recent} <span className="text-purple-600 dark:text-purple-400">{t.portfolio.title.projects}</span>
|
||||||
|
</h2>
|
||||||
|
<p className="text-lg text-gray-600 dark:text-gray-300 text-center max-w-3xl mx-auto">
|
||||||
|
{t.portfolio.subtitle}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||||
|
{demoPortfolio.map((project, index) => (
|
||||||
|
<div key={project._id} className="group bg-white dark:bg-gray-700 rounded-2xl overflow-hidden shadow-lg hover:shadow-2xl transition-all duration-300 transform hover:-translate-y-2" data-aos="fade-up" data-aos-delay={index * 100}>
|
||||||
|
<div className="relative overflow-hidden">
|
||||||
|
<img src={project.images?.[0]?.url} alt={project.title} className="w-full h-48 object-cover" />
|
||||||
|
<div className="absolute inset-0 bg-gradient-to-t from-black/50 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
|
||||||
|
<div className="absolute bottom-4 left-4 right-4 translate-y-4 group-hover:translate-y-0 transition-transform duration-300 opacity-0 group-hover:opacity-100">
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{project.technologies?.slice(0, 3).map((tech) => (
|
||||||
|
<span key={tech} className="px-2 py-1 bg-white/20 glass-effect text-white text-xs rounded-full">{tech}</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="p-6">
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<span className="px-3 py-1 bg-blue-100 dark:bg-blue-900 text-blue-600 dark:text-blue-300 text-sm rounded-full font-medium">{project.category}</span>
|
||||||
|
<span className="text-gray-500 dark:text-gray-400 text-sm flex items-center"><Eye className="w-4 h-4 mr-1" />{project.viewCount ?? 0}</span>
|
||||||
|
</div>
|
||||||
|
<h3 className="text-xl font-bold text-gray-900 dark:text-white mb-2 group-hover:text-blue-600 dark:group-hover:text-blue-400 transition-colors">{project.title}</h3>
|
||||||
|
<p className="text-gray-600 dark:text-gray-300 mb-4">{project.shortDescription ?? project.description}</p>
|
||||||
|
<a href="#" className="inline-flex items-center text-blue-600 dark:text-blue-400 font-semibold hover:text-blue-700 dark:hover:text-blue-300 transition-colors">
|
||||||
|
{t.common.view_details}
|
||||||
|
<ArrowRight className="ml-1 w-4 h-4" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-center mt-12" data-aos="fade-up">
|
||||||
|
<a href="#portfolio" className="inline-flex items-center px-6 py-3 bg-purple-600 dark:bg-purple-500 text-white font-semibold rounded-lg hover:bg-purple-700 dark:hover:bg-purple-400 transition-colors">
|
||||||
|
{t.portfolio.view_all}
|
||||||
|
<ArrowRight className="ml-2 w-5 h-5" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Calculator CTA */}
|
||||||
|
<section id="calculator" className="py-20 cta-section">
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 text-center relative z-10">
|
||||||
|
<div data-aos="fade-up">
|
||||||
|
<h2 className="text-2xl md:text-3xl font-bold text-white mb-6">{t.calculator.cta.title}</h2>
|
||||||
|
<p className="text-lg text-blue-100 mb-8 max-w-3xl mx-auto">{t.calculator.cta.subtitle}</p>
|
||||||
|
<a href="#calculator" className="inline-flex items-center px-8 py-4 bg-white text-blue-600 font-bold rounded-full text-lg hover:bg-gray-100 transition-colors transform hover:scale-105">
|
||||||
|
<Calculator className="mr-3" />
|
||||||
|
{t.calculator.cta.button}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Services CTA */}
|
||||||
|
<section className="py-20 bg-gradient-to-r from-blue-600 to-purple-600 text-white">
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
|
||||||
|
<h2 className="text-3xl md:text-4xl font-bold mb-6" data-aos="fade-up">
|
||||||
|
{t.services.cta.title}
|
||||||
|
</h2>
|
||||||
|
<p className="text-xl mb-8 opacity-90" data-aos="fade-up" data-aos-delay="200">
|
||||||
|
{t.services.cta.subtitle}
|
||||||
|
</p>
|
||||||
|
<div className="flex flex-col sm:flex-row gap-4 justify-center items-center" data-aos="fade-up" data-aos-delay="400">
|
||||||
|
<a href="#calculator" className="border-2 border-white text-white px-8 py-3 rounded-full hover:bg-white hover:text-blue-600 transition-colors font-semibold">
|
||||||
|
{t.services.cta.calculate_cost}
|
||||||
|
</a>
|
||||||
|
<a href="#portfolio" className="border-2 border-white text-white px-8 py-3 rounded-full hover:bg-white hover:text-blue-600 transition-colors font-semibold">
|
||||||
|
{t.services.cta.view_portfolio}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Contact */}
|
||||||
|
<section id="contact" className="py-20 bg-white dark:bg-gray-900">
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 items-center">
|
||||||
|
<div data-aos="fade-right">
|
||||||
|
<h2 className="text-2xl md:text-3xl font-bold text-gray-900 dark:text-white mb-6">
|
||||||
|
{t.contact.cta.ready} <span className="text-blue-600 dark:text-blue-400">{t.contact.cta.start}</span>
|
||||||
|
{t.contact.cta.question}
|
||||||
|
</h2>
|
||||||
|
<p className="text-lg text-gray-600 dark:text-gray-300 mb-8">{t.contact.cta.subtitle}</p>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="w-12 h-12 bg-blue-100 dark:bg-blue-900 rounded-full flex items-center justify-center mr-4">
|
||||||
|
<Phone className="w-6 h-6 text-blue-600 dark:text-blue-400" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="font-semibold text-gray-900 dark:text-white">{t.contact.phone.title}</div>
|
||||||
|
<div className="text-gray-600 dark:text-gray-300">{t.contact.phone.number}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="w-12 h-12 bg-purple-100 dark:bg-purple-900 rounded-full flex items-center justify-center mr-4">
|
||||||
|
<Mail className="w-6 h-6 text-purple-600 dark:text-purple-400" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="font-semibold text-gray-900 dark:text-white">{t.contact.email.title}</div>
|
||||||
|
<div className="text-gray-600 dark:text-gray-300">{t.contact.email.address}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="w-12 h-12 bg-green-100 dark:bg-green-900 rounded-full flex items-center justify-center mr-4">
|
||||||
|
<MessageCircle className="w-6 h-6 text-green-600 dark:text-green-400" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="font-semibold text-gray-900 dark:text-white">{t.contact.telegram.title}</div>
|
||||||
|
<div className="text-gray-600 dark:text-gray-300">{t.contact.telegram.subtitle}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div data-aos="fade-left">
|
||||||
|
<div className="p-8 rounded-2xl shadow-lg bg-white dark:bg-gray-800">
|
||||||
|
<h3 className="text-2xl font-bold text-gray-900 dark:text-white mb-6">{t.contact.form.title}</h3>
|
||||||
|
<form className="space-y-4" onSubmit={(e) => e.preventDefault()}>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder={t.contact.form.name}
|
||||||
|
required
|
||||||
|
className="w-full px-4 py-3 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:bg-gray-700 dark:text-white"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
placeholder={t.contact.form.email}
|
||||||
|
required
|
||||||
|
className="w-full px-4 py-3 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:bg-gray-700 dark:text-white"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="tel"
|
||||||
|
placeholder={t.contact.form.phone}
|
||||||
|
className="w-full px-4 py-3 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:bg-gray-700 dark:text-white"
|
||||||
|
/>
|
||||||
|
<select
|
||||||
|
className="w-full px-4 py-3 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:bg-gray-700 dark:text-white"
|
||||||
|
title={t.contact.form.service.select}
|
||||||
|
>
|
||||||
|
<option value="">{t.contact.form.service.select}</option>
|
||||||
|
<option value="web">{t.contact.form.service.web}</option>
|
||||||
|
<option value="mobile">{t.contact.form.service.mobile}</option>
|
||||||
|
<option value="design">{t.contact.form.service.design}</option>
|
||||||
|
<option value="branding">{t.contact.form.service.branding}</option>
|
||||||
|
<option value="consulting">{t.contact.form.service.consulting}</option>
|
||||||
|
<option value="other">{t.contact.form.service.other}</option>
|
||||||
|
</select>
|
||||||
|
<textarea
|
||||||
|
rows={4}
|
||||||
|
placeholder={t.contact.form.message}
|
||||||
|
required
|
||||||
|
className="w-full px-4 py-3 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent resize-none dark:bg-gray-700 dark:text-white"
|
||||||
|
></textarea>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="w-full bg-gradient-to-r from-blue-600 to-purple-600 text-white font-semibold py-3 rounded-lg hover:from-blue-700 hover:to-purple-700 transition-all duration-300 transform hover:scale-105"
|
||||||
|
>
|
||||||
|
{t.contact.form.submit}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
256
.history/views/admin-bundle-comparison_20251026211814.ejs
Normal file
@@ -0,0 +1,256 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ko">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Admin Bundle Comparison - SmartSolTech</title>
|
||||||
|
|
||||||
|
<!-- Tailwind CSS -->
|
||||||
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
|
||||||
|
<!-- Font Awesome -->
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
|
||||||
|
|
||||||
|
<!-- Google Fonts -->
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@300;400;500;700&display=swap" rel="stylesheet">
|
||||||
|
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: 'Noto Sans KR', 'Malgun Gothic', 'Apple SD Gothic Neo', sans-serif;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="bg-gray-50">
|
||||||
|
<div class="min-h-screen py-12">
|
||||||
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="text-center mb-12">
|
||||||
|
<h1 class="text-4xl font-bold text-gray-900 mb-4">
|
||||||
|
SmartSolTech Admin Bundle 비교
|
||||||
|
</h1>
|
||||||
|
<p class="text-lg text-gray-600 max-w-3xl mx-auto">
|
||||||
|
우리 웹사이트에 가장 적합한 관리자 패널을 선택해보세요. 각 솔루션의 기능과 디자인을 비교해보실 수 있습니다.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Admin Bundle Comparison -->
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8 mb-12">
|
||||||
|
<!-- AdminLTE Card -->
|
||||||
|
<div class="bg-white rounded-xl shadow-lg overflow-hidden">
|
||||||
|
<div class="p-6 bg-gradient-to-r from-blue-500 to-purple-600 text-white">
|
||||||
|
<h2 class="text-2xl font-bold mb-2">AdminLTE 3</h2>
|
||||||
|
<p class="text-blue-100">무료 오픈소스 관리자 템플릿</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="p-6">
|
||||||
|
<div class="space-y-4 mb-6">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<i class="fas fa-check-circle text-green-500 mr-3"></i>
|
||||||
|
<span>완전 무료 오픈소스</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<i class="fas fa-check-circle text-green-500 mr-3"></i>
|
||||||
|
<span>한국어 지원 우수</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<i class="fas fa-check-circle text-green-500 mr-3"></i>
|
||||||
|
<span>풍부한 컴포넌트</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<i class="fas fa-check-circle text-green-500 mr-3"></i>
|
||||||
|
<span>jQuery 기반 (안정성)</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<i class="fas fa-check-circle text-green-500 mr-3"></i>
|
||||||
|
<span>대규모 커뮤니티</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<i class="fas fa-star text-yellow-500 mr-3"></i>
|
||||||
|
<span>Bootstrap 4/5 호환</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-6">
|
||||||
|
<h4 class="font-semibold mb-3">주요 기능:</h4>
|
||||||
|
<ul class="text-sm text-gray-600 space-y-1">
|
||||||
|
<li>• 반응형 대시보드</li>
|
||||||
|
<li>• 다양한 차트와 위젯</li>
|
||||||
|
<li>• 카드형 레이아웃</li>
|
||||||
|
<li>• 사이드바 접기/펼치기</li>
|
||||||
|
<li>• 어두운/밝은 테마</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex space-x-3">
|
||||||
|
<a href="/demo/demo-adminlte" target="_blank"
|
||||||
|
class="flex-1 bg-blue-600 text-white text-center py-3 rounded-lg hover:bg-blue-700 transition-colors">
|
||||||
|
<i class="fas fa-eye mr-2"></i>
|
||||||
|
데모 보기
|
||||||
|
</a>
|
||||||
|
<a href="https://adminlte.io/" target="_blank"
|
||||||
|
class="flex-1 bg-gray-200 text-gray-700 text-center py-3 rounded-lg hover:bg-gray-300 transition-colors">
|
||||||
|
<i class="fas fa-external-link-alt mr-2"></i>
|
||||||
|
공식 사이트
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tabler Card -->
|
||||||
|
<div class="bg-white rounded-xl shadow-lg overflow-hidden">
|
||||||
|
<div class="p-6 bg-gradient-to-r from-purple-500 to-pink-600 text-white">
|
||||||
|
<h2 class="text-2xl font-bold mb-2">Tabler</h2>
|
||||||
|
<p class="text-purple-100">모던 관리자 대시보드 템플릿</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="p-6">
|
||||||
|
<div class="space-y-4 mb-6">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<i class="fas fa-check-circle text-green-500 mr-3"></i>
|
||||||
|
<span>모던한 디자인</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<i class="fas fa-check-circle text-green-500 mr-3"></i>
|
||||||
|
<span>빠른 성능</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<i class="fas fa-check-circle text-green-500 mr-3"></i>
|
||||||
|
<span>SVG 아이콘 시스템</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<i class="fas fa-star text-yellow-500 mr-3"></i>
|
||||||
|
<span>Bootstrap 5 기반</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<i class="fas fa-star text-yellow-500 mr-3"></i>
|
||||||
|
<span>TypeScript 지원</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<i class="fas fa-star text-yellow-500 mr-3"></i>
|
||||||
|
<span>최신 웹 표준</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-6">
|
||||||
|
<h4 class="font-semibold mb-3">주요 기능:</h4>
|
||||||
|
<ul class="text-sm text-gray-600 space-y-1">
|
||||||
|
<li>• 깔끔한 미니멀 디자인</li>
|
||||||
|
<li>• 3000+ 무료 SVG 아이콘</li>
|
||||||
|
<li>• 드롭다운 메뉴</li>
|
||||||
|
<li>• 모바일 최적화</li>
|
||||||
|
<li>• 빠른 로딩 속도</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex space-x-3">
|
||||||
|
<a href="/demo/demo-tabler" target="_blank"
|
||||||
|
class="flex-1 bg-purple-600 text-white text-center py-3 rounded-lg hover:bg-purple-700 transition-colors">
|
||||||
|
<i class="fas fa-eye mr-2"></i>
|
||||||
|
데모 보기
|
||||||
|
</a>
|
||||||
|
<a href="https://tabler.io/" target="_blank"
|
||||||
|
class="flex-1 bg-gray-200 text-gray-700 text-center py-3 rounded-lg hover:bg-gray-300 transition-colors">
|
||||||
|
<i class="fas fa-external-link-alt mr-2"></i>
|
||||||
|
공식 사이트
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Comparison Table -->
|
||||||
|
<div class="bg-white rounded-xl shadow-lg overflow-hidden mb-12">
|
||||||
|
<div class="p-6 bg-gradient-to-r from-gray-800 to-gray-900 text-white">
|
||||||
|
<h3 class="text-xl font-bold">상세 비교표</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table class="w-full">
|
||||||
|
<thead class="bg-gray-50">
|
||||||
|
<tr>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">기능</th>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">AdminLTE</th>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Tabler</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="bg-white divide-y divide-gray-200">
|
||||||
|
<tr>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">가격</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-green-600">완전 무료</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-green-600">무료 + Pro 버전</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="bg-gray-50">
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">한국어 지원</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-green-600">우수</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-yellow-600">보통</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">디자인</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-blue-600">클래식</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-purple-600">모던</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="bg-gray-50">
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">컴포넌트 수</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-green-600">매우 많음</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-blue-600">적당함</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">성능</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-blue-600">양호</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-green-600">우수</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="bg-gray-50">
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">커뮤니티</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-green-600">대규모</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-blue-600">성장중</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">학습 곡선</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-green-600">쉬움</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-yellow-600">보통</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Recommendation -->
|
||||||
|
<div class="bg-gradient-to-r from-green-400 to-blue-500 rounded-xl p-8 text-white">
|
||||||
|
<div class="text-center">
|
||||||
|
<h3 class="text-2xl font-bold mb-4">
|
||||||
|
<i class="fas fa-thumbs-up mr-2"></i>
|
||||||
|
추천 결과
|
||||||
|
</h3>
|
||||||
|
<p class="text-lg mb-6">
|
||||||
|
SmartSolTech 웹사이트의 특성을 고려한 맞춤 추천
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="bg-white bg-opacity-20 rounded-lg p-6 max-w-2xl mx-auto">
|
||||||
|
<h4 class="text-xl font-semibold mb-3">
|
||||||
|
🏆 AdminLTE 3 추천
|
||||||
|
</h4>
|
||||||
|
<p class="text-left">
|
||||||
|
<strong>추천 이유:</strong><br>
|
||||||
|
• 한국 고객을 위한 완벽한 한국어 지원<br>
|
||||||
|
• 무료로 모든 기능 사용 가능<br>
|
||||||
|
• 기존 시스템과의 호환성 우수<br>
|
||||||
|
• 풍부한 문서화와 커뮤니티 지원<br>
|
||||||
|
• 기업용 웹사이트에 적합한 안정적인 디자인
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Back to Main Site -->
|
||||||
|
<div class="text-center mt-12">
|
||||||
|
<a href="/" class="inline-flex items-center px-6 py-3 bg-gray-800 text-white rounded-lg hover:bg-gray-900 transition-colors">
|
||||||
|
<i class="fas fa-arrow-left mr-2"></i>
|
||||||
|
메인 사이트로 돌아가기
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
256
.history/views/admin-bundle-comparison_20251026211849.ejs
Normal file
@@ -0,0 +1,256 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ko">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Admin Bundle Comparison - SmartSolTech</title>
|
||||||
|
|
||||||
|
<!-- Tailwind CSS -->
|
||||||
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
|
||||||
|
<!-- Font Awesome -->
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
|
||||||
|
|
||||||
|
<!-- Google Fonts -->
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@300;400;500;700&display=swap" rel="stylesheet">
|
||||||
|
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: 'Noto Sans KR', 'Malgun Gothic', 'Apple SD Gothic Neo', sans-serif;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="bg-gray-50">
|
||||||
|
<div class="min-h-screen py-12">
|
||||||
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="text-center mb-12">
|
||||||
|
<h1 class="text-4xl font-bold text-gray-900 mb-4">
|
||||||
|
SmartSolTech Admin Bundle 비교
|
||||||
|
</h1>
|
||||||
|
<p class="text-lg text-gray-600 max-w-3xl mx-auto">
|
||||||
|
우리 웹사이트에 가장 적합한 관리자 패널을 선택해보세요. 각 솔루션의 기능과 디자인을 비교해보실 수 있습니다.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Admin Bundle Comparison -->
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8 mb-12">
|
||||||
|
<!-- AdminLTE Card -->
|
||||||
|
<div class="bg-white rounded-xl shadow-lg overflow-hidden">
|
||||||
|
<div class="p-6 bg-gradient-to-r from-blue-500 to-purple-600 text-white">
|
||||||
|
<h2 class="text-2xl font-bold mb-2">AdminLTE 3</h2>
|
||||||
|
<p class="text-blue-100">무료 오픈소스 관리자 템플릿</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="p-6">
|
||||||
|
<div class="space-y-4 mb-6">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<i class="fas fa-check-circle text-green-500 mr-3"></i>
|
||||||
|
<span>완전 무료 오픈소스</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<i class="fas fa-check-circle text-green-500 mr-3"></i>
|
||||||
|
<span>한국어 지원 우수</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<i class="fas fa-check-circle text-green-500 mr-3"></i>
|
||||||
|
<span>풍부한 컴포넌트</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<i class="fas fa-check-circle text-green-500 mr-3"></i>
|
||||||
|
<span>jQuery 기반 (안정성)</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<i class="fas fa-check-circle text-green-500 mr-3"></i>
|
||||||
|
<span>대규모 커뮤니티</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<i class="fas fa-star text-yellow-500 mr-3"></i>
|
||||||
|
<span>Bootstrap 4/5 호환</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-6">
|
||||||
|
<h4 class="font-semibold mb-3">주요 기능:</h4>
|
||||||
|
<ul class="text-sm text-gray-600 space-y-1">
|
||||||
|
<li>• 반응형 대시보드</li>
|
||||||
|
<li>• 다양한 차트와 위젯</li>
|
||||||
|
<li>• 카드형 레이아웃</li>
|
||||||
|
<li>• 사이드바 접기/펼치기</li>
|
||||||
|
<li>• 어두운/밝은 테마</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex space-x-3">
|
||||||
|
<a href="/demo/demo-adminlte" target="_blank"
|
||||||
|
class="flex-1 bg-blue-600 text-white text-center py-3 rounded-lg hover:bg-blue-700 transition-colors">
|
||||||
|
<i class="fas fa-eye mr-2"></i>
|
||||||
|
데모 보기
|
||||||
|
</a>
|
||||||
|
<a href="https://adminlte.io/" target="_blank"
|
||||||
|
class="flex-1 bg-gray-200 text-gray-700 text-center py-3 rounded-lg hover:bg-gray-300 transition-colors">
|
||||||
|
<i class="fas fa-external-link-alt mr-2"></i>
|
||||||
|
공식 사이트
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tabler Card -->
|
||||||
|
<div class="bg-white rounded-xl shadow-lg overflow-hidden">
|
||||||
|
<div class="p-6 bg-gradient-to-r from-purple-500 to-pink-600 text-white">
|
||||||
|
<h2 class="text-2xl font-bold mb-2">Tabler</h2>
|
||||||
|
<p class="text-purple-100">모던 관리자 대시보드 템플릿</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="p-6">
|
||||||
|
<div class="space-y-4 mb-6">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<i class="fas fa-check-circle text-green-500 mr-3"></i>
|
||||||
|
<span>모던한 디자인</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<i class="fas fa-check-circle text-green-500 mr-3"></i>
|
||||||
|
<span>빠른 성능</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<i class="fas fa-check-circle text-green-500 mr-3"></i>
|
||||||
|
<span>SVG 아이콘 시스템</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<i class="fas fa-star text-yellow-500 mr-3"></i>
|
||||||
|
<span>Bootstrap 5 기반</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<i class="fas fa-star text-yellow-500 mr-3"></i>
|
||||||
|
<span>TypeScript 지원</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<i class="fas fa-star text-yellow-500 mr-3"></i>
|
||||||
|
<span>최신 웹 표준</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-6">
|
||||||
|
<h4 class="font-semibold mb-3">주요 기능:</h4>
|
||||||
|
<ul class="text-sm text-gray-600 space-y-1">
|
||||||
|
<li>• 깔끔한 미니멀 디자인</li>
|
||||||
|
<li>• 3000+ 무료 SVG 아이콘</li>
|
||||||
|
<li>• 드롭다운 메뉴</li>
|
||||||
|
<li>• 모바일 최적화</li>
|
||||||
|
<li>• 빠른 로딩 속도</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex space-x-3">
|
||||||
|
<a href="/demo/demo-tabler" target="_blank"
|
||||||
|
class="flex-1 bg-purple-600 text-white text-center py-3 rounded-lg hover:bg-purple-700 transition-colors">
|
||||||
|
<i class="fas fa-eye mr-2"></i>
|
||||||
|
데모 보기
|
||||||
|
</a>
|
||||||
|
<a href="https://tabler.io/" target="_blank"
|
||||||
|
class="flex-1 bg-gray-200 text-gray-700 text-center py-3 rounded-lg hover:bg-gray-300 transition-colors">
|
||||||
|
<i class="fas fa-external-link-alt mr-2"></i>
|
||||||
|
공식 사이트
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Comparison Table -->
|
||||||
|
<div class="bg-white rounded-xl shadow-lg overflow-hidden mb-12">
|
||||||
|
<div class="p-6 bg-gradient-to-r from-gray-800 to-gray-900 text-white">
|
||||||
|
<h3 class="text-xl font-bold">상세 비교표</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table class="w-full">
|
||||||
|
<thead class="bg-gray-50">
|
||||||
|
<tr>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">기능</th>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">AdminLTE</th>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Tabler</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="bg-white divide-y divide-gray-200">
|
||||||
|
<tr>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">가격</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-green-600">완전 무료</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-green-600">무료 + Pro 버전</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="bg-gray-50">
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">한국어 지원</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-green-600">우수</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-yellow-600">보통</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">디자인</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-blue-600">클래식</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-purple-600">모던</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="bg-gray-50">
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">컴포넌트 수</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-green-600">매우 많음</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-blue-600">적당함</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">성능</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-blue-600">양호</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-green-600">우수</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="bg-gray-50">
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">커뮤니티</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-green-600">대규모</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-blue-600">성장중</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">학습 곡선</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-green-600">쉬움</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-yellow-600">보통</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Recommendation -->
|
||||||
|
<div class="bg-gradient-to-r from-green-400 to-blue-500 rounded-xl p-8 text-white">
|
||||||
|
<div class="text-center">
|
||||||
|
<h3 class="text-2xl font-bold mb-4">
|
||||||
|
<i class="fas fa-thumbs-up mr-2"></i>
|
||||||
|
추천 결과
|
||||||
|
</h3>
|
||||||
|
<p class="text-lg mb-6">
|
||||||
|
SmartSolTech 웹사이트의 특성을 고려한 맞춤 추천
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="bg-white bg-opacity-20 rounded-lg p-6 max-w-2xl mx-auto">
|
||||||
|
<h4 class="text-xl font-semibold mb-3">
|
||||||
|
🏆 AdminLTE 3 추천
|
||||||
|
</h4>
|
||||||
|
<p class="text-left">
|
||||||
|
<strong>추천 이유:</strong><br>
|
||||||
|
• 한국 고객을 위한 완벽한 한국어 지원<br>
|
||||||
|
• 무료로 모든 기능 사용 가능<br>
|
||||||
|
• 기존 시스템과의 호환성 우수<br>
|
||||||
|
• 풍부한 문서화와 커뮤니티 지원<br>
|
||||||
|
• 기업용 웹사이트에 적합한 안정적인 디자인
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Back to Main Site -->
|
||||||
|
<div class="text-center mt-12">
|
||||||
|
<a href="/" class="inline-flex items-center px-6 py-3 bg-gray-800 text-white rounded-lg hover:bg-gray-900 transition-colors">
|
||||||
|
<i class="fas fa-arrow-left mr-2"></i>
|
||||||
|
메인 사이트로 돌아가기
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
361
.history/views/admin/banner-editor_20251026215001.ejs
Normal file
@@ -0,0 +1,361 @@
|
|||||||
|
<!-- Content Header (Page header) -->
|
||||||
|
<section class="content-header">
|
||||||
|
<div class="container-fluid">
|
||||||
|
<div class="row mb-2">
|
||||||
|
<div class="col-sm-6">
|
||||||
|
<h1><i class="fas fa-images mr-2"></i>Редактор Баннеров</h1>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-6">
|
||||||
|
<ol class="breadcrumb float-sm-right">
|
||||||
|
<li class="breadcrumb-item"><a href="/admin">Админ</a></li>
|
||||||
|
<li class="breadcrumb-item active">Баннеры</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Main content -->
|
||||||
|
<section class="content">
|
||||||
|
<div class="container-fluid">
|
||||||
|
<!-- Banner Upload Card -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title">Загрузить новый баннер</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="upload-zone p-4 text-center border-2 border-dashed border-gray-300 rounded"
|
||||||
|
ondrop="handleDrop(event)" ondragover="handleDragOver(event)" ondragleave="handleDragLeave(event)">
|
||||||
|
<i class="fas fa-cloud-upload-alt fa-3x text-muted mb-3"></i>
|
||||||
|
<p class="h5 text-muted mb-2">Перетащите изображения сюда или нажмите для выбора</p>
|
||||||
|
<p class="text-muted">Поддерживаются: JPG, PNG, GIF (максимум 10MB)</p>
|
||||||
|
<input type="file" id="banner-upload" multiple accept="image/*" class="d-none">
|
||||||
|
<button type="button" class="btn btn-primary mt-3" onclick="document.getElementById('banner-upload').click()">
|
||||||
|
<i class="fas fa-plus mr-1"></i>Выбрать файлы
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Current Banners Card -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title">Текущие баннеры</h3>
|
||||||
|
<div class="card-tools">
|
||||||
|
<button class="btn btn-sm btn-default" onclick="loadBanners()">
|
||||||
|
<i class="fas fa-sync-alt"></i> Обновить
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div id="banners-list" class="row">
|
||||||
|
<!-- Banners will be loaded here -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Empty state -->
|
||||||
|
<div id="empty-state" class="text-center py-5" style="display: none;">
|
||||||
|
<i class="fas fa-images fa-4x text-muted mb-3"></i>
|
||||||
|
<h4 class="text-muted">Нет загруженных баннеров</h4>
|
||||||
|
<p class="text-muted">Загрузите ваши первые баннеры используя форму выше</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Banner Edit Modal -->
|
||||||
|
<div class="modal fade" id="edit-banner-modal" tabindex="-1" role="dialog">
|
||||||
|
<div class="modal-dialog modal-lg" role="document">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h4 class="modal-title">Редактировать баннер</h4>
|
||||||
|
<button type="button" class="close" data-dismiss="modal">
|
||||||
|
<span>×</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<form id="edit-banner-form">
|
||||||
|
<div class="modal-body">
|
||||||
|
<input type="hidden" id="edit-banner-id">
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<img id="edit-banner-preview" src="" alt="Banner preview" class="img-fluid rounded">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="edit-banner-title">Заголовок</label>
|
||||||
|
<input type="text" class="form-control" id="edit-banner-title" name="title" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="edit-banner-subtitle">Подзаголовок</label>
|
||||||
|
<input type="text" class="form-control" id="edit-banner-subtitle" name="subtitle">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="edit-banner-description">Описание</label>
|
||||||
|
<textarea class="form-control" id="edit-banner-description" name="description" rows="3"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="edit-banner-button-text">Текст кнопки</label>
|
||||||
|
<input type="text" class="form-control" id="edit-banner-button-text" name="buttonText">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="edit-banner-button-url">Ссылка кнопки</label>
|
||||||
|
<input type="url" class="form-control" id="edit-banner-button-url" name="buttonUrl">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="edit-banner-order">Порядок отображения</label>
|
||||||
|
<input type="number" class="form-control" id="edit-banner-order" name="order" min="0">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="custom-control custom-switch">
|
||||||
|
<input type="checkbox" class="custom-control-input" id="edit-banner-active" name="active">
|
||||||
|
<label class="custom-control-label" for="edit-banner-active">Активен</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-dismiss="modal">Отмена</button>
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
<i class="fas fa-save mr-1"></i>Сохранить
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.upload-zone.dragover {
|
||||||
|
border-color: #007bff !important;
|
||||||
|
background-color: #f8f9fa !important;
|
||||||
|
}
|
||||||
|
.banner-item {
|
||||||
|
transition: transform 0.2s;
|
||||||
|
}
|
||||||
|
.banner-item:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let banners = [];
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
loadBanners();
|
||||||
|
|
||||||
|
// Setup file upload
|
||||||
|
document.getElementById('banner-upload').addEventListener('change', handleFileSelect);
|
||||||
|
|
||||||
|
// Setup edit form
|
||||||
|
document.getElementById('edit-banner-form').addEventListener('submit', saveBanner);
|
||||||
|
});
|
||||||
|
|
||||||
|
async function loadBanners() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/admin/banners');
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
banners = data.banners;
|
||||||
|
renderBanners();
|
||||||
|
} else {
|
||||||
|
throw new Error(data.message);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading banners:', error);
|
||||||
|
alert('Ошибка загрузки баннеров: ' + error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderBanners() {
|
||||||
|
const container = document.getElementById('banners-list');
|
||||||
|
const emptyState = document.getElementById('empty-state');
|
||||||
|
|
||||||
|
if (banners.length === 0) {
|
||||||
|
container.innerHTML = '';
|
||||||
|
emptyState.style.display = 'block';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
emptyState.style.display = 'none';
|
||||||
|
|
||||||
|
container.innerHTML = banners.map(banner => `
|
||||||
|
<div class="col-md-4 mb-4">
|
||||||
|
<div class="card banner-item">
|
||||||
|
<img src="${banner.imageUrl}" class="card-img-top" style="height: 200px; object-fit: cover;">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">${banner.title || 'Без названия'}</h5>
|
||||||
|
<p class="card-text text-muted small">${banner.subtitle || ''}</p>
|
||||||
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
|
<div class="btn-group">
|
||||||
|
<button class="btn btn-sm btn-outline-primary" onclick="editBanner('${banner._id}')">
|
||||||
|
<i class="fas fa-edit"></i>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-sm btn-outline-danger" onclick="deleteBanner('${banner._id}')">
|
||||||
|
<i class="fas fa-trash"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<small class="text-muted">
|
||||||
|
${banner.active ? '<span class="badge badge-success">Активен</span>' : '<span class="badge badge-secondary">Неактивен</span>'}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDragOver(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.currentTarget.classList.add('dragover');
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDragLeave(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.currentTarget.classList.remove('dragover');
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDrop(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.currentTarget.classList.remove('dragover');
|
||||||
|
|
||||||
|
const files = Array.from(event.dataTransfer.files);
|
||||||
|
uploadFiles(files);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleFileSelect(event) {
|
||||||
|
const files = Array.from(event.target.files);
|
||||||
|
uploadFiles(files);
|
||||||
|
event.target.value = ''; // Reset input
|
||||||
|
}
|
||||||
|
|
||||||
|
async function uploadFiles(files) {
|
||||||
|
for (const file of files) {
|
||||||
|
if (!file.type.startsWith('image/')) {
|
||||||
|
alert(`Файл ${file.name} не является изображением`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file.size > 10 * 1024 * 1024) { // 10MB
|
||||||
|
alert(`Файл ${file.name} слишком большой (максимум 10MB)`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
await uploadFile(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
loadBanners(); // Refresh the list
|
||||||
|
}
|
||||||
|
|
||||||
|
async function uploadFile(file) {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('banner', file);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/admin/banners/upload', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (!data.success) {
|
||||||
|
throw new Error(data.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Файл ${file.name} успешно загружен`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error uploading ${file.name}:`, error);
|
||||||
|
alert(`Ошибка загрузки ${file.name}: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function editBanner(bannerId) {
|
||||||
|
const banner = banners.find(b => b._id === bannerId);
|
||||||
|
if (!banner) return;
|
||||||
|
|
||||||
|
// Fill the form
|
||||||
|
document.getElementById('edit-banner-id').value = banner._id;
|
||||||
|
document.getElementById('edit-banner-preview').src = banner.imageUrl;
|
||||||
|
document.getElementById('edit-banner-title').value = banner.title || '';
|
||||||
|
document.getElementById('edit-banner-subtitle').value = banner.subtitle || '';
|
||||||
|
document.getElementById('edit-banner-description').value = banner.description || '';
|
||||||
|
document.getElementById('edit-banner-button-text').value = banner.buttonText || '';
|
||||||
|
document.getElementById('edit-banner-button-url').value = banner.buttonUrl || '';
|
||||||
|
document.getElementById('edit-banner-order').value = banner.order || 0;
|
||||||
|
document.getElementById('edit-banner-active').checked = banner.active || false;
|
||||||
|
|
||||||
|
// Show modal
|
||||||
|
$('#edit-banner-modal').modal('show');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveBanner(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
const formData = new FormData(event.target);
|
||||||
|
const bannerId = document.getElementById('edit-banner-id').value;
|
||||||
|
|
||||||
|
const bannerData = {
|
||||||
|
title: formData.get('title'),
|
||||||
|
subtitle: formData.get('subtitle'),
|
||||||
|
description: formData.get('description'),
|
||||||
|
buttonText: formData.get('buttonText'),
|
||||||
|
buttonUrl: formData.get('buttonUrl'),
|
||||||
|
order: parseInt(formData.get('order')) || 0,
|
||||||
|
active: formData.has('active')
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/admin/banners/${bannerId}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(bannerData)
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
$('#edit-banner-modal').modal('hide');
|
||||||
|
loadBanners();
|
||||||
|
alert('Баннер обновлен');
|
||||||
|
} else {
|
||||||
|
throw new Error(data.message);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error saving banner:', error);
|
||||||
|
alert('Ошибка сохранения баннера: ' + error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteBanner(bannerId) {
|
||||||
|
if (!confirm('Вы уверены, что хотите удалить этот баннер?')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/admin/banners/${bannerId}`, {
|
||||||
|
method: 'DELETE'
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
loadBanners();
|
||||||
|
alert('Баннер удален');
|
||||||
|
} else {
|
||||||
|
throw new Error(data.message);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error deleting banner:', error);
|
||||||
|
alert('Ошибка удаления баннера: ' + error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
361
.history/views/admin/banner-editor_20251026215051.ejs
Normal file
@@ -0,0 +1,361 @@
|
|||||||
|
<!-- Content Header (Page header) -->
|
||||||
|
<section class="content-header">
|
||||||
|
<div class="container-fluid">
|
||||||
|
<div class="row mb-2">
|
||||||
|
<div class="col-sm-6">
|
||||||
|
<h1><i class="fas fa-images mr-2"></i>Редактор Баннеров</h1>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-6">
|
||||||
|
<ol class="breadcrumb float-sm-right">
|
||||||
|
<li class="breadcrumb-item"><a href="/admin">Админ</a></li>
|
||||||
|
<li class="breadcrumb-item active">Баннеры</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Main content -->
|
||||||
|
<section class="content">
|
||||||
|
<div class="container-fluid">
|
||||||
|
<!-- Banner Upload Card -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title">Загрузить новый баннер</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="upload-zone p-4 text-center border-2 border-dashed border-gray-300 rounded"
|
||||||
|
ondrop="handleDrop(event)" ondragover="handleDragOver(event)" ondragleave="handleDragLeave(event)">
|
||||||
|
<i class="fas fa-cloud-upload-alt fa-3x text-muted mb-3"></i>
|
||||||
|
<p class="h5 text-muted mb-2">Перетащите изображения сюда или нажмите для выбора</p>
|
||||||
|
<p class="text-muted">Поддерживаются: JPG, PNG, GIF (максимум 10MB)</p>
|
||||||
|
<input type="file" id="banner-upload" multiple accept="image/*" class="d-none">
|
||||||
|
<button type="button" class="btn btn-primary mt-3" onclick="document.getElementById('banner-upload').click()">
|
||||||
|
<i class="fas fa-plus mr-1"></i>Выбрать файлы
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Current Banners Card -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title">Текущие баннеры</h3>
|
||||||
|
<div class="card-tools">
|
||||||
|
<button class="btn btn-sm btn-default" onclick="loadBanners()">
|
||||||
|
<i class="fas fa-sync-alt"></i> Обновить
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div id="banners-list" class="row">
|
||||||
|
<!-- Banners will be loaded here -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Empty state -->
|
||||||
|
<div id="empty-state" class="text-center py-5" style="display: none;">
|
||||||
|
<i class="fas fa-images fa-4x text-muted mb-3"></i>
|
||||||
|
<h4 class="text-muted">Нет загруженных баннеров</h4>
|
||||||
|
<p class="text-muted">Загрузите ваши первые баннеры используя форму выше</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Banner Edit Modal -->
|
||||||
|
<div class="modal fade" id="edit-banner-modal" tabindex="-1" role="dialog">
|
||||||
|
<div class="modal-dialog modal-lg" role="document">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h4 class="modal-title">Редактировать баннер</h4>
|
||||||
|
<button type="button" class="close" data-dismiss="modal">
|
||||||
|
<span>×</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<form id="edit-banner-form">
|
||||||
|
<div class="modal-body">
|
||||||
|
<input type="hidden" id="edit-banner-id">
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<img id="edit-banner-preview" src="" alt="Banner preview" class="img-fluid rounded">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="edit-banner-title">Заголовок</label>
|
||||||
|
<input type="text" class="form-control" id="edit-banner-title" name="title" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="edit-banner-subtitle">Подзаголовок</label>
|
||||||
|
<input type="text" class="form-control" id="edit-banner-subtitle" name="subtitle">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="edit-banner-description">Описание</label>
|
||||||
|
<textarea class="form-control" id="edit-banner-description" name="description" rows="3"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="edit-banner-button-text">Текст кнопки</label>
|
||||||
|
<input type="text" class="form-control" id="edit-banner-button-text" name="buttonText">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="edit-banner-button-url">Ссылка кнопки</label>
|
||||||
|
<input type="url" class="form-control" id="edit-banner-button-url" name="buttonUrl">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="edit-banner-order">Порядок отображения</label>
|
||||||
|
<input type="number" class="form-control" id="edit-banner-order" name="order" min="0">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="custom-control custom-switch">
|
||||||
|
<input type="checkbox" class="custom-control-input" id="edit-banner-active" name="active">
|
||||||
|
<label class="custom-control-label" for="edit-banner-active">Активен</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-dismiss="modal">Отмена</button>
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
<i class="fas fa-save mr-1"></i>Сохранить
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.upload-zone.dragover {
|
||||||
|
border-color: #007bff !important;
|
||||||
|
background-color: #f8f9fa !important;
|
||||||
|
}
|
||||||
|
.banner-item {
|
||||||
|
transition: transform 0.2s;
|
||||||
|
}
|
||||||
|
.banner-item:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let banners = [];
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
loadBanners();
|
||||||
|
|
||||||
|
// Setup file upload
|
||||||
|
document.getElementById('banner-upload').addEventListener('change', handleFileSelect);
|
||||||
|
|
||||||
|
// Setup edit form
|
||||||
|
document.getElementById('edit-banner-form').addEventListener('submit', saveBanner);
|
||||||
|
});
|
||||||
|
|
||||||
|
async function loadBanners() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/admin/banners');
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
banners = data.banners;
|
||||||
|
renderBanners();
|
||||||
|
} else {
|
||||||
|
throw new Error(data.message);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading banners:', error);
|
||||||
|
alert('Ошибка загрузки баннеров: ' + error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderBanners() {
|
||||||
|
const container = document.getElementById('banners-list');
|
||||||
|
const emptyState = document.getElementById('empty-state');
|
||||||
|
|
||||||
|
if (banners.length === 0) {
|
||||||
|
container.innerHTML = '';
|
||||||
|
emptyState.style.display = 'block';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
emptyState.style.display = 'none';
|
||||||
|
|
||||||
|
container.innerHTML = banners.map(banner => `
|
||||||
|
<div class="col-md-4 mb-4">
|
||||||
|
<div class="card banner-item">
|
||||||
|
<img src="${banner.imageUrl}" class="card-img-top" style="height: 200px; object-fit: cover;">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">${banner.title || 'Без названия'}</h5>
|
||||||
|
<p class="card-text text-muted small">${banner.subtitle || ''}</p>
|
||||||
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
|
<div class="btn-group">
|
||||||
|
<button class="btn btn-sm btn-outline-primary" onclick="editBanner('${banner._id}')">
|
||||||
|
<i class="fas fa-edit"></i>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-sm btn-outline-danger" onclick="deleteBanner('${banner._id}')">
|
||||||
|
<i class="fas fa-trash"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<small class="text-muted">
|
||||||
|
${banner.active ? '<span class="badge badge-success">Активен</span>' : '<span class="badge badge-secondary">Неактивен</span>'}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDragOver(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.currentTarget.classList.add('dragover');
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDragLeave(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.currentTarget.classList.remove('dragover');
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDrop(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.currentTarget.classList.remove('dragover');
|
||||||
|
|
||||||
|
const files = Array.from(event.dataTransfer.files);
|
||||||
|
uploadFiles(files);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleFileSelect(event) {
|
||||||
|
const files = Array.from(event.target.files);
|
||||||
|
uploadFiles(files);
|
||||||
|
event.target.value = ''; // Reset input
|
||||||
|
}
|
||||||
|
|
||||||
|
async function uploadFiles(files) {
|
||||||
|
for (const file of files) {
|
||||||
|
if (!file.type.startsWith('image/')) {
|
||||||
|
alert(`Файл ${file.name} не является изображением`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file.size > 10 * 1024 * 1024) { // 10MB
|
||||||
|
alert(`Файл ${file.name} слишком большой (максимум 10MB)`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
await uploadFile(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
loadBanners(); // Refresh the list
|
||||||
|
}
|
||||||
|
|
||||||
|
async function uploadFile(file) {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('banner', file);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/admin/banners/upload', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (!data.success) {
|
||||||
|
throw new Error(data.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Файл ${file.name} успешно загружен`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error uploading ${file.name}:`, error);
|
||||||
|
alert(`Ошибка загрузки ${file.name}: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function editBanner(bannerId) {
|
||||||
|
const banner = banners.find(b => b._id === bannerId);
|
||||||
|
if (!banner) return;
|
||||||
|
|
||||||
|
// Fill the form
|
||||||
|
document.getElementById('edit-banner-id').value = banner._id;
|
||||||
|
document.getElementById('edit-banner-preview').src = banner.imageUrl;
|
||||||
|
document.getElementById('edit-banner-title').value = banner.title || '';
|
||||||
|
document.getElementById('edit-banner-subtitle').value = banner.subtitle || '';
|
||||||
|
document.getElementById('edit-banner-description').value = banner.description || '';
|
||||||
|
document.getElementById('edit-banner-button-text').value = banner.buttonText || '';
|
||||||
|
document.getElementById('edit-banner-button-url').value = banner.buttonUrl || '';
|
||||||
|
document.getElementById('edit-banner-order').value = banner.order || 0;
|
||||||
|
document.getElementById('edit-banner-active').checked = banner.active || false;
|
||||||
|
|
||||||
|
// Show modal
|
||||||
|
$('#edit-banner-modal').modal('show');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveBanner(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
const formData = new FormData(event.target);
|
||||||
|
const bannerId = document.getElementById('edit-banner-id').value;
|
||||||
|
|
||||||
|
const bannerData = {
|
||||||
|
title: formData.get('title'),
|
||||||
|
subtitle: formData.get('subtitle'),
|
||||||
|
description: formData.get('description'),
|
||||||
|
buttonText: formData.get('buttonText'),
|
||||||
|
buttonUrl: formData.get('buttonUrl'),
|
||||||
|
order: parseInt(formData.get('order')) || 0,
|
||||||
|
active: formData.has('active')
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/admin/banners/${bannerId}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(bannerData)
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
$('#edit-banner-modal').modal('hide');
|
||||||
|
loadBanners();
|
||||||
|
alert('Баннер обновлен');
|
||||||
|
} else {
|
||||||
|
throw new Error(data.message);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error saving banner:', error);
|
||||||
|
alert('Ошибка сохранения баннера: ' + error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteBanner(bannerId) {
|
||||||
|
if (!confirm('Вы уверены, что хотите удалить этот баннер?')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/admin/banners/${bannerId}`, {
|
||||||
|
method: 'DELETE'
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
loadBanners();
|
||||||
|
alert('Баннер удален');
|
||||||
|
} else {
|
||||||
|
throw new Error(data.message);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error deleting banner:', error);
|
||||||
|
alert('Ошибка удаления баннера: ' + error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
359
.history/views/admin/dashboard-adminlte_20251026211437.ejs
Normal file
@@ -0,0 +1,359 @@
|
|||||||
|
<!-- Dashboard Content -->
|
||||||
|
<div class="row">
|
||||||
|
<!-- Stats Cards -->
|
||||||
|
<div class="col-lg-3 col-6">
|
||||||
|
<div class="info-box">
|
||||||
|
<span class="info-box-icon bg-info elevation-1">
|
||||||
|
<i class="fas fa-briefcase"></i>
|
||||||
|
</span>
|
||||||
|
<div class="info-box-content">
|
||||||
|
<span class="info-box-text">포트폴리오 프로젝트</span>
|
||||||
|
<span class="info-box-number"><%= stats.portfolioCount || 0 %></span>
|
||||||
|
<div class="progress">
|
||||||
|
<div class="progress-bar bg-info" style="width: 70%"></div>
|
||||||
|
</div>
|
||||||
|
<span class="progress-description">
|
||||||
|
70% 완료된 프로젝트
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-lg-3 col-6">
|
||||||
|
<div class="info-box">
|
||||||
|
<span class="info-box-icon bg-success elevation-1">
|
||||||
|
<i class="fas fa-cogs"></i>
|
||||||
|
</span>
|
||||||
|
<div class="info-box-content">
|
||||||
|
<span class="info-box-text">제공 서비스</span>
|
||||||
|
<span class="info-box-number"><%= stats.servicesCount || 0 %></span>
|
||||||
|
<div class="progress">
|
||||||
|
<div class="progress-bar bg-success" style="width: 100%"></div>
|
||||||
|
</div>
|
||||||
|
<span class="progress-description">
|
||||||
|
모든 서비스 활성화
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-lg-3 col-6">
|
||||||
|
<div class="info-box">
|
||||||
|
<span class="info-box-icon bg-warning elevation-1">
|
||||||
|
<i class="fas fa-envelope"></i>
|
||||||
|
</span>
|
||||||
|
<div class="info-box-content">
|
||||||
|
<span class="info-box-text">문의 메시지</span>
|
||||||
|
<span class="info-box-number"><%= stats.contactsCount || 0 %></span>
|
||||||
|
<div class="progress">
|
||||||
|
<div class="progress-bar bg-warning" style="width: 60%"></div>
|
||||||
|
</div>
|
||||||
|
<span class="progress-description">
|
||||||
|
60% 응답 완료
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-lg-3 col-6">
|
||||||
|
<div class="info-box">
|
||||||
|
<span class="info-box-icon bg-danger elevation-1">
|
||||||
|
<i class="fas fa-users"></i>
|
||||||
|
</span>
|
||||||
|
<div class="info-box-content">
|
||||||
|
<span class="info-box-text">관리자 계정</span>
|
||||||
|
<span class="info-box-number"><%= stats.usersCount || 0 %></span>
|
||||||
|
<div class="progress">
|
||||||
|
<div class="progress-bar bg-danger" style="width: 100%"></div>
|
||||||
|
</div>
|
||||||
|
<span class="progress-description">
|
||||||
|
모든 계정 활성화
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Main Content Row -->
|
||||||
|
<div class="row">
|
||||||
|
<!-- Recent Portfolio Projects -->
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title">
|
||||||
|
<i class="fas fa-briefcase mr-1"></i>
|
||||||
|
최근 포트폴리오 프로젝트
|
||||||
|
</h3>
|
||||||
|
<div class="card-tools">
|
||||||
|
<button type="button" class="btn btn-tool" data-card-widget="collapse">
|
||||||
|
<i class="fas fa-minus"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<% if (recentPortfolio && recentPortfolio.length > 0) { %>
|
||||||
|
<ul class="list-unstyled">
|
||||||
|
<% recentPortfolio.forEach(function(project, index) { %>
|
||||||
|
<li class="d-flex align-items-center <%= index < recentPortfolio.length - 1 ? 'border-bottom pb-3 mb-3' : '' %>">
|
||||||
|
<div class="flex-shrink-0">
|
||||||
|
<span class="badge badge-info badge-pill">
|
||||||
|
<i class="fas fa-code"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex-grow-1 ml-3">
|
||||||
|
<h6 class="mb-1 font-weight-bold"><%= project.title %></h6>
|
||||||
|
<p class="text-muted mb-1"><%= project.category %></p>
|
||||||
|
<small class="text-muted">
|
||||||
|
<i class="fas fa-calendar mr-1"></i>
|
||||||
|
<%= project.createdAt ? project.createdAt.toLocaleDateString('ko-KR') : '날짜 없음' %>
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
<div class="flex-shrink-0">
|
||||||
|
<span class="badge badge-<%= project.status === 'completed' ? 'success' : project.status === 'in-progress' ? 'warning' : 'secondary' %>">
|
||||||
|
<%= project.status === 'completed' ? '완료' : project.status === 'in-progress' ? '진행중' : '계획' %>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<% }); %>
|
||||||
|
</ul>
|
||||||
|
<div class="text-center mt-3">
|
||||||
|
<a href="/admin/portfolio" class="btn btn-primary btn-sm">
|
||||||
|
<i class="fas fa-eye mr-1"></i>
|
||||||
|
모든 프로젝트 보기
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<% } else { %>
|
||||||
|
<div class="text-center py-4">
|
||||||
|
<i class="fas fa-briefcase fa-3x text-muted mb-3"></i>
|
||||||
|
<p class="text-muted">아직 포트폴리오 프로젝트가 없습니다.</p>
|
||||||
|
<a href="/admin/portfolio/add" class="btn btn-primary btn-sm">
|
||||||
|
<i class="fas fa-plus mr-1"></i>
|
||||||
|
첫 번째 프로젝트 추가
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<% } %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Recent Contact Messages -->
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title">
|
||||||
|
<i class="fas fa-envelope mr-1"></i>
|
||||||
|
최근 문의 메시지
|
||||||
|
</h3>
|
||||||
|
<div class="card-tools">
|
||||||
|
<button type="button" class="btn btn-tool" data-card-widget="collapse">
|
||||||
|
<i class="fas fa-minus"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<% if (recentContacts && recentContacts.length > 0) { %>
|
||||||
|
<ul class="list-unstyled">
|
||||||
|
<% recentContacts.forEach(function(contact, index) { %>
|
||||||
|
<li class="d-flex align-items-center <%= index < recentContacts.length - 1 ? 'border-bottom pb-3 mb-3' : '' %>">
|
||||||
|
<div class="flex-shrink-0">
|
||||||
|
<div class="bg-primary text-white rounded-circle d-flex align-items-center justify-content-center" style="width: 40px; height: 40px;">
|
||||||
|
<%= contact.name ? contact.name.charAt(0).toUpperCase() : 'U' %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex-grow-1 ml-3">
|
||||||
|
<h6 class="mb-1 font-weight-bold"><%= contact.name %></h6>
|
||||||
|
<p class="text-muted mb-1"><%= contact.email %></p>
|
||||||
|
<small class="text-muted">
|
||||||
|
<i class="fas fa-clock mr-1"></i>
|
||||||
|
<%= contact.createdAt ? contact.createdAt.toLocaleDateString('ko-KR') : '날짜 없음' %>
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
<div class="flex-shrink-0">
|
||||||
|
<span class="badge badge-<%= contact.status === 'replied' ? 'success' : contact.status === 'pending' ? 'warning' : 'secondary' %>">
|
||||||
|
<%= contact.status === 'replied' ? '답변완료' : contact.status === 'pending' ? '대기중' : '신규' %>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<% }); %>
|
||||||
|
</ul>
|
||||||
|
<div class="text-center mt-3">
|
||||||
|
<a href="/admin/contacts" class="btn btn-warning btn-sm">
|
||||||
|
<i class="fas fa-envelope-open mr-1"></i>
|
||||||
|
모든 문의 보기
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<% } else { %>
|
||||||
|
<div class="text-center py-4">
|
||||||
|
<i class="fas fa-envelope fa-3x text-muted mb-3"></i>
|
||||||
|
<p class="text-muted">새로운 문의가 없습니다.</p>
|
||||||
|
<small class="text-muted">고객 문의가 들어오면 여기에 표시됩니다.</small>
|
||||||
|
</div>
|
||||||
|
<% } %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Quick Actions & Tools -->
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title">
|
||||||
|
<i class="fas fa-bolt mr-1"></i>
|
||||||
|
빠른 작업
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-3 col-md-6">
|
||||||
|
<div class="info-box bg-gradient-info">
|
||||||
|
<span class="info-box-icon">
|
||||||
|
<i class="fas fa-plus"></i>
|
||||||
|
</span>
|
||||||
|
<div class="info-box-content">
|
||||||
|
<span class="info-box-text">새 프로젝트</span>
|
||||||
|
<span class="info-box-number">추가하기</span>
|
||||||
|
<div class="progress">
|
||||||
|
<div class="progress-bar" style="width: 100%"></div>
|
||||||
|
</div>
|
||||||
|
<a href="/admin/portfolio/add" class="progress-description text-white">
|
||||||
|
포트폴리오에 새 프로젝트 추가 →
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-lg-3 col-md-6">
|
||||||
|
<div class="info-box bg-gradient-success">
|
||||||
|
<span class="info-box-icon">
|
||||||
|
<i class="fas fa-cog"></i>
|
||||||
|
</span>
|
||||||
|
<div class="info-box-content">
|
||||||
|
<span class="info-box-text">서비스 관리</span>
|
||||||
|
<span class="info-box-number">설정</span>
|
||||||
|
<div class="progress">
|
||||||
|
<div class="progress-bar" style="width: 100%"></div>
|
||||||
|
</div>
|
||||||
|
<a href="/admin/services" class="progress-description text-white">
|
||||||
|
서비스 가격 및 내용 수정 →
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-lg-3 col-md-6">
|
||||||
|
<div class="info-box bg-gradient-warning">
|
||||||
|
<span class="info-box-icon">
|
||||||
|
<i class="fas fa-images"></i>
|
||||||
|
</span>
|
||||||
|
<div class="info-box-content">
|
||||||
|
<span class="info-box-text">미디어</span>
|
||||||
|
<span class="info-box-number">업로드</span>
|
||||||
|
<div class="progress">
|
||||||
|
<div class="progress-bar" style="width: 100%"></div>
|
||||||
|
</div>
|
||||||
|
<a href="/admin/media" class="progress-description text-white">
|
||||||
|
이미지 및 파일 관리 →
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-lg-3 col-md-6">
|
||||||
|
<div class="info-box bg-gradient-danger">
|
||||||
|
<span class="info-box-icon">
|
||||||
|
<i class="fas fa-wrench"></i>
|
||||||
|
</span>
|
||||||
|
<div class="info-box-content">
|
||||||
|
<span class="info-box-text">사이트 설정</span>
|
||||||
|
<span class="info-box-number">관리</span>
|
||||||
|
<div class="progress">
|
||||||
|
<div class="progress-bar" style="width: 100%"></div>
|
||||||
|
</div>
|
||||||
|
<a href="/admin/settings" class="progress-description text-white">
|
||||||
|
전체 사이트 설정 변경 →
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- System Status -->
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card card-primary">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title">
|
||||||
|
<i class="fas fa-server mr-1"></i>
|
||||||
|
시스템 상태
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-6">
|
||||||
|
<div class="description-block border-right">
|
||||||
|
<span class="description-percentage text-success">
|
||||||
|
<i class="fas fa-caret-up"></i> 99.2%
|
||||||
|
</span>
|
||||||
|
<h5 class="description-header">서버 업타임</h5>
|
||||||
|
<span class="description-text">지난 30일</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-6">
|
||||||
|
<div class="description-block">
|
||||||
|
<span class="description-percentage text-info">
|
||||||
|
<i class="fas fa-caret-up"></i> 2.3초
|
||||||
|
</span>
|
||||||
|
<h5 class="description-header">평균 응답시간</h5>
|
||||||
|
<span class="description-text">페이지 로딩</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card card-success">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title">
|
||||||
|
<i class="fab fa-telegram mr-1"></i>
|
||||||
|
텔레그램 봇 상태
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-6">
|
||||||
|
<div class="description-block border-right">
|
||||||
|
<span class="description-percentage text-success">
|
||||||
|
<i class="fas fa-check-circle"></i> 연결됨
|
||||||
|
</span>
|
||||||
|
<h5 class="description-header">봇 상태</h5>
|
||||||
|
<span class="description-text">정상 작동</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-6">
|
||||||
|
<div class="description-block">
|
||||||
|
<span class="description-percentage text-info">
|
||||||
|
<i class="fas fa-paper-plane"></i> 24개
|
||||||
|
</span>
|
||||||
|
<h5 class="description-header">전송된 알림</h5>
|
||||||
|
<span class="description-text">오늘</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-center mt-2">
|
||||||
|
<a href="/admin/telegram" class="btn btn-success btn-sm">
|
||||||
|
<i class="fab fa-telegram mr-1"></i>
|
||||||
|
텔레그램 설정
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
359
.history/views/admin/dashboard-adminlte_20251026211450.ejs
Normal file
@@ -0,0 +1,359 @@
|
|||||||
|
<!-- Dashboard Content -->
|
||||||
|
<div class="row">
|
||||||
|
<!-- Stats Cards -->
|
||||||
|
<div class="col-lg-3 col-6">
|
||||||
|
<div class="info-box">
|
||||||
|
<span class="info-box-icon bg-info elevation-1">
|
||||||
|
<i class="fas fa-briefcase"></i>
|
||||||
|
</span>
|
||||||
|
<div class="info-box-content">
|
||||||
|
<span class="info-box-text">포트폴리오 프로젝트</span>
|
||||||
|
<span class="info-box-number"><%= stats.portfolioCount || 0 %></span>
|
||||||
|
<div class="progress">
|
||||||
|
<div class="progress-bar bg-info" style="width: 70%"></div>
|
||||||
|
</div>
|
||||||
|
<span class="progress-description">
|
||||||
|
70% 완료된 프로젝트
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-lg-3 col-6">
|
||||||
|
<div class="info-box">
|
||||||
|
<span class="info-box-icon bg-success elevation-1">
|
||||||
|
<i class="fas fa-cogs"></i>
|
||||||
|
</span>
|
||||||
|
<div class="info-box-content">
|
||||||
|
<span class="info-box-text">제공 서비스</span>
|
||||||
|
<span class="info-box-number"><%= stats.servicesCount || 0 %></span>
|
||||||
|
<div class="progress">
|
||||||
|
<div class="progress-bar bg-success" style="width: 100%"></div>
|
||||||
|
</div>
|
||||||
|
<span class="progress-description">
|
||||||
|
모든 서비스 활성화
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-lg-3 col-6">
|
||||||
|
<div class="info-box">
|
||||||
|
<span class="info-box-icon bg-warning elevation-1">
|
||||||
|
<i class="fas fa-envelope"></i>
|
||||||
|
</span>
|
||||||
|
<div class="info-box-content">
|
||||||
|
<span class="info-box-text">문의 메시지</span>
|
||||||
|
<span class="info-box-number"><%= stats.contactsCount || 0 %></span>
|
||||||
|
<div class="progress">
|
||||||
|
<div class="progress-bar bg-warning" style="width: 60%"></div>
|
||||||
|
</div>
|
||||||
|
<span class="progress-description">
|
||||||
|
60% 응답 완료
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-lg-3 col-6">
|
||||||
|
<div class="info-box">
|
||||||
|
<span class="info-box-icon bg-danger elevation-1">
|
||||||
|
<i class="fas fa-users"></i>
|
||||||
|
</span>
|
||||||
|
<div class="info-box-content">
|
||||||
|
<span class="info-box-text">관리자 계정</span>
|
||||||
|
<span class="info-box-number"><%= stats.usersCount || 0 %></span>
|
||||||
|
<div class="progress">
|
||||||
|
<div class="progress-bar bg-danger" style="width: 100%"></div>
|
||||||
|
</div>
|
||||||
|
<span class="progress-description">
|
||||||
|
모든 계정 활성화
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Main Content Row -->
|
||||||
|
<div class="row">
|
||||||
|
<!-- Recent Portfolio Projects -->
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title">
|
||||||
|
<i class="fas fa-briefcase mr-1"></i>
|
||||||
|
최근 포트폴리오 프로젝트
|
||||||
|
</h3>
|
||||||
|
<div class="card-tools">
|
||||||
|
<button type="button" class="btn btn-tool" data-card-widget="collapse">
|
||||||
|
<i class="fas fa-minus"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<% if (recentPortfolio && recentPortfolio.length > 0) { %>
|
||||||
|
<ul class="list-unstyled">
|
||||||
|
<% recentPortfolio.forEach(function(project, index) { %>
|
||||||
|
<li class="d-flex align-items-center <%= index < recentPortfolio.length - 1 ? 'border-bottom pb-3 mb-3' : '' %>">
|
||||||
|
<div class="flex-shrink-0">
|
||||||
|
<span class="badge badge-info badge-pill">
|
||||||
|
<i class="fas fa-code"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex-grow-1 ml-3">
|
||||||
|
<h6 class="mb-1 font-weight-bold"><%= project.title %></h6>
|
||||||
|
<p class="text-muted mb-1"><%= project.category %></p>
|
||||||
|
<small class="text-muted">
|
||||||
|
<i class="fas fa-calendar mr-1"></i>
|
||||||
|
<%= project.createdAt ? project.createdAt.toLocaleDateString('ko-KR') : '날짜 없음' %>
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
<div class="flex-shrink-0">
|
||||||
|
<span class="badge badge-<%= project.status === 'completed' ? 'success' : project.status === 'in-progress' ? 'warning' : 'secondary' %>">
|
||||||
|
<%= project.status === 'completed' ? '완료' : project.status === 'in-progress' ? '진행중' : '계획' %>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<% }); %>
|
||||||
|
</ul>
|
||||||
|
<div class="text-center mt-3">
|
||||||
|
<a href="/admin/portfolio" class="btn btn-primary btn-sm">
|
||||||
|
<i class="fas fa-eye mr-1"></i>
|
||||||
|
모든 프로젝트 보기
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<% } else { %>
|
||||||
|
<div class="text-center py-4">
|
||||||
|
<i class="fas fa-briefcase fa-3x text-muted mb-3"></i>
|
||||||
|
<p class="text-muted">아직 포트폴리오 프로젝트가 없습니다.</p>
|
||||||
|
<a href="/admin/portfolio/add" class="btn btn-primary btn-sm">
|
||||||
|
<i class="fas fa-plus mr-1"></i>
|
||||||
|
첫 번째 프로젝트 추가
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<% } %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Recent Contact Messages -->
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title">
|
||||||
|
<i class="fas fa-envelope mr-1"></i>
|
||||||
|
최근 문의 메시지
|
||||||
|
</h3>
|
||||||
|
<div class="card-tools">
|
||||||
|
<button type="button" class="btn btn-tool" data-card-widget="collapse">
|
||||||
|
<i class="fas fa-minus"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<% if (recentContacts && recentContacts.length > 0) { %>
|
||||||
|
<ul class="list-unstyled">
|
||||||
|
<% recentContacts.forEach(function(contact, index) { %>
|
||||||
|
<li class="d-flex align-items-center <%= index < recentContacts.length - 1 ? 'border-bottom pb-3 mb-3' : '' %>">
|
||||||
|
<div class="flex-shrink-0">
|
||||||
|
<div class="bg-primary text-white rounded-circle d-flex align-items-center justify-content-center" style="width: 40px; height: 40px;">
|
||||||
|
<%= contact.name ? contact.name.charAt(0).toUpperCase() : 'U' %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex-grow-1 ml-3">
|
||||||
|
<h6 class="mb-1 font-weight-bold"><%= contact.name %></h6>
|
||||||
|
<p class="text-muted mb-1"><%= contact.email %></p>
|
||||||
|
<small class="text-muted">
|
||||||
|
<i class="fas fa-clock mr-1"></i>
|
||||||
|
<%= contact.createdAt ? contact.createdAt.toLocaleDateString('ko-KR') : '날짜 없음' %>
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
<div class="flex-shrink-0">
|
||||||
|
<span class="badge badge-<%= contact.status === 'replied' ? 'success' : contact.status === 'pending' ? 'warning' : 'secondary' %>">
|
||||||
|
<%= contact.status === 'replied' ? '답변완료' : contact.status === 'pending' ? '대기중' : '신규' %>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<% }); %>
|
||||||
|
</ul>
|
||||||
|
<div class="text-center mt-3">
|
||||||
|
<a href="/admin/contacts" class="btn btn-warning btn-sm">
|
||||||
|
<i class="fas fa-envelope-open mr-1"></i>
|
||||||
|
모든 문의 보기
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<% } else { %>
|
||||||
|
<div class="text-center py-4">
|
||||||
|
<i class="fas fa-envelope fa-3x text-muted mb-3"></i>
|
||||||
|
<p class="text-muted">새로운 문의가 없습니다.</p>
|
||||||
|
<small class="text-muted">고객 문의가 들어오면 여기에 표시됩니다.</small>
|
||||||
|
</div>
|
||||||
|
<% } %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Quick Actions & Tools -->
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title">
|
||||||
|
<i class="fas fa-bolt mr-1"></i>
|
||||||
|
빠른 작업
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-3 col-md-6">
|
||||||
|
<div class="info-box bg-gradient-info">
|
||||||
|
<span class="info-box-icon">
|
||||||
|
<i class="fas fa-plus"></i>
|
||||||
|
</span>
|
||||||
|
<div class="info-box-content">
|
||||||
|
<span class="info-box-text">새 프로젝트</span>
|
||||||
|
<span class="info-box-number">추가하기</span>
|
||||||
|
<div class="progress">
|
||||||
|
<div class="progress-bar" style="width: 100%"></div>
|
||||||
|
</div>
|
||||||
|
<a href="/admin/portfolio/add" class="progress-description text-white">
|
||||||
|
포트폴리오에 새 프로젝트 추가 →
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-lg-3 col-md-6">
|
||||||
|
<div class="info-box bg-gradient-success">
|
||||||
|
<span class="info-box-icon">
|
||||||
|
<i class="fas fa-cog"></i>
|
||||||
|
</span>
|
||||||
|
<div class="info-box-content">
|
||||||
|
<span class="info-box-text">서비스 관리</span>
|
||||||
|
<span class="info-box-number">설정</span>
|
||||||
|
<div class="progress">
|
||||||
|
<div class="progress-bar" style="width: 100%"></div>
|
||||||
|
</div>
|
||||||
|
<a href="/admin/services" class="progress-description text-white">
|
||||||
|
서비스 가격 및 내용 수정 →
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-lg-3 col-md-6">
|
||||||
|
<div class="info-box bg-gradient-warning">
|
||||||
|
<span class="info-box-icon">
|
||||||
|
<i class="fas fa-images"></i>
|
||||||
|
</span>
|
||||||
|
<div class="info-box-content">
|
||||||
|
<span class="info-box-text">미디어</span>
|
||||||
|
<span class="info-box-number">업로드</span>
|
||||||
|
<div class="progress">
|
||||||
|
<div class="progress-bar" style="width: 100%"></div>
|
||||||
|
</div>
|
||||||
|
<a href="/admin/media" class="progress-description text-white">
|
||||||
|
이미지 및 파일 관리 →
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-lg-3 col-md-6">
|
||||||
|
<div class="info-box bg-gradient-danger">
|
||||||
|
<span class="info-box-icon">
|
||||||
|
<i class="fas fa-wrench"></i>
|
||||||
|
</span>
|
||||||
|
<div class="info-box-content">
|
||||||
|
<span class="info-box-text">사이트 설정</span>
|
||||||
|
<span class="info-box-number">관리</span>
|
||||||
|
<div class="progress">
|
||||||
|
<div class="progress-bar" style="width: 100%"></div>
|
||||||
|
</div>
|
||||||
|
<a href="/admin/settings" class="progress-description text-white">
|
||||||
|
전체 사이트 설정 변경 →
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- System Status -->
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card card-primary">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title">
|
||||||
|
<i class="fas fa-server mr-1"></i>
|
||||||
|
시스템 상태
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-6">
|
||||||
|
<div class="description-block border-right">
|
||||||
|
<span class="description-percentage text-success">
|
||||||
|
<i class="fas fa-caret-up"></i> 99.2%
|
||||||
|
</span>
|
||||||
|
<h5 class="description-header">서버 업타임</h5>
|
||||||
|
<span class="description-text">지난 30일</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-6">
|
||||||
|
<div class="description-block">
|
||||||
|
<span class="description-percentage text-info">
|
||||||
|
<i class="fas fa-caret-up"></i> 2.3초
|
||||||
|
</span>
|
||||||
|
<h5 class="description-header">평균 응답시간</h5>
|
||||||
|
<span class="description-text">페이지 로딩</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card card-success">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title">
|
||||||
|
<i class="fab fa-telegram mr-1"></i>
|
||||||
|
텔레그램 봇 상태
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-6">
|
||||||
|
<div class="description-block border-right">
|
||||||
|
<span class="description-percentage text-success">
|
||||||
|
<i class="fas fa-check-circle"></i> 연결됨
|
||||||
|
</span>
|
||||||
|
<h5 class="description-header">봇 상태</h5>
|
||||||
|
<span class="description-text">정상 작동</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-6">
|
||||||
|
<div class="description-block">
|
||||||
|
<span class="description-percentage text-info">
|
||||||
|
<i class="fas fa-paper-plane"></i> 24개
|
||||||
|
</span>
|
||||||
|
<h5 class="description-header">전송된 알림</h5>
|
||||||
|
<span class="description-text">오늘</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-center mt-2">
|
||||||
|
<a href="/admin/telegram" class="btn btn-success btn-sm">
|
||||||
|
<i class="fab fa-telegram mr-1"></i>
|
||||||
|
텔레그램 설정
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
342
.history/views/admin/dashboard-tabler_20251026211643.ejs
Normal file
@@ -0,0 +1,342 @@
|
|||||||
|
<!-- Tabler Dashboard -->
|
||||||
|
<div class="row row-deck row-cards">
|
||||||
|
<!-- Stats Cards -->
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="row row-cards">
|
||||||
|
<div class="col-sm-6 col-lg-3">
|
||||||
|
<div class="card card-sm">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row align-items-center">
|
||||||
|
<div class="col-auto">
|
||||||
|
<span class="bg-primary text-white avatar">
|
||||||
|
<i class="fas fa-briefcase"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<div class="font-weight-medium">
|
||||||
|
포트폴리오 프로젝트
|
||||||
|
</div>
|
||||||
|
<div class="text-muted">
|
||||||
|
<%= stats.portfolioCount || 0 %>개 프로젝트
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-sm-6 col-lg-3">
|
||||||
|
<div class="card card-sm">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row align-items-center">
|
||||||
|
<div class="col-auto">
|
||||||
|
<span class="bg-green text-white avatar">
|
||||||
|
<i class="fas fa-cogs"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<div class="font-weight-medium">
|
||||||
|
제공 서비스
|
||||||
|
</div>
|
||||||
|
<div class="text-muted">
|
||||||
|
<%= stats.servicesCount || 0 %>개 서비스
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-sm-6 col-lg-3">
|
||||||
|
<div class="card card-sm">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row align-items-center">
|
||||||
|
<div class="col-auto">
|
||||||
|
<span class="bg-yellow text-white avatar">
|
||||||
|
<i class="fas fa-envelope"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<div class="font-weight-medium">
|
||||||
|
문의 메시지
|
||||||
|
</div>
|
||||||
|
<div class="text-muted">
|
||||||
|
<%= stats.contactsCount || 0 %>개 메시지
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-sm-6 col-lg-3">
|
||||||
|
<div class="card card-sm">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row align-items-center">
|
||||||
|
<div class="col-auto">
|
||||||
|
<span class="bg-red text-white avatar">
|
||||||
|
<i class="fas fa-users"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<div class="font-weight-medium">
|
||||||
|
관리자 계정
|
||||||
|
</div>
|
||||||
|
<div class="text-muted">
|
||||||
|
<%= stats.usersCount || 0 %>명 사용자
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Recent Portfolio Projects -->
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title">최근 포트폴리오 프로젝트</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<% if (recentPortfolio && recentPortfolio.length > 0) { %>
|
||||||
|
<div class="divide-y">
|
||||||
|
<% recentPortfolio.forEach(function(project, index) { %>
|
||||||
|
<div class="row <%= index < recentPortfolio.length - 1 ? 'py-2' : 'pt-2' %>">
|
||||||
|
<div class="col-auto">
|
||||||
|
<span class="avatar avatar-sm bg-blue-lt">
|
||||||
|
<i class="fas fa-code"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<div class="text-truncate">
|
||||||
|
<strong><%= project.title %></strong>
|
||||||
|
</div>
|
||||||
|
<div class="text-muted">
|
||||||
|
<%= project.category %> •
|
||||||
|
<%= project.createdAt ? project.createdAt.toLocaleDateString('ko-KR') : '날짜 없음' %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<span class="badge bg-<%= project.status === 'completed' ? 'green' : project.status === 'in-progress' ? 'yellow' : 'blue' %>">
|
||||||
|
<%= project.status === 'completed' ? '완료' : project.status === 'in-progress' ? '진행중' : '계획' %>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% }); %>
|
||||||
|
</div>
|
||||||
|
<div class="mt-3">
|
||||||
|
<a href="/admin/portfolio" class="btn btn-primary btn-sm w-100">
|
||||||
|
모든 프로젝트 보기
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<% } else { %>
|
||||||
|
<div class="empty">
|
||||||
|
<div class="empty-img">
|
||||||
|
<i class="fas fa-briefcase fa-3x text-muted"></i>
|
||||||
|
</div>
|
||||||
|
<p class="empty-title">포트폴리오가 없습니다</p>
|
||||||
|
<p class="empty-subtitle text-muted">
|
||||||
|
첫 번째 프로젝트를 추가해보세요
|
||||||
|
</p>
|
||||||
|
<div class="empty-action">
|
||||||
|
<a href="/admin/portfolio/add" class="btn btn-primary">
|
||||||
|
<i class="fas fa-plus"></i>
|
||||||
|
프로젝트 추가
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% } %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Recent Contact Messages -->
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title">최근 문의 메시지</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<% if (recentContacts && recentContacts.length > 0) { %>
|
||||||
|
<div class="divide-y">
|
||||||
|
<% recentContacts.forEach(function(contact, index) { %>
|
||||||
|
<div class="row <%= index < recentContacts.length - 1 ? 'py-2' : 'pt-2' %>">
|
||||||
|
<div class="col-auto">
|
||||||
|
<span class="avatar avatar-sm">
|
||||||
|
<%= contact.name ? contact.name.charAt(0).toUpperCase() : 'U' %>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<div class="text-truncate">
|
||||||
|
<strong><%= contact.name %></strong>
|
||||||
|
</div>
|
||||||
|
<div class="text-muted">
|
||||||
|
<%= contact.email %> •
|
||||||
|
<%= contact.createdAt ? contact.createdAt.toLocaleDateString('ko-KR') : '날짜 없음' %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<span class="badge bg-<%= contact.status === 'replied' ? 'green' : contact.status === 'pending' ? 'yellow' : 'blue' %>">
|
||||||
|
<%= contact.status === 'replied' ? '답변완료' : contact.status === 'pending' ? '대기중' : '신규' %>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% }); %>
|
||||||
|
</div>
|
||||||
|
<div class="mt-3">
|
||||||
|
<a href="/admin/contacts" class="btn btn-warning btn-sm w-100">
|
||||||
|
모든 문의 보기
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<% } else { %>
|
||||||
|
<div class="empty">
|
||||||
|
<div class="empty-img">
|
||||||
|
<i class="fas fa-envelope fa-3x text-muted"></i>
|
||||||
|
</div>
|
||||||
|
<p class="empty-title">새로운 문의가 없습니다</p>
|
||||||
|
<p class="empty-subtitle text-muted">
|
||||||
|
고객 문의가 들어오면 여기에 표시됩니다
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<% } %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Quick Actions -->
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title">빠른 작업</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-3">
|
||||||
|
<a href="/admin/portfolio/add" class="card card-link bg-primary-lt">
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<div class="text-primary mb-3">
|
||||||
|
<i class="fas fa-plus fa-2x"></i>
|
||||||
|
</div>
|
||||||
|
<div class="font-weight-medium">새 프로젝트</div>
|
||||||
|
<div class="text-muted">포트폴리오에 추가</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-3">
|
||||||
|
<a href="/admin/services" class="card card-link bg-green-lt">
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<div class="text-green mb-3">
|
||||||
|
<i class="fas fa-cog fa-2x"></i>
|
||||||
|
</div>
|
||||||
|
<div class="font-weight-medium">서비스 관리</div>
|
||||||
|
<div class="text-muted">가격 및 내용 수정</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-3">
|
||||||
|
<a href="/admin/media" class="card card-link bg-yellow-lt">
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<div class="text-yellow mb-3">
|
||||||
|
<i class="fas fa-images fa-2x"></i>
|
||||||
|
</div>
|
||||||
|
<div class="font-weight-medium">미디어 업로드</div>
|
||||||
|
<div class="text-muted">이미지 및 파일 관리</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-3">
|
||||||
|
<a href="/admin/settings" class="card card-link bg-red-lt">
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<div class="text-red mb-3">
|
||||||
|
<i class="fas fa-wrench fa-2x"></i>
|
||||||
|
</div>
|
||||||
|
<div class="font-weight-medium">사이트 설정</div>
|
||||||
|
<div class="text-muted">전체 설정 관리</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- System Status & Analytics -->
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title">시스템 상태</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-6">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<div class="me-3">
|
||||||
|
<i class="fas fa-server fa-2x text-green"></i>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="h4 mb-0">99.2%</div>
|
||||||
|
<div class="text-muted">서버 업타임</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-6">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<div class="me-3">
|
||||||
|
<i class="fas fa-clock fa-2x text-blue"></i>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="h4 mb-0">2.3초</div>
|
||||||
|
<div class="text-muted">평균 응답시간</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Telegram Bot Status -->
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title">텔레그램 봇</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-6">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<div class="me-3">
|
||||||
|
<i class="fab fa-telegram fa-2x text-green"></i>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="h4 mb-0 text-green">연결됨</div>
|
||||||
|
<div class="text-muted">봇 상태</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-6">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<div class="me-3">
|
||||||
|
<i class="fas fa-paper-plane fa-2x text-blue"></i>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="h4 mb-0">24</div>
|
||||||
|
<div class="text-muted">오늘 전송</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-3">
|
||||||
|
<a href="/admin/telegram" class="btn btn-outline-primary w-100">
|
||||||
|
텔레그램 설정
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
342
.history/views/admin/dashboard-tabler_20251026211716.ejs
Normal file
@@ -0,0 +1,342 @@
|
|||||||
|
<!-- Tabler Dashboard -->
|
||||||
|
<div class="row row-deck row-cards">
|
||||||
|
<!-- Stats Cards -->
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="row row-cards">
|
||||||
|
<div class="col-sm-6 col-lg-3">
|
||||||
|
<div class="card card-sm">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row align-items-center">
|
||||||
|
<div class="col-auto">
|
||||||
|
<span class="bg-primary text-white avatar">
|
||||||
|
<i class="fas fa-briefcase"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<div class="font-weight-medium">
|
||||||
|
포트폴리오 프로젝트
|
||||||
|
</div>
|
||||||
|
<div class="text-muted">
|
||||||
|
<%= stats.portfolioCount || 0 %>개 프로젝트
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-sm-6 col-lg-3">
|
||||||
|
<div class="card card-sm">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row align-items-center">
|
||||||
|
<div class="col-auto">
|
||||||
|
<span class="bg-green text-white avatar">
|
||||||
|
<i class="fas fa-cogs"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<div class="font-weight-medium">
|
||||||
|
제공 서비스
|
||||||
|
</div>
|
||||||
|
<div class="text-muted">
|
||||||
|
<%= stats.servicesCount || 0 %>개 서비스
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-sm-6 col-lg-3">
|
||||||
|
<div class="card card-sm">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row align-items-center">
|
||||||
|
<div class="col-auto">
|
||||||
|
<span class="bg-yellow text-white avatar">
|
||||||
|
<i class="fas fa-envelope"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<div class="font-weight-medium">
|
||||||
|
문의 메시지
|
||||||
|
</div>
|
||||||
|
<div class="text-muted">
|
||||||
|
<%= stats.contactsCount || 0 %>개 메시지
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-sm-6 col-lg-3">
|
||||||
|
<div class="card card-sm">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row align-items-center">
|
||||||
|
<div class="col-auto">
|
||||||
|
<span class="bg-red text-white avatar">
|
||||||
|
<i class="fas fa-users"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<div class="font-weight-medium">
|
||||||
|
관리자 계정
|
||||||
|
</div>
|
||||||
|
<div class="text-muted">
|
||||||
|
<%= stats.usersCount || 0 %>명 사용자
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Recent Portfolio Projects -->
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title">최근 포트폴리오 프로젝트</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<% if (recentPortfolio && recentPortfolio.length > 0) { %>
|
||||||
|
<div class="divide-y">
|
||||||
|
<% recentPortfolio.forEach(function(project, index) { %>
|
||||||
|
<div class="row <%= index < recentPortfolio.length - 1 ? 'py-2' : 'pt-2' %>">
|
||||||
|
<div class="col-auto">
|
||||||
|
<span class="avatar avatar-sm bg-blue-lt">
|
||||||
|
<i class="fas fa-code"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<div class="text-truncate">
|
||||||
|
<strong><%= project.title %></strong>
|
||||||
|
</div>
|
||||||
|
<div class="text-muted">
|
||||||
|
<%= project.category %> •
|
||||||
|
<%= project.createdAt ? project.createdAt.toLocaleDateString('ko-KR') : '날짜 없음' %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<span class="badge bg-<%= project.status === 'completed' ? 'green' : project.status === 'in-progress' ? 'yellow' : 'blue' %>">
|
||||||
|
<%= project.status === 'completed' ? '완료' : project.status === 'in-progress' ? '진행중' : '계획' %>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% }); %>
|
||||||
|
</div>
|
||||||
|
<div class="mt-3">
|
||||||
|
<a href="/admin/portfolio" class="btn btn-primary btn-sm w-100">
|
||||||
|
모든 프로젝트 보기
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<% } else { %>
|
||||||
|
<div class="empty">
|
||||||
|
<div class="empty-img">
|
||||||
|
<i class="fas fa-briefcase fa-3x text-muted"></i>
|
||||||
|
</div>
|
||||||
|
<p class="empty-title">포트폴리오가 없습니다</p>
|
||||||
|
<p class="empty-subtitle text-muted">
|
||||||
|
첫 번째 프로젝트를 추가해보세요
|
||||||
|
</p>
|
||||||
|
<div class="empty-action">
|
||||||
|
<a href="/admin/portfolio/add" class="btn btn-primary">
|
||||||
|
<i class="fas fa-plus"></i>
|
||||||
|
프로젝트 추가
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% } %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Recent Contact Messages -->
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title">최근 문의 메시지</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<% if (recentContacts && recentContacts.length > 0) { %>
|
||||||
|
<div class="divide-y">
|
||||||
|
<% recentContacts.forEach(function(contact, index) { %>
|
||||||
|
<div class="row <%= index < recentContacts.length - 1 ? 'py-2' : 'pt-2' %>">
|
||||||
|
<div class="col-auto">
|
||||||
|
<span class="avatar avatar-sm">
|
||||||
|
<%= contact.name ? contact.name.charAt(0).toUpperCase() : 'U' %>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<div class="text-truncate">
|
||||||
|
<strong><%= contact.name %></strong>
|
||||||
|
</div>
|
||||||
|
<div class="text-muted">
|
||||||
|
<%= contact.email %> •
|
||||||
|
<%= contact.createdAt ? contact.createdAt.toLocaleDateString('ko-KR') : '날짜 없음' %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<span class="badge bg-<%= contact.status === 'replied' ? 'green' : contact.status === 'pending' ? 'yellow' : 'blue' %>">
|
||||||
|
<%= contact.status === 'replied' ? '답변완료' : contact.status === 'pending' ? '대기중' : '신규' %>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% }); %>
|
||||||
|
</div>
|
||||||
|
<div class="mt-3">
|
||||||
|
<a href="/admin/contacts" class="btn btn-warning btn-sm w-100">
|
||||||
|
모든 문의 보기
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<% } else { %>
|
||||||
|
<div class="empty">
|
||||||
|
<div class="empty-img">
|
||||||
|
<i class="fas fa-envelope fa-3x text-muted"></i>
|
||||||
|
</div>
|
||||||
|
<p class="empty-title">새로운 문의가 없습니다</p>
|
||||||
|
<p class="empty-subtitle text-muted">
|
||||||
|
고객 문의가 들어오면 여기에 표시됩니다
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<% } %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Quick Actions -->
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title">빠른 작업</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-3">
|
||||||
|
<a href="/admin/portfolio/add" class="card card-link bg-primary-lt">
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<div class="text-primary mb-3">
|
||||||
|
<i class="fas fa-plus fa-2x"></i>
|
||||||
|
</div>
|
||||||
|
<div class="font-weight-medium">새 프로젝트</div>
|
||||||
|
<div class="text-muted">포트폴리오에 추가</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-3">
|
||||||
|
<a href="/admin/services" class="card card-link bg-green-lt">
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<div class="text-green mb-3">
|
||||||
|
<i class="fas fa-cog fa-2x"></i>
|
||||||
|
</div>
|
||||||
|
<div class="font-weight-medium">서비스 관리</div>
|
||||||
|
<div class="text-muted">가격 및 내용 수정</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-3">
|
||||||
|
<a href="/admin/media" class="card card-link bg-yellow-lt">
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<div class="text-yellow mb-3">
|
||||||
|
<i class="fas fa-images fa-2x"></i>
|
||||||
|
</div>
|
||||||
|
<div class="font-weight-medium">미디어 업로드</div>
|
||||||
|
<div class="text-muted">이미지 및 파일 관리</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-3">
|
||||||
|
<a href="/admin/settings" class="card card-link bg-red-lt">
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<div class="text-red mb-3">
|
||||||
|
<i class="fas fa-wrench fa-2x"></i>
|
||||||
|
</div>
|
||||||
|
<div class="font-weight-medium">사이트 설정</div>
|
||||||
|
<div class="text-muted">전체 설정 관리</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- System Status & Analytics -->
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title">시스템 상태</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-6">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<div class="me-3">
|
||||||
|
<i class="fas fa-server fa-2x text-green"></i>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="h4 mb-0">99.2%</div>
|
||||||
|
<div class="text-muted">서버 업타임</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-6">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<div class="me-3">
|
||||||
|
<i class="fas fa-clock fa-2x text-blue"></i>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="h4 mb-0">2.3초</div>
|
||||||
|
<div class="text-muted">평균 응답시간</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Telegram Bot Status -->
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title">텔레그램 봇</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-6">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<div class="me-3">
|
||||||
|
<i class="fab fa-telegram fa-2x text-green"></i>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="h4 mb-0 text-green">연결됨</div>
|
||||||
|
<div class="text-muted">봇 상태</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-6">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<div class="me-3">
|
||||||
|
<i class="fas fa-paper-plane fa-2x text-blue"></i>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="h4 mb-0">24</div>
|
||||||
|
<div class="text-muted">오늘 전송</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-3">
|
||||||
|
<a href="/admin/telegram" class="btn btn-outline-primary w-100">
|
||||||
|
텔레그램 설정
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
360
.history/views/admin/dashboard_20251026212530.ejs
Normal file
@@ -0,0 +1,360 @@
|
|||||||
|
|
||||||
|
<!-- Dashboard Content -->
|
||||||
|
<div class="row">
|
||||||
|
<!-- Stats Cards -->
|
||||||
|
<div class="col-lg-3 col-6">
|
||||||
|
<div class="info-box">
|
||||||
|
<span class="info-box-icon bg-info elevation-1">
|
||||||
|
<i class="fas fa-briefcase"></i>
|
||||||
|
</span>
|
||||||
|
<div class="info-box-content">
|
||||||
|
<span class="info-box-text">포트폴리오 프로젝트</span>
|
||||||
|
<span class="info-box-number"><%= stats.portfolioCount || 0 %></span>
|
||||||
|
<div class="progress">
|
||||||
|
<div class="progress-bar bg-info" style="width: 70%"></div>
|
||||||
|
</div>
|
||||||
|
<span class="progress-description">
|
||||||
|
70% 완료된 프로젝트
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-lg-3 col-6">
|
||||||
|
<div class="info-box">
|
||||||
|
<span class="info-box-icon bg-success elevation-1">
|
||||||
|
<i class="fas fa-cogs"></i>
|
||||||
|
</span>
|
||||||
|
<div class="info-box-content">
|
||||||
|
<span class="info-box-text">제공 서비스</span>
|
||||||
|
<span class="info-box-number"><%= stats.servicesCount || 0 %></span>
|
||||||
|
<div class="progress">
|
||||||
|
<div class="progress-bar bg-success" style="width: 100%"></div>
|
||||||
|
</div>
|
||||||
|
<span class="progress-description">
|
||||||
|
모든 서비스 활성화
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-lg-3 col-6">
|
||||||
|
<div class="info-box">
|
||||||
|
<span class="info-box-icon bg-warning elevation-1">
|
||||||
|
<i class="fas fa-envelope"></i>
|
||||||
|
</span>
|
||||||
|
<div class="info-box-content">
|
||||||
|
<span class="info-box-text">문의 메시지</span>
|
||||||
|
<span class="info-box-number"><%= stats.contactsCount || 0 %></span>
|
||||||
|
<div class="progress">
|
||||||
|
<div class="progress-bar bg-warning" style="width: 60%"></div>
|
||||||
|
</div>
|
||||||
|
<span class="progress-description">
|
||||||
|
60% 응답 완료
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-lg-3 col-6">
|
||||||
|
<div class="info-box">
|
||||||
|
<span class="info-box-icon bg-danger elevation-1">
|
||||||
|
<i class="fas fa-users"></i>
|
||||||
|
</span>
|
||||||
|
<div class="info-box-content">
|
||||||
|
<span class="info-box-text">관리자 계정</span>
|
||||||
|
<span class="info-box-number"><%= stats.usersCount || 0 %></span>
|
||||||
|
<div class="progress">
|
||||||
|
<div class="progress-bar bg-danger" style="width: 100%"></div>
|
||||||
|
</div>
|
||||||
|
<span class="progress-description">
|
||||||
|
모든 계정 활성화
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Main Content Row -->
|
||||||
|
<div class="row">
|
||||||
|
<!-- Recent Portfolio Projects -->
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title">
|
||||||
|
<i class="fas fa-briefcase mr-1"></i>
|
||||||
|
최근 포트폴리오 프로젝트
|
||||||
|
</h3>
|
||||||
|
<div class="card-tools">
|
||||||
|
<button type="button" class="btn btn-tool" data-card-widget="collapse">
|
||||||
|
<i class="fas fa-minus"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<% if (recentPortfolio && recentPortfolio.length > 0) { %>
|
||||||
|
<ul class="list-unstyled">
|
||||||
|
<% recentPortfolio.forEach(function(project, index) { %>
|
||||||
|
<li class="d-flex align-items-center <%= index < recentPortfolio.length - 1 ? 'border-bottom pb-3 mb-3' : '' %>">
|
||||||
|
<div class="flex-shrink-0">
|
||||||
|
<span class="badge badge-info badge-pill">
|
||||||
|
<i class="fas fa-code"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex-grow-1 ml-3">
|
||||||
|
<h6 class="mb-1 font-weight-bold"><%= project.title %></h6>
|
||||||
|
<p class="text-muted mb-1"><%= project.category %></p>
|
||||||
|
<small class="text-muted">
|
||||||
|
<i class="fas fa-calendar mr-1"></i>
|
||||||
|
<%= project.createdAt ? project.createdAt.toLocaleDateString('ko-KR') : '날짜 없음' %>
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
<div class="flex-shrink-0">
|
||||||
|
<span class="badge badge-<%= project.status === 'completed' ? 'success' : project.status === 'in-progress' ? 'warning' : 'secondary' %>">
|
||||||
|
<%= project.status === 'completed' ? '완료' : project.status === 'in-progress' ? '진행중' : '계획' %>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<% }); %>
|
||||||
|
</ul>
|
||||||
|
<div class="text-center mt-3">
|
||||||
|
<a href="/admin/portfolio" class="btn btn-primary btn-sm">
|
||||||
|
<i class="fas fa-eye mr-1"></i>
|
||||||
|
모든 프로젝트 보기
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<% } else { %>
|
||||||
|
<div class="text-center py-4">
|
||||||
|
<i class="fas fa-briefcase fa-3x text-muted mb-3"></i>
|
||||||
|
<p class="text-muted">아직 포트폴리오 프로젝트가 없습니다.</p>
|
||||||
|
<a href="/admin/portfolio/add" class="btn btn-primary btn-sm">
|
||||||
|
<i class="fas fa-plus mr-1"></i>
|
||||||
|
첫 번째 프로젝트 추가
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<% } %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Recent Contact Messages -->
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title">
|
||||||
|
<i class="fas fa-envelope mr-1"></i>
|
||||||
|
최근 문의 메시지
|
||||||
|
</h3>
|
||||||
|
<div class="card-tools">
|
||||||
|
<button type="button" class="btn btn-tool" data-card-widget="collapse">
|
||||||
|
<i class="fas fa-minus"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<% if (recentContacts && recentContacts.length > 0) { %>
|
||||||
|
<ul class="list-unstyled">
|
||||||
|
<% recentContacts.forEach(function(contact, index) { %>
|
||||||
|
<li class="d-flex align-items-center <%= index < recentContacts.length - 1 ? 'border-bottom pb-3 mb-3' : '' %>">
|
||||||
|
<div class="flex-shrink-0">
|
||||||
|
<div class="bg-primary text-white rounded-circle d-flex align-items-center justify-content-center" style="width: 40px; height: 40px;">
|
||||||
|
<%= contact.name ? contact.name.charAt(0).toUpperCase() : 'U' %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex-grow-1 ml-3">
|
||||||
|
<h6 class="mb-1 font-weight-bold"><%= contact.name %></h6>
|
||||||
|
<p class="text-muted mb-1"><%= contact.email %></p>
|
||||||
|
<small class="text-muted">
|
||||||
|
<i class="fas fa-clock mr-1"></i>
|
||||||
|
<%= contact.createdAt ? contact.createdAt.toLocaleDateString('ko-KR') : '날짜 없음' %>
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
<div class="flex-shrink-0">
|
||||||
|
<span class="badge badge-<%= contact.status === 'replied' ? 'success' : contact.status === 'pending' ? 'warning' : 'secondary' %>">
|
||||||
|
<%= contact.status === 'replied' ? '답변완료' : contact.status === 'pending' ? '대기중' : '신규' %>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<% }); %>
|
||||||
|
</ul>
|
||||||
|
<div class="text-center mt-3">
|
||||||
|
<a href="/admin/contacts" class="btn btn-warning btn-sm">
|
||||||
|
<i class="fas fa-envelope-open mr-1"></i>
|
||||||
|
모든 문의 보기
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<% } else { %>
|
||||||
|
<div class="text-center py-4">
|
||||||
|
<i class="fas fa-envelope fa-3x text-muted mb-3"></i>
|
||||||
|
<p class="text-muted">새로운 문의가 없습니다.</p>
|
||||||
|
<small class="text-muted">고객 문의가 들어오면 여기에 표시됩니다.</small>
|
||||||
|
</div>
|
||||||
|
<% } %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Quick Actions & Tools -->
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title">
|
||||||
|
<i class="fas fa-bolt mr-1"></i>
|
||||||
|
빠른 작업
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-3 col-md-6">
|
||||||
|
<div class="info-box bg-gradient-info">
|
||||||
|
<span class="info-box-icon">
|
||||||
|
<i class="fas fa-plus"></i>
|
||||||
|
</span>
|
||||||
|
<div class="info-box-content">
|
||||||
|
<span class="info-box-text">새 프로젝트</span>
|
||||||
|
<span class="info-box-number">추가하기</span>
|
||||||
|
<div class="progress">
|
||||||
|
<div class="progress-bar" style="width: 100%"></div>
|
||||||
|
</div>
|
||||||
|
<a href="/admin/portfolio/add" class="progress-description text-white">
|
||||||
|
포트폴리오에 새 프로젝트 추가 →
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-lg-3 col-md-6">
|
||||||
|
<div class="info-box bg-gradient-success">
|
||||||
|
<span class="info-box-icon">
|
||||||
|
<i class="fas fa-cog"></i>
|
||||||
|
</span>
|
||||||
|
<div class="info-box-content">
|
||||||
|
<span class="info-box-text">서비스 관리</span>
|
||||||
|
<span class="info-box-number">설정</span>
|
||||||
|
<div class="progress">
|
||||||
|
<div class="progress-bar" style="width: 100%"></div>
|
||||||
|
</div>
|
||||||
|
<a href="/admin/services" class="progress-description text-white">
|
||||||
|
서비스 가격 및 내용 수정 →
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-lg-3 col-md-6">
|
||||||
|
<div class="info-box bg-gradient-warning">
|
||||||
|
<span class="info-box-icon">
|
||||||
|
<i class="fas fa-images"></i>
|
||||||
|
</span>
|
||||||
|
<div class="info-box-content">
|
||||||
|
<span class="info-box-text">미디어</span>
|
||||||
|
<span class="info-box-number">업로드</span>
|
||||||
|
<div class="progress">
|
||||||
|
<div class="progress-bar" style="width: 100%"></div>
|
||||||
|
</div>
|
||||||
|
<a href="/admin/media" class="progress-description text-white">
|
||||||
|
이미지 및 파일 관리 →
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-lg-3 col-md-6">
|
||||||
|
<div class="info-box bg-gradient-danger">
|
||||||
|
<span class="info-box-icon">
|
||||||
|
<i class="fas fa-wrench"></i>
|
||||||
|
</span>
|
||||||
|
<div class="info-box-content">
|
||||||
|
<span class="info-box-text">사이트 설정</span>
|
||||||
|
<span class="info-box-number">관리</span>
|
||||||
|
<div class="progress">
|
||||||
|
<div class="progress-bar" style="width: 100%"></div>
|
||||||
|
</div>
|
||||||
|
<a href="/admin/settings" class="progress-description text-white">
|
||||||
|
전체 사이트 설정 변경 →
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- System Status -->
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card card-primary">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title">
|
||||||
|
<i class="fas fa-server mr-1"></i>
|
||||||
|
시스템 상태
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-6">
|
||||||
|
<div class="description-block border-right">
|
||||||
|
<span class="description-percentage text-success">
|
||||||
|
<i class="fas fa-caret-up"></i> 99.2%
|
||||||
|
</span>
|
||||||
|
<h5 class="description-header">서버 업타임</h5>
|
||||||
|
<span class="description-text">지난 30일</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-6">
|
||||||
|
<div class="description-block">
|
||||||
|
<span class="description-percentage text-info">
|
||||||
|
<i class="fas fa-caret-up"></i> 2.3초
|
||||||
|
</span>
|
||||||
|
<h5 class="description-header">평균 응답시간</h5>
|
||||||
|
<span class="description-text">페이지 로딩</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card card-success">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title">
|
||||||
|
<i class="fab fa-telegram mr-1"></i>
|
||||||
|
텔레그램 봇 상태
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-6">
|
||||||
|
<div class="description-block border-right">
|
||||||
|
<span class="description-percentage text-success">
|
||||||
|
<i class="fas fa-check-circle"></i> 연결됨
|
||||||
|
</span>
|
||||||
|
<h5 class="description-header">봇 상태</h5>
|
||||||
|
<span class="description-text">정상 작동</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-6">
|
||||||
|
<div class="description-block">
|
||||||
|
<span class="description-percentage text-info">
|
||||||
|
<i class="fas fa-paper-plane"></i> 24개
|
||||||
|
</span>
|
||||||
|
<h5 class="description-header">전송된 알림</h5>
|
||||||
|
<span class="description-text">오늘</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-center mt-2">
|
||||||
|
<a href="/admin/telegram" class="btn btn-success btn-sm">
|
||||||
|
<i class="fab fa-telegram mr-1"></i>
|
||||||
|
텔레그램 설정
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
360
.history/views/admin/dashboard_20251026212607.ejs
Normal file
@@ -0,0 +1,360 @@
|
|||||||
|
|
||||||
|
<!-- Dashboard Content -->
|
||||||
|
<div class="row">
|
||||||
|
<!-- Stats Cards -->
|
||||||
|
<div class="col-lg-3 col-6">
|
||||||
|
<div class="info-box">
|
||||||
|
<span class="info-box-icon bg-info elevation-1">
|
||||||
|
<i class="fas fa-briefcase"></i>
|
||||||
|
</span>
|
||||||
|
<div class="info-box-content">
|
||||||
|
<span class="info-box-text">포트폴리오 프로젝트</span>
|
||||||
|
<span class="info-box-number"><%= stats.portfolioCount || 0 %></span>
|
||||||
|
<div class="progress">
|
||||||
|
<div class="progress-bar bg-info" style="width: 70%"></div>
|
||||||
|
</div>
|
||||||
|
<span class="progress-description">
|
||||||
|
70% 완료된 프로젝트
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-lg-3 col-6">
|
||||||
|
<div class="info-box">
|
||||||
|
<span class="info-box-icon bg-success elevation-1">
|
||||||
|
<i class="fas fa-cogs"></i>
|
||||||
|
</span>
|
||||||
|
<div class="info-box-content">
|
||||||
|
<span class="info-box-text">제공 서비스</span>
|
||||||
|
<span class="info-box-number"><%= stats.servicesCount || 0 %></span>
|
||||||
|
<div class="progress">
|
||||||
|
<div class="progress-bar bg-success" style="width: 100%"></div>
|
||||||
|
</div>
|
||||||
|
<span class="progress-description">
|
||||||
|
모든 서비스 활성화
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-lg-3 col-6">
|
||||||
|
<div class="info-box">
|
||||||
|
<span class="info-box-icon bg-warning elevation-1">
|
||||||
|
<i class="fas fa-envelope"></i>
|
||||||
|
</span>
|
||||||
|
<div class="info-box-content">
|
||||||
|
<span class="info-box-text">문의 메시지</span>
|
||||||
|
<span class="info-box-number"><%= stats.contactsCount || 0 %></span>
|
||||||
|
<div class="progress">
|
||||||
|
<div class="progress-bar bg-warning" style="width: 60%"></div>
|
||||||
|
</div>
|
||||||
|
<span class="progress-description">
|
||||||
|
60% 응답 완료
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-lg-3 col-6">
|
||||||
|
<div class="info-box">
|
||||||
|
<span class="info-box-icon bg-danger elevation-1">
|
||||||
|
<i class="fas fa-users"></i>
|
||||||
|
</span>
|
||||||
|
<div class="info-box-content">
|
||||||
|
<span class="info-box-text">관리자 계정</span>
|
||||||
|
<span class="info-box-number"><%= stats.usersCount || 0 %></span>
|
||||||
|
<div class="progress">
|
||||||
|
<div class="progress-bar bg-danger" style="width: 100%"></div>
|
||||||
|
</div>
|
||||||
|
<span class="progress-description">
|
||||||
|
모든 계정 활성화
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Main Content Row -->
|
||||||
|
<div class="row">
|
||||||
|
<!-- Recent Portfolio Projects -->
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title">
|
||||||
|
<i class="fas fa-briefcase mr-1"></i>
|
||||||
|
최근 포트폴리오 프로젝트
|
||||||
|
</h3>
|
||||||
|
<div class="card-tools">
|
||||||
|
<button type="button" class="btn btn-tool" data-card-widget="collapse">
|
||||||
|
<i class="fas fa-minus"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<% if (recentPortfolio && recentPortfolio.length > 0) { %>
|
||||||
|
<ul class="list-unstyled">
|
||||||
|
<% recentPortfolio.forEach(function(project, index) { %>
|
||||||
|
<li class="d-flex align-items-center <%= index < recentPortfolio.length - 1 ? 'border-bottom pb-3 mb-3' : '' %>">
|
||||||
|
<div class="flex-shrink-0">
|
||||||
|
<span class="badge badge-info badge-pill">
|
||||||
|
<i class="fas fa-code"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex-grow-1 ml-3">
|
||||||
|
<h6 class="mb-1 font-weight-bold"><%= project.title %></h6>
|
||||||
|
<p class="text-muted mb-1"><%= project.category %></p>
|
||||||
|
<small class="text-muted">
|
||||||
|
<i class="fas fa-calendar mr-1"></i>
|
||||||
|
<%= project.createdAt ? project.createdAt.toLocaleDateString('ko-KR') : '날짜 없음' %>
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
<div class="flex-shrink-0">
|
||||||
|
<span class="badge badge-<%= project.status === 'completed' ? 'success' : project.status === 'in-progress' ? 'warning' : 'secondary' %>">
|
||||||
|
<%= project.status === 'completed' ? '완료' : project.status === 'in-progress' ? '진행중' : '계획' %>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<% }); %>
|
||||||
|
</ul>
|
||||||
|
<div class="text-center mt-3">
|
||||||
|
<a href="/admin/portfolio" class="btn btn-primary btn-sm">
|
||||||
|
<i class="fas fa-eye mr-1"></i>
|
||||||
|
모든 프로젝트 보기
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<% } else { %>
|
||||||
|
<div class="text-center py-4">
|
||||||
|
<i class="fas fa-briefcase fa-3x text-muted mb-3"></i>
|
||||||
|
<p class="text-muted">아직 포트폴리오 프로젝트가 없습니다.</p>
|
||||||
|
<a href="/admin/portfolio/add" class="btn btn-primary btn-sm">
|
||||||
|
<i class="fas fa-plus mr-1"></i>
|
||||||
|
첫 번째 프로젝트 추가
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<% } %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Recent Contact Messages -->
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title">
|
||||||
|
<i class="fas fa-envelope mr-1"></i>
|
||||||
|
최근 문의 메시지
|
||||||
|
</h3>
|
||||||
|
<div class="card-tools">
|
||||||
|
<button type="button" class="btn btn-tool" data-card-widget="collapse">
|
||||||
|
<i class="fas fa-minus"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<% if (recentContacts && recentContacts.length > 0) { %>
|
||||||
|
<ul class="list-unstyled">
|
||||||
|
<% recentContacts.forEach(function(contact, index) { %>
|
||||||
|
<li class="d-flex align-items-center <%= index < recentContacts.length - 1 ? 'border-bottom pb-3 mb-3' : '' %>">
|
||||||
|
<div class="flex-shrink-0">
|
||||||
|
<div class="bg-primary text-white rounded-circle d-flex align-items-center justify-content-center" style="width: 40px; height: 40px;">
|
||||||
|
<%= contact.name ? contact.name.charAt(0).toUpperCase() : 'U' %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex-grow-1 ml-3">
|
||||||
|
<h6 class="mb-1 font-weight-bold"><%= contact.name %></h6>
|
||||||
|
<p class="text-muted mb-1"><%= contact.email %></p>
|
||||||
|
<small class="text-muted">
|
||||||
|
<i class="fas fa-clock mr-1"></i>
|
||||||
|
<%= contact.createdAt ? contact.createdAt.toLocaleDateString('ko-KR') : '날짜 없음' %>
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
<div class="flex-shrink-0">
|
||||||
|
<span class="badge badge-<%= contact.status === 'replied' ? 'success' : contact.status === 'pending' ? 'warning' : 'secondary' %>">
|
||||||
|
<%= contact.status === 'replied' ? '답변완료' : contact.status === 'pending' ? '대기중' : '신규' %>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<% }); %>
|
||||||
|
</ul>
|
||||||
|
<div class="text-center mt-3">
|
||||||
|
<a href="/admin/contacts" class="btn btn-warning btn-sm">
|
||||||
|
<i class="fas fa-envelope-open mr-1"></i>
|
||||||
|
모든 문의 보기
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<% } else { %>
|
||||||
|
<div class="text-center py-4">
|
||||||
|
<i class="fas fa-envelope fa-3x text-muted mb-3"></i>
|
||||||
|
<p class="text-muted">새로운 문의가 없습니다.</p>
|
||||||
|
<small class="text-muted">고객 문의가 들어오면 여기에 표시됩니다.</small>
|
||||||
|
</div>
|
||||||
|
<% } %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Quick Actions & Tools -->
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title">
|
||||||
|
<i class="fas fa-bolt mr-1"></i>
|
||||||
|
빠른 작업
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-3 col-md-6">
|
||||||
|
<div class="info-box bg-gradient-info">
|
||||||
|
<span class="info-box-icon">
|
||||||
|
<i class="fas fa-plus"></i>
|
||||||
|
</span>
|
||||||
|
<div class="info-box-content">
|
||||||
|
<span class="info-box-text">새 프로젝트</span>
|
||||||
|
<span class="info-box-number">추가하기</span>
|
||||||
|
<div class="progress">
|
||||||
|
<div class="progress-bar" style="width: 100%"></div>
|
||||||
|
</div>
|
||||||
|
<a href="/admin/portfolio/add" class="progress-description text-white">
|
||||||
|
포트폴리오에 새 프로젝트 추가 →
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-lg-3 col-md-6">
|
||||||
|
<div class="info-box bg-gradient-success">
|
||||||
|
<span class="info-box-icon">
|
||||||
|
<i class="fas fa-cog"></i>
|
||||||
|
</span>
|
||||||
|
<div class="info-box-content">
|
||||||
|
<span class="info-box-text">서비스 관리</span>
|
||||||
|
<span class="info-box-number">설정</span>
|
||||||
|
<div class="progress">
|
||||||
|
<div class="progress-bar" style="width: 100%"></div>
|
||||||
|
</div>
|
||||||
|
<a href="/admin/services" class="progress-description text-white">
|
||||||
|
서비스 가격 및 내용 수정 →
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-lg-3 col-md-6">
|
||||||
|
<div class="info-box bg-gradient-warning">
|
||||||
|
<span class="info-box-icon">
|
||||||
|
<i class="fas fa-images"></i>
|
||||||
|
</span>
|
||||||
|
<div class="info-box-content">
|
||||||
|
<span class="info-box-text">미디어</span>
|
||||||
|
<span class="info-box-number">업로드</span>
|
||||||
|
<div class="progress">
|
||||||
|
<div class="progress-bar" style="width: 100%"></div>
|
||||||
|
</div>
|
||||||
|
<a href="/admin/media" class="progress-description text-white">
|
||||||
|
이미지 및 파일 관리 →
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-lg-3 col-md-6">
|
||||||
|
<div class="info-box bg-gradient-danger">
|
||||||
|
<span class="info-box-icon">
|
||||||
|
<i class="fas fa-wrench"></i>
|
||||||
|
</span>
|
||||||
|
<div class="info-box-content">
|
||||||
|
<span class="info-box-text">사이트 설정</span>
|
||||||
|
<span class="info-box-number">관리</span>
|
||||||
|
<div class="progress">
|
||||||
|
<div class="progress-bar" style="width: 100%"></div>
|
||||||
|
</div>
|
||||||
|
<a href="/admin/settings" class="progress-description text-white">
|
||||||
|
전체 사이트 설정 변경 →
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- System Status -->
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card card-primary">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title">
|
||||||
|
<i class="fas fa-server mr-1"></i>
|
||||||
|
시스템 상태
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-6">
|
||||||
|
<div class="description-block border-right">
|
||||||
|
<span class="description-percentage text-success">
|
||||||
|
<i class="fas fa-caret-up"></i> 99.2%
|
||||||
|
</span>
|
||||||
|
<h5 class="description-header">서버 업타임</h5>
|
||||||
|
<span class="description-text">지난 30일</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-6">
|
||||||
|
<div class="description-block">
|
||||||
|
<span class="description-percentage text-info">
|
||||||
|
<i class="fas fa-caret-up"></i> 2.3초
|
||||||
|
</span>
|
||||||
|
<h5 class="description-header">평균 응답시간</h5>
|
||||||
|
<span class="description-text">페이지 로딩</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card card-success">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title">
|
||||||
|
<i class="fab fa-telegram mr-1"></i>
|
||||||
|
텔레그램 봇 상태
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-6">
|
||||||
|
<div class="description-block border-right">
|
||||||
|
<span class="description-percentage text-success">
|
||||||
|
<i class="fas fa-check-circle"></i> 연결됨
|
||||||
|
</span>
|
||||||
|
<h5 class="description-header">봇 상태</h5>
|
||||||
|
<span class="description-text">정상 작동</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-6">
|
||||||
|
<div class="description-block">
|
||||||
|
<span class="description-percentage text-info">
|
||||||
|
<i class="fas fa-paper-plane"></i> 24개
|
||||||
|
</span>
|
||||||
|
<h5 class="description-header">전송된 알림</h5>
|
||||||
|
<span class="description-text">오늘</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-center mt-2">
|
||||||
|
<a href="/admin/telegram" class="btn btn-success btn-sm">
|
||||||
|
<i class="fab fa-telegram mr-1"></i>
|
||||||
|
텔레그램 설정
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
268
.history/views/admin/layout-adminlte_20251026211349.ejs
Normal file
@@ -0,0 +1,268 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="<%= currentLanguage %>">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title><%= title %> - SmartSolTech Admin</title>
|
||||||
|
|
||||||
|
<!-- AdminLTE CSS -->
|
||||||
|
<link rel="stylesheet" href="/node_modules/admin-lte/dist/css/adminlte.min.css">
|
||||||
|
|
||||||
|
<!-- Font Awesome -->
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
|
||||||
|
|
||||||
|
<!-- Google Fonts -->
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@300;400;500;700&display=swap" rel="stylesheet">
|
||||||
|
|
||||||
|
<!-- Custom Korean Admin Styles -->
|
||||||
|
<style>
|
||||||
|
body, .content-wrapper, .main-sidebar {
|
||||||
|
font-family: 'Noto Sans KR', 'Malgun Gothic', 'Apple SD Gothic Neo', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-text {
|
||||||
|
font-weight: 700;
|
||||||
|
color: #007bff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-sidebar .nav-link {
|
||||||
|
border-radius: 8px;
|
||||||
|
margin: 2px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-sidebar .nav-link.active {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-header h1 {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #2c3e50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
border-radius: 8px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-box {
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-box-icon {
|
||||||
|
border-radius: 12px 0 0 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-light {
|
||||||
|
background: white !important;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-sidebar {
|
||||||
|
background: linear-gradient(180deg, #f8f9fa 0%, #e9ecef 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-dark-primary .nav-sidebar > .nav-item > .nav-link:hover {
|
||||||
|
background-color: rgba(255,255,255,0.1);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="hold-transition sidebar-mini layout-fixed">
|
||||||
|
<div class="wrapper">
|
||||||
|
<!-- Navbar -->
|
||||||
|
<nav class="main-header navbar navbar-expand navbar-light">
|
||||||
|
<!-- Left navbar links -->
|
||||||
|
<ul class="navbar-nav">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" data-widget="pushmenu" href="#" role="button">
|
||||||
|
<i class="fas fa-bars"></i>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item d-none d-sm-inline-block">
|
||||||
|
<a href="/" class="nav-link" target="_blank">
|
||||||
|
<i class="fas fa-external-link-alt mr-1"></i>
|
||||||
|
사이트 보기
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<!-- Right navbar links -->
|
||||||
|
<ul class="navbar-nav ml-auto">
|
||||||
|
<!-- User Menu -->
|
||||||
|
<li class="nav-item dropdown">
|
||||||
|
<a class="nav-link" data-toggle="dropdown" href="#">
|
||||||
|
<i class="far fa-user mr-1"></i>
|
||||||
|
<%= user ? user.name : '관리자' %>
|
||||||
|
</a>
|
||||||
|
<div class="dropdown-menu dropdown-menu-lg dropdown-menu-right">
|
||||||
|
<div class="dropdown-divider"></div>
|
||||||
|
<a href="/admin/settings" class="dropdown-item">
|
||||||
|
<i class="fas fa-cog mr-2"></i> 설정
|
||||||
|
</a>
|
||||||
|
<div class="dropdown-divider"></div>
|
||||||
|
<form action="/admin/logout" method="post" class="dropdown-item p-0">
|
||||||
|
<button type="submit" class="btn btn-link text-left w-100 text-danger">
|
||||||
|
<i class="fas fa-sign-out-alt mr-2"></i> 로그아웃
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<!-- Main Sidebar Container -->
|
||||||
|
<aside class="main-sidebar sidebar-light-primary elevation-4">
|
||||||
|
<!-- Brand Logo -->
|
||||||
|
<a href="/admin/dashboard" class="brand-link">
|
||||||
|
<img src="/images/icons/icon-192x192.png" alt="SmartSolTech" class="brand-image img-circle elevation-3" style="opacity: .8">
|
||||||
|
<span class="brand-text font-weight-bold">SmartSolTech</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<!-- Sidebar -->
|
||||||
|
<div class="sidebar">
|
||||||
|
<!-- Sidebar Menu -->
|
||||||
|
<nav class="mt-3">
|
||||||
|
<ul class="nav nav-pills nav-sidebar flex-column" data-widget="treeview" role="menu" data-accordion="false">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/admin/dashboard" class="nav-link <%= currentPage === 'dashboard' ? 'active' : '' %>">
|
||||||
|
<i class="nav-icon fas fa-tachometer-alt"></i>
|
||||||
|
<p>대시보드</p>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/admin/portfolio" class="nav-link <%= currentPage === 'portfolio' ? 'active' : '' %>">
|
||||||
|
<i class="nav-icon fas fa-briefcase"></i>
|
||||||
|
<p>
|
||||||
|
포트폴리오
|
||||||
|
<span class="badge badge-info right"><%= stats?.portfolioCount || 0 %></span>
|
||||||
|
</p>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/admin/services" class="nav-link <%= currentPage === 'services' ? 'active' : '' %>">
|
||||||
|
<i class="nav-icon fas fa-cogs"></i>
|
||||||
|
<p>
|
||||||
|
서비스 관리
|
||||||
|
<span class="badge badge-success right"><%= stats?.servicesCount || 0 %></span>
|
||||||
|
</p>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/admin/contacts" class="nav-link <%= currentPage === 'contacts' ? 'active' : '' %>">
|
||||||
|
<i class="nav-icon fas fa-envelope"></i>
|
||||||
|
<p>
|
||||||
|
문의 관리
|
||||||
|
<span class="badge badge-warning right"><%= stats?.contactsCount || 0 %></span>
|
||||||
|
</p>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/admin/media" class="nav-link <%= currentPage === 'media' ? 'active' : '' %>">
|
||||||
|
<i class="nav-icon fas fa-images"></i>
|
||||||
|
<p>미디어 관리</p>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="nav-header">시스템 설정</li>
|
||||||
|
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/admin/settings" class="nav-link <%= currentPage === 'settings' ? 'active' : '' %>">
|
||||||
|
<i class="nav-icon fas fa-cog"></i>
|
||||||
|
<p>사이트 설정</p>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/admin/telegram" class="nav-link <%= currentPage === 'telegram' ? 'active' : '' %>">
|
||||||
|
<i class="nav-icon fab fa-telegram"></i>
|
||||||
|
<p>텔레그램 봇</p>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/admin/banner-editor" class="nav-link <%= currentPage === 'banner-editor' ? 'active' : '' %>">
|
||||||
|
<i class="nav-icon fas fa-paint-brush"></i>
|
||||||
|
<p>배너 편집기</p>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<!-- Content Wrapper -->
|
||||||
|
<div class="content-wrapper">
|
||||||
|
<!-- Content Header -->
|
||||||
|
<div class="content-header">
|
||||||
|
<div class="container-fluid">
|
||||||
|
<div class="row mb-2">
|
||||||
|
<div class="col-sm-6">
|
||||||
|
<h1 class="m-0"><%= title %></h1>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-6">
|
||||||
|
<ol class="breadcrumb float-sm-right">
|
||||||
|
<li class="breadcrumb-item"><a href="/admin/dashboard">홈</a></li>
|
||||||
|
<% if (currentPage !== 'dashboard') { %>
|
||||||
|
<li class="breadcrumb-item active"><%= title %></li>
|
||||||
|
<% } %>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Main content -->
|
||||||
|
<section class="content">
|
||||||
|
<div class="container-fluid">
|
||||||
|
<%- body %>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<footer class="main-footer">
|
||||||
|
<strong>© 2024 <a href="/">SmartSolTech</a></strong>
|
||||||
|
모든 권리 보유.
|
||||||
|
<div class="float-right d-none d-sm-inline-block">
|
||||||
|
<b>Version</b> 2.0.0
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- AdminLTE JavaScript -->
|
||||||
|
<script src="/node_modules/admin-lte/plugins/jquery/jquery.min.js"></script>
|
||||||
|
<script src="/node_modules/admin-lte/plugins/bootstrap/js/bootstrap.bundle.min.js"></script>
|
||||||
|
<script src="/node_modules/admin-lte/dist/js/adminlte.min.js"></script>
|
||||||
|
|
||||||
|
<!-- Custom JavaScript -->
|
||||||
|
<script src="/js/main.js"></script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Korean localization for AdminLTE
|
||||||
|
$(document).ready(function() {
|
||||||
|
// Update any English text to Korean
|
||||||
|
$('.brand-link .brand-text').text('스마트솔테크');
|
||||||
|
|
||||||
|
// Add smooth transitions
|
||||||
|
$('.nav-link').on('click', function() {
|
||||||
|
if (!$(this).hasClass('active')) {
|
||||||
|
$('.nav-link.active').removeClass('active');
|
||||||
|
$(this).addClass('active');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
268
.history/views/admin/layout-adminlte_20251026211450.ejs
Normal file
@@ -0,0 +1,268 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="<%= currentLanguage %>">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title><%= title %> - SmartSolTech Admin</title>
|
||||||
|
|
||||||
|
<!-- AdminLTE CSS -->
|
||||||
|
<link rel="stylesheet" href="/node_modules/admin-lte/dist/css/adminlte.min.css">
|
||||||
|
|
||||||
|
<!-- Font Awesome -->
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
|
||||||
|
|
||||||
|
<!-- Google Fonts -->
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@300;400;500;700&display=swap" rel="stylesheet">
|
||||||
|
|
||||||
|
<!-- Custom Korean Admin Styles -->
|
||||||
|
<style>
|
||||||
|
body, .content-wrapper, .main-sidebar {
|
||||||
|
font-family: 'Noto Sans KR', 'Malgun Gothic', 'Apple SD Gothic Neo', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-text {
|
||||||
|
font-weight: 700;
|
||||||
|
color: #007bff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-sidebar .nav-link {
|
||||||
|
border-radius: 8px;
|
||||||
|
margin: 2px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-sidebar .nav-link.active {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-header h1 {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #2c3e50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
border-radius: 8px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-box {
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-box-icon {
|
||||||
|
border-radius: 12px 0 0 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-light {
|
||||||
|
background: white !important;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-sidebar {
|
||||||
|
background: linear-gradient(180deg, #f8f9fa 0%, #e9ecef 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-dark-primary .nav-sidebar > .nav-item > .nav-link:hover {
|
||||||
|
background-color: rgba(255,255,255,0.1);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="hold-transition sidebar-mini layout-fixed">
|
||||||
|
<div class="wrapper">
|
||||||
|
<!-- Navbar -->
|
||||||
|
<nav class="main-header navbar navbar-expand navbar-light">
|
||||||
|
<!-- Left navbar links -->
|
||||||
|
<ul class="navbar-nav">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" data-widget="pushmenu" href="#" role="button">
|
||||||
|
<i class="fas fa-bars"></i>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item d-none d-sm-inline-block">
|
||||||
|
<a href="/" class="nav-link" target="_blank">
|
||||||
|
<i class="fas fa-external-link-alt mr-1"></i>
|
||||||
|
사이트 보기
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<!-- Right navbar links -->
|
||||||
|
<ul class="navbar-nav ml-auto">
|
||||||
|
<!-- User Menu -->
|
||||||
|
<li class="nav-item dropdown">
|
||||||
|
<a class="nav-link" data-toggle="dropdown" href="#">
|
||||||
|
<i class="far fa-user mr-1"></i>
|
||||||
|
<%= user ? user.name : '관리자' %>
|
||||||
|
</a>
|
||||||
|
<div class="dropdown-menu dropdown-menu-lg dropdown-menu-right">
|
||||||
|
<div class="dropdown-divider"></div>
|
||||||
|
<a href="/admin/settings" class="dropdown-item">
|
||||||
|
<i class="fas fa-cog mr-2"></i> 설정
|
||||||
|
</a>
|
||||||
|
<div class="dropdown-divider"></div>
|
||||||
|
<form action="/admin/logout" method="post" class="dropdown-item p-0">
|
||||||
|
<button type="submit" class="btn btn-link text-left w-100 text-danger">
|
||||||
|
<i class="fas fa-sign-out-alt mr-2"></i> 로그아웃
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<!-- Main Sidebar Container -->
|
||||||
|
<aside class="main-sidebar sidebar-light-primary elevation-4">
|
||||||
|
<!-- Brand Logo -->
|
||||||
|
<a href="/admin/dashboard" class="brand-link">
|
||||||
|
<img src="/images/icons/icon-192x192.png" alt="SmartSolTech" class="brand-image img-circle elevation-3" style="opacity: .8">
|
||||||
|
<span class="brand-text font-weight-bold">SmartSolTech</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<!-- Sidebar -->
|
||||||
|
<div class="sidebar">
|
||||||
|
<!-- Sidebar Menu -->
|
||||||
|
<nav class="mt-3">
|
||||||
|
<ul class="nav nav-pills nav-sidebar flex-column" data-widget="treeview" role="menu" data-accordion="false">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/admin/dashboard" class="nav-link <%= currentPage === 'dashboard' ? 'active' : '' %>">
|
||||||
|
<i class="nav-icon fas fa-tachometer-alt"></i>
|
||||||
|
<p>대시보드</p>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/admin/portfolio" class="nav-link <%= currentPage === 'portfolio' ? 'active' : '' %>">
|
||||||
|
<i class="nav-icon fas fa-briefcase"></i>
|
||||||
|
<p>
|
||||||
|
포트폴리오
|
||||||
|
<span class="badge badge-info right"><%= stats?.portfolioCount || 0 %></span>
|
||||||
|
</p>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/admin/services" class="nav-link <%= currentPage === 'services' ? 'active' : '' %>">
|
||||||
|
<i class="nav-icon fas fa-cogs"></i>
|
||||||
|
<p>
|
||||||
|
서비스 관리
|
||||||
|
<span class="badge badge-success right"><%= stats?.servicesCount || 0 %></span>
|
||||||
|
</p>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/admin/contacts" class="nav-link <%= currentPage === 'contacts' ? 'active' : '' %>">
|
||||||
|
<i class="nav-icon fas fa-envelope"></i>
|
||||||
|
<p>
|
||||||
|
문의 관리
|
||||||
|
<span class="badge badge-warning right"><%= stats?.contactsCount || 0 %></span>
|
||||||
|
</p>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/admin/media" class="nav-link <%= currentPage === 'media' ? 'active' : '' %>">
|
||||||
|
<i class="nav-icon fas fa-images"></i>
|
||||||
|
<p>미디어 관리</p>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="nav-header">시스템 설정</li>
|
||||||
|
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/admin/settings" class="nav-link <%= currentPage === 'settings' ? 'active' : '' %>">
|
||||||
|
<i class="nav-icon fas fa-cog"></i>
|
||||||
|
<p>사이트 설정</p>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/admin/telegram" class="nav-link <%= currentPage === 'telegram' ? 'active' : '' %>">
|
||||||
|
<i class="nav-icon fab fa-telegram"></i>
|
||||||
|
<p>텔레그램 봇</p>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/admin/banner-editor" class="nav-link <%= currentPage === 'banner-editor' ? 'active' : '' %>">
|
||||||
|
<i class="nav-icon fas fa-paint-brush"></i>
|
||||||
|
<p>배너 편집기</p>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<!-- Content Wrapper -->
|
||||||
|
<div class="content-wrapper">
|
||||||
|
<!-- Content Header -->
|
||||||
|
<div class="content-header">
|
||||||
|
<div class="container-fluid">
|
||||||
|
<div class="row mb-2">
|
||||||
|
<div class="col-sm-6">
|
||||||
|
<h1 class="m-0"><%= title %></h1>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-6">
|
||||||
|
<ol class="breadcrumb float-sm-right">
|
||||||
|
<li class="breadcrumb-item"><a href="/admin/dashboard">홈</a></li>
|
||||||
|
<% if (currentPage !== 'dashboard') { %>
|
||||||
|
<li class="breadcrumb-item active"><%= title %></li>
|
||||||
|
<% } %>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Main content -->
|
||||||
|
<section class="content">
|
||||||
|
<div class="container-fluid">
|
||||||
|
<%- body %>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<footer class="main-footer">
|
||||||
|
<strong>© 2024 <a href="/">SmartSolTech</a></strong>
|
||||||
|
모든 권리 보유.
|
||||||
|
<div class="float-right d-none d-sm-inline-block">
|
||||||
|
<b>Version</b> 2.0.0
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- AdminLTE JavaScript -->
|
||||||
|
<script src="/node_modules/admin-lte/plugins/jquery/jquery.min.js"></script>
|
||||||
|
<script src="/node_modules/admin-lte/plugins/bootstrap/js/bootstrap.bundle.min.js"></script>
|
||||||
|
<script src="/node_modules/admin-lte/dist/js/adminlte.min.js"></script>
|
||||||
|
|
||||||
|
<!-- Custom JavaScript -->
|
||||||
|
<script src="/js/main.js"></script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Korean localization for AdminLTE
|
||||||
|
$(document).ready(function() {
|
||||||
|
// Update any English text to Korean
|
||||||
|
$('.brand-link .brand-text').text('스마트솔테크');
|
||||||
|
|
||||||
|
// Add smooth transitions
|
||||||
|
$('.nav-link').on('click', function() {
|
||||||
|
if (!$(this).hasClass('active')) {
|
||||||
|
$('.nav-link.active').removeClass('active');
|
||||||
|
$(this).addClass('active');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
251
.history/views/admin/layout-tabler_20251026211559.ejs
Normal file
@@ -0,0 +1,251 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="<%= currentLanguage %>">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title><%= title %> - SmartSolTech Admin</title>
|
||||||
|
|
||||||
|
<!-- Tabler CSS -->
|
||||||
|
<link href="/node_modules/@tabler/core/dist/css/tabler.min.css" rel="stylesheet"/>
|
||||||
|
<link href="/node_modules/@tabler/icons/icons-sprite.svg" rel="stylesheet"/>
|
||||||
|
|
||||||
|
<!-- Font Awesome -->
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
|
||||||
|
|
||||||
|
<!-- Google Fonts -->
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@300;400;500;700&display=swap" rel="stylesheet">
|
||||||
|
|
||||||
|
<!-- Custom Korean Admin Styles -->
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: 'Noto Sans KR', 'Malgun Gothic', 'Apple SD Gothic Neo', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-brand {
|
||||||
|
font-weight: 700;
|
||||||
|
color: #206bc4 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link {
|
||||||
|
border-radius: 8px;
|
||||||
|
margin: 2px 0;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link.active {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header h2 {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #2c3e50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
border-radius: 8px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar {
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||||
|
background: white !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-nav .nav-link:hover {
|
||||||
|
background-color: rgba(32, 107, 196, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-vertical .navbar-nav .nav-link {
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-wrapper {
|
||||||
|
background: #f8f9fa;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="page">
|
||||||
|
<!-- Sidebar -->
|
||||||
|
<aside class="navbar navbar-vertical navbar-expand-lg" data-bs-theme="light">
|
||||||
|
<div class="container-fluid">
|
||||||
|
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#sidebar-menu" aria-controls="sidebar-menu" aria-expanded="false" aria-label="Toggle navigation">
|
||||||
|
<span class="navbar-toggler-icon"></span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<h1 class="navbar-brand navbar-brand-autodark">
|
||||||
|
<a href="/admin/dashboard">
|
||||||
|
<img src="/images/icons/icon-192x192.png" width="110" height="32" alt="SmartSolTech" class="navbar-brand-image">
|
||||||
|
SmartSolTech
|
||||||
|
</a>
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<div class="collapse navbar-collapse" id="sidebar-menu">
|
||||||
|
<ul class="navbar-nav pt-lg-3">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link <%= currentPage === 'dashboard' ? 'active' : '' %>" href="/admin/dashboard">
|
||||||
|
<span class="nav-link-icon d-md-none d-lg-inline-block">
|
||||||
|
<i class="fas fa-tachometer-alt"></i>
|
||||||
|
</span>
|
||||||
|
<span class="nav-link-title">대시보드</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="nav-item dropdown">
|
||||||
|
<a class="nav-link dropdown-toggle" href="#navbar-help" data-bs-toggle="dropdown" data-bs-auto-close="false" role="button" aria-expanded="false">
|
||||||
|
<span class="nav-link-icon d-md-none d-lg-inline-block">
|
||||||
|
<i class="fas fa-briefcase"></i>
|
||||||
|
</span>
|
||||||
|
<span class="nav-link-title">포트폴리오</span>
|
||||||
|
</a>
|
||||||
|
<div class="dropdown-menu">
|
||||||
|
<a class="dropdown-item" href="/admin/portfolio">
|
||||||
|
모든 프로젝트
|
||||||
|
</a>
|
||||||
|
<a class="dropdown-item" href="/admin/portfolio/add">
|
||||||
|
새 프로젝트 추가
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link <%= currentPage === 'services' ? 'active' : '' %>" href="/admin/services">
|
||||||
|
<span class="nav-link-icon d-md-none d-lg-inline-block">
|
||||||
|
<i class="fas fa-cogs"></i>
|
||||||
|
</span>
|
||||||
|
<span class="nav-link-title">서비스 관리</span>
|
||||||
|
<span class="badge badge-sm bg-green text-white ms-2"><%= stats?.servicesCount || 0 %></span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link <%= currentPage === 'contacts' ? 'active' : '' %>" href="/admin/contacts">
|
||||||
|
<span class="nav-link-icon d-md-none d-lg-inline-block">
|
||||||
|
<i class="fas fa-envelope"></i>
|
||||||
|
</span>
|
||||||
|
<span class="nav-link-title">문의 관리</span>
|
||||||
|
<span class="badge badge-sm bg-yellow text-white ms-2"><%= stats?.contactsCount || 0 %></span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link <%= currentPage === 'media' ? 'active' : '' %>" href="/admin/media">
|
||||||
|
<span class="nav-link-icon d-md-none d-lg-inline-block">
|
||||||
|
<i class="fas fa-images"></i>
|
||||||
|
</span>
|
||||||
|
<span class="nav-link-title">미디어 관리</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="nav-item dropdown">
|
||||||
|
<a class="nav-link dropdown-toggle" href="#navbar-settings" data-bs-toggle="dropdown" data-bs-auto-close="false" role="button" aria-expanded="false">
|
||||||
|
<span class="nav-link-icon d-md-none d-lg-inline-block">
|
||||||
|
<i class="fas fa-cog"></i>
|
||||||
|
</span>
|
||||||
|
<span class="nav-link-title">시스템</span>
|
||||||
|
</a>
|
||||||
|
<div class="dropdown-menu">
|
||||||
|
<a class="dropdown-item" href="/admin/settings">
|
||||||
|
사이트 설정
|
||||||
|
</a>
|
||||||
|
<a class="dropdown-item" href="/admin/telegram">
|
||||||
|
텔레그램 봇
|
||||||
|
</a>
|
||||||
|
<a class="dropdown-item" href="/admin/banner-editor">
|
||||||
|
배너 편집기
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<!-- Header -->
|
||||||
|
<header class="navbar navbar-expand-md d-print-none">
|
||||||
|
<div class="container-xl">
|
||||||
|
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbar-menu" aria-controls="navbar-menu" aria-expanded="false" aria-label="Toggle navigation">
|
||||||
|
<span class="navbar-toggler-icon"></span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="navbar-nav flex-row order-md-last">
|
||||||
|
<div class="nav-item dropdown">
|
||||||
|
<a href="#" class="nav-link d-flex lh-1 text-reset p-0" data-bs-toggle="dropdown" aria-label="Open user menu">
|
||||||
|
<span class="avatar avatar-sm" style="background-image: url('/images/icons/icon-192x192.png')"></span>
|
||||||
|
<div class="d-none d-xl-block ps-2">
|
||||||
|
<div><%= user ? user.name : '관리자' %></div>
|
||||||
|
<div class="mt-1 small text-muted">관리자</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<div class="dropdown-menu dropdown-menu-end dropdown-menu-arrow">
|
||||||
|
<a href="/admin/settings" class="dropdown-item">설정</a>
|
||||||
|
<a href="/" target="_blank" class="dropdown-item">사이트 보기</a>
|
||||||
|
<div class="dropdown-divider"></div>
|
||||||
|
<form action="/admin/logout" method="post" class="dropdown-item p-0">
|
||||||
|
<button type="submit" class="btn btn-link text-left w-100 text-danger">
|
||||||
|
로그아웃
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="page-wrapper">
|
||||||
|
<!-- Page header -->
|
||||||
|
<div class="page-header d-print-none">
|
||||||
|
<div class="container-xl">
|
||||||
|
<div class="row g-2 align-items-center">
|
||||||
|
<div class="col">
|
||||||
|
<h2 class="page-title">
|
||||||
|
<%= title %>
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Page body -->
|
||||||
|
<div class="page-body">
|
||||||
|
<div class="container-xl">
|
||||||
|
<%- body %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tabler JavaScript -->
|
||||||
|
<script src="/node_modules/@tabler/core/dist/js/tabler.min.js"></script>
|
||||||
|
|
||||||
|
<!-- Custom JavaScript -->
|
||||||
|
<script src="/js/main.js"></script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Korean localization and enhancements
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Add smooth transitions for navigation
|
||||||
|
const navLinks = document.querySelectorAll('.nav-link');
|
||||||
|
navLinks.forEach(link => {
|
||||||
|
link.addEventListener('click', function() {
|
||||||
|
if (!this.classList.contains('active') && !this.classList.contains('dropdown-toggle')) {
|
||||||
|
const currentActive = document.querySelector('.nav-link.active');
|
||||||
|
if (currentActive) {
|
||||||
|
currentActive.classList.remove('active');
|
||||||
|
}
|
||||||
|
this.classList.add('active');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
251
.history/views/admin/layout-tabler_20251026211716.ejs
Normal file
@@ -0,0 +1,251 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="<%= currentLanguage %>">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title><%= title %> - SmartSolTech Admin</title>
|
||||||
|
|
||||||
|
<!-- Tabler CSS -->
|
||||||
|
<link href="/node_modules/@tabler/core/dist/css/tabler.min.css" rel="stylesheet"/>
|
||||||
|
<link href="/node_modules/@tabler/icons/icons-sprite.svg" rel="stylesheet"/>
|
||||||
|
|
||||||
|
<!-- Font Awesome -->
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
|
||||||
|
|
||||||
|
<!-- Google Fonts -->
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@300;400;500;700&display=swap" rel="stylesheet">
|
||||||
|
|
||||||
|
<!-- Custom Korean Admin Styles -->
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: 'Noto Sans KR', 'Malgun Gothic', 'Apple SD Gothic Neo', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-brand {
|
||||||
|
font-weight: 700;
|
||||||
|
color: #206bc4 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link {
|
||||||
|
border-radius: 8px;
|
||||||
|
margin: 2px 0;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link.active {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header h2 {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #2c3e50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
border-radius: 8px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar {
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||||
|
background: white !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-nav .nav-link:hover {
|
||||||
|
background-color: rgba(32, 107, 196, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-vertical .navbar-nav .nav-link {
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-wrapper {
|
||||||
|
background: #f8f9fa;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="page">
|
||||||
|
<!-- Sidebar -->
|
||||||
|
<aside class="navbar navbar-vertical navbar-expand-lg" data-bs-theme="light">
|
||||||
|
<div class="container-fluid">
|
||||||
|
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#sidebar-menu" aria-controls="sidebar-menu" aria-expanded="false" aria-label="Toggle navigation">
|
||||||
|
<span class="navbar-toggler-icon"></span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<h1 class="navbar-brand navbar-brand-autodark">
|
||||||
|
<a href="/admin/dashboard">
|
||||||
|
<img src="/images/icons/icon-192x192.png" width="110" height="32" alt="SmartSolTech" class="navbar-brand-image">
|
||||||
|
SmartSolTech
|
||||||
|
</a>
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<div class="collapse navbar-collapse" id="sidebar-menu">
|
||||||
|
<ul class="navbar-nav pt-lg-3">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link <%= currentPage === 'dashboard' ? 'active' : '' %>" href="/admin/dashboard">
|
||||||
|
<span class="nav-link-icon d-md-none d-lg-inline-block">
|
||||||
|
<i class="fas fa-tachometer-alt"></i>
|
||||||
|
</span>
|
||||||
|
<span class="nav-link-title">대시보드</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="nav-item dropdown">
|
||||||
|
<a class="nav-link dropdown-toggle" href="#navbar-help" data-bs-toggle="dropdown" data-bs-auto-close="false" role="button" aria-expanded="false">
|
||||||
|
<span class="nav-link-icon d-md-none d-lg-inline-block">
|
||||||
|
<i class="fas fa-briefcase"></i>
|
||||||
|
</span>
|
||||||
|
<span class="nav-link-title">포트폴리오</span>
|
||||||
|
</a>
|
||||||
|
<div class="dropdown-menu">
|
||||||
|
<a class="dropdown-item" href="/admin/portfolio">
|
||||||
|
모든 프로젝트
|
||||||
|
</a>
|
||||||
|
<a class="dropdown-item" href="/admin/portfolio/add">
|
||||||
|
새 프로젝트 추가
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link <%= currentPage === 'services' ? 'active' : '' %>" href="/admin/services">
|
||||||
|
<span class="nav-link-icon d-md-none d-lg-inline-block">
|
||||||
|
<i class="fas fa-cogs"></i>
|
||||||
|
</span>
|
||||||
|
<span class="nav-link-title">서비스 관리</span>
|
||||||
|
<span class="badge badge-sm bg-green text-white ms-2"><%= stats?.servicesCount || 0 %></span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link <%= currentPage === 'contacts' ? 'active' : '' %>" href="/admin/contacts">
|
||||||
|
<span class="nav-link-icon d-md-none d-lg-inline-block">
|
||||||
|
<i class="fas fa-envelope"></i>
|
||||||
|
</span>
|
||||||
|
<span class="nav-link-title">문의 관리</span>
|
||||||
|
<span class="badge badge-sm bg-yellow text-white ms-2"><%= stats?.contactsCount || 0 %></span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link <%= currentPage === 'media' ? 'active' : '' %>" href="/admin/media">
|
||||||
|
<span class="nav-link-icon d-md-none d-lg-inline-block">
|
||||||
|
<i class="fas fa-images"></i>
|
||||||
|
</span>
|
||||||
|
<span class="nav-link-title">미디어 관리</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="nav-item dropdown">
|
||||||
|
<a class="nav-link dropdown-toggle" href="#navbar-settings" data-bs-toggle="dropdown" data-bs-auto-close="false" role="button" aria-expanded="false">
|
||||||
|
<span class="nav-link-icon d-md-none d-lg-inline-block">
|
||||||
|
<i class="fas fa-cog"></i>
|
||||||
|
</span>
|
||||||
|
<span class="nav-link-title">시스템</span>
|
||||||
|
</a>
|
||||||
|
<div class="dropdown-menu">
|
||||||
|
<a class="dropdown-item" href="/admin/settings">
|
||||||
|
사이트 설정
|
||||||
|
</a>
|
||||||
|
<a class="dropdown-item" href="/admin/telegram">
|
||||||
|
텔레그램 봇
|
||||||
|
</a>
|
||||||
|
<a class="dropdown-item" href="/admin/banner-editor">
|
||||||
|
배너 편집기
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<!-- Header -->
|
||||||
|
<header class="navbar navbar-expand-md d-print-none">
|
||||||
|
<div class="container-xl">
|
||||||
|
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbar-menu" aria-controls="navbar-menu" aria-expanded="false" aria-label="Toggle navigation">
|
||||||
|
<span class="navbar-toggler-icon"></span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="navbar-nav flex-row order-md-last">
|
||||||
|
<div class="nav-item dropdown">
|
||||||
|
<a href="#" class="nav-link d-flex lh-1 text-reset p-0" data-bs-toggle="dropdown" aria-label="Open user menu">
|
||||||
|
<span class="avatar avatar-sm" style="background-image: url('/images/icons/icon-192x192.png')"></span>
|
||||||
|
<div class="d-none d-xl-block ps-2">
|
||||||
|
<div><%= user ? user.name : '관리자' %></div>
|
||||||
|
<div class="mt-1 small text-muted">관리자</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<div class="dropdown-menu dropdown-menu-end dropdown-menu-arrow">
|
||||||
|
<a href="/admin/settings" class="dropdown-item">설정</a>
|
||||||
|
<a href="/" target="_blank" class="dropdown-item">사이트 보기</a>
|
||||||
|
<div class="dropdown-divider"></div>
|
||||||
|
<form action="/admin/logout" method="post" class="dropdown-item p-0">
|
||||||
|
<button type="submit" class="btn btn-link text-left w-100 text-danger">
|
||||||
|
로그아웃
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="page-wrapper">
|
||||||
|
<!-- Page header -->
|
||||||
|
<div class="page-header d-print-none">
|
||||||
|
<div class="container-xl">
|
||||||
|
<div class="row g-2 align-items-center">
|
||||||
|
<div class="col">
|
||||||
|
<h2 class="page-title">
|
||||||
|
<%= title %>
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Page body -->
|
||||||
|
<div class="page-body">
|
||||||
|
<div class="container-xl">
|
||||||
|
<%- body %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tabler JavaScript -->
|
||||||
|
<script src="/node_modules/@tabler/core/dist/js/tabler.min.js"></script>
|
||||||
|
|
||||||
|
<!-- Custom JavaScript -->
|
||||||
|
<script src="/js/main.js"></script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Korean localization and enhancements
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Add smooth transitions for navigation
|
||||||
|
const navLinks = document.querySelectorAll('.nav-link');
|
||||||
|
navLinks.forEach(link => {
|
||||||
|
link.addEventListener('click', function() {
|
||||||
|
if (!this.classList.contains('active') && !this.classList.contains('dropdown-toggle')) {
|
||||||
|
const currentActive = document.querySelector('.nav-link.active');
|
||||||
|
if (currentActive) {
|
||||||
|
currentActive.classList.remove('active');
|
||||||
|
}
|
||||||
|
this.classList.add('active');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
276
.history/views/admin/layout_20251026212417.ejs
Normal file
@@ -0,0 +1,276 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="<%= currentLanguage %>">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title><%= title %> - SmartSolTech Admin</title>
|
||||||
|
|
||||||
|
<!-- AdminLTE CSS -->
|
||||||
|
<link rel="stylesheet" href="/node_modules/admin-lte/dist/css/adminlte.min.css">
|
||||||
|
|
||||||
|
<!-- Font Awesome -->
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
|
||||||
|
|
||||||
|
<!-- Google Fonts -->
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@300;400;500;700&display=swap" rel="stylesheet">
|
||||||
|
|
||||||
|
<!-- Custom Korean Admin Styles -->
|
||||||
|
<style>
|
||||||
|
body, .content-wrapper, .main-sidebar {
|
||||||
|
font-family: 'Noto Sans KR', 'Malgun Gothic', 'Apple SD Gothic Neo', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-text {
|
||||||
|
font-weight: 700;
|
||||||
|
color: #007bff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-sidebar .nav-link {
|
||||||
|
border-radius: 8px;
|
||||||
|
margin: 2px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-sidebar .nav-link.active {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-header h1 {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #2c3e50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
border-radius: 8px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-box {
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-box-icon {
|
||||||
|
border-radius: 12px 0 0 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-light {
|
||||||
|
background: white !important;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-sidebar {
|
||||||
|
background: linear-gradient(180deg, #f8f9fa 0%, #e9ecef 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-dark-primary .nav-sidebar > .nav-item > .nav-link:hover {
|
||||||
|
background-color: rgba(255,255,255,0.1);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="hold-transition sidebar-mini layout-fixed">
|
||||||
|
<div class="wrapper">
|
||||||
|
<!-- Navbar -->
|
||||||
|
<nav class="main-header navbar navbar-expand navbar-light">
|
||||||
|
<!-- Left navbar links -->
|
||||||
|
<ul class="navbar-nav">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" data-widget="pushmenu" href="#" role="button">
|
||||||
|
<i class="fas fa-bars"></i>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item d-none d-sm-inline-block">
|
||||||
|
<a href="/" class="nav-link" target="_blank">
|
||||||
|
<i class="fas fa-external-link-alt mr-1"></i>
|
||||||
|
사이트 보기
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<!-- Right navbar links -->
|
||||||
|
<ul class="navbar-nav ml-auto">
|
||||||
|
<!-- User Menu -->
|
||||||
|
<li class="nav-item dropdown">
|
||||||
|
<a class="nav-link" data-toggle="dropdown" href="#">
|
||||||
|
<i class="far fa-user mr-1"></i>
|
||||||
|
<%= user ? user.name : '관리자' %>
|
||||||
|
</a>
|
||||||
|
<div class="dropdown-menu dropdown-menu-lg dropdown-menu-right">
|
||||||
|
<div class="dropdown-divider"></div>
|
||||||
|
<a href="/admin/settings" class="dropdown-item">
|
||||||
|
<i class="fas fa-cog mr-2"></i> 설정
|
||||||
|
</a>
|
||||||
|
<div class="dropdown-divider"></div>
|
||||||
|
<form action="/admin/logout" method="post" class="dropdown-item p-0">
|
||||||
|
<button type="submit" class="btn btn-link text-left w-100 text-danger">
|
||||||
|
<i class="fas fa-sign-out-alt mr-2"></i> 로그아웃
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<!-- Main Sidebar Container -->
|
||||||
|
<aside class="main-sidebar sidebar-light-primary elevation-4">
|
||||||
|
<!-- Brand Logo -->
|
||||||
|
<a href="/admin/dashboard" class="brand-link">
|
||||||
|
<img src="/images/icons/icon-192x192.png" alt="SmartSolTech" class="brand-image img-circle elevation-3" style="opacity: .8">
|
||||||
|
<span class="brand-text font-weight-bold">SmartSolTech</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<!-- Sidebar -->
|
||||||
|
<div class="sidebar">
|
||||||
|
<!-- Sidebar Menu -->
|
||||||
|
<nav class="mt-3">
|
||||||
|
<ul class="nav nav-pills nav-sidebar flex-column" data-widget="treeview" role="menu" data-accordion="false">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/admin/dashboard" class="nav-link <%= currentPage === 'dashboard' ? 'active' : '' %>">
|
||||||
|
<i class="nav-icon fas fa-tachometer-alt"></i>
|
||||||
|
<p>대시보드</p>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/admin/portfolio" class="nav-link <%= currentPage === 'portfolio' ? 'active' : '' %>">
|
||||||
|
<i class="nav-icon fas fa-briefcase"></i>
|
||||||
|
<p>
|
||||||
|
포트폴리오
|
||||||
|
<% if (typeof stats !== 'undefined' && stats.portfolioCount) { %>
|
||||||
|
<span class="badge badge-info right"><%= stats.portfolioCount %></span>
|
||||||
|
<% } %>
|
||||||
|
</p>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/admin/services" class="nav-link <%= currentPage === 'services' ? 'active' : '' %>">
|
||||||
|
<i class="nav-icon fas fa-cogs"></i>
|
||||||
|
<p>
|
||||||
|
서비스 관리
|
||||||
|
<% if (typeof stats !== 'undefined' && stats.servicesCount) { %>
|
||||||
|
<span class="badge badge-success right"><%= stats.servicesCount %></span>
|
||||||
|
<% } %>
|
||||||
|
</p>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/admin/contacts" class="nav-link <%= currentPage === 'contacts' ? 'active' : '' %>">
|
||||||
|
<i class="nav-icon fas fa-envelope"></i>
|
||||||
|
<p>
|
||||||
|
문의 관리
|
||||||
|
<% if (typeof stats !== 'undefined' && stats.contactsCount) { %>
|
||||||
|
<span class="badge badge-warning right"><%= stats.contactsCount %></span>
|
||||||
|
<% } %>
|
||||||
|
</p>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/admin/media" class="nav-link <%= currentPage === 'media' ? 'active' : '' %>">
|
||||||
|
<i class="nav-icon fas fa-images"></i>
|
||||||
|
<p>미디어 관리</p>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="nav-header">시스템 설정</li>
|
||||||
|
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/admin/settings" class="nav-link <%= currentPage === 'settings' ? 'active' : '' %>">
|
||||||
|
<i class="nav-icon fas fa-cog"></i>
|
||||||
|
<p>사이트 설정</p>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/admin/telegram" class="nav-link <%= currentPage === 'telegram' ? 'active' : '' %>">
|
||||||
|
<i class="nav-icon fab fa-telegram"></i>
|
||||||
|
<p>텔레그램 봇</p>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/admin/banner-editor" class="nav-link <%= currentPage === 'banner-editor' ? 'active' : '' %>">
|
||||||
|
<i class="nav-icon fas fa-paint-brush"></i>
|
||||||
|
<p>배너 편집기</p>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<!-- Content Wrapper -->
|
||||||
|
<div class="content-wrapper">
|
||||||
|
<!-- Content Header -->
|
||||||
|
<div class="content-header">
|
||||||
|
<div class="container-fluid">
|
||||||
|
<div class="row mb-2">
|
||||||
|
<div class="col-sm-6">
|
||||||
|
<h1 class="m-0"><%= title %></h1>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-6">
|
||||||
|
<ol class="breadcrumb float-sm-right">
|
||||||
|
<li class="breadcrumb-item"><a href="/admin/dashboard">홈</a></li>
|
||||||
|
<% if (currentPage !== 'dashboard') { %>
|
||||||
|
<li class="breadcrumb-item active"><%= title %></li>
|
||||||
|
<% } %>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Main content -->
|
||||||
|
<section class="content">
|
||||||
|
<div class="container-fluid">
|
||||||
|
<%- body %>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<footer class="main-footer">
|
||||||
|
<strong>© 2024 <a href="/">SmartSolTech</a></strong>
|
||||||
|
모든 권리 보유.
|
||||||
|
<div class="float-right d-none d-sm-inline-block">
|
||||||
|
<b>Version</b> 2.0.0
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- jQuery -->
|
||||||
|
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||||
|
<!-- Bootstrap 4 -->
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
<!-- AdminLTE App -->
|
||||||
|
<script src="/node_modules/admin-lte/dist/js/adminlte.min.js"></script>
|
||||||
|
|
||||||
|
<!-- Custom JavaScript -->
|
||||||
|
<script src="/js/main.js"></script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Korean localization for AdminLTE
|
||||||
|
$(document).ready(function() {
|
||||||
|
// Update any English text to Korean
|
||||||
|
$('.brand-link .brand-text').text('스마트솔테크');
|
||||||
|
|
||||||
|
// Add smooth transitions
|
||||||
|
$('.nav-link').on('click', function() {
|
||||||
|
if (!$(this).hasClass('active')) {
|
||||||
|
$('.nav-link.active').removeClass('active');
|
||||||
|
$(this).addClass('active');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
276
.history/views/admin/layout_20251026212427.ejs
Normal file
@@ -0,0 +1,276 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="<%= currentLanguage %>">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title><%= title %> - SmartSolTech Admin</title>
|
||||||
|
|
||||||
|
<!-- AdminLTE CSS -->
|
||||||
|
<link rel="stylesheet" href="/node_modules/admin-lte/dist/css/adminlte.min.css">
|
||||||
|
|
||||||
|
<!-- Font Awesome -->
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
|
||||||
|
|
||||||
|
<!-- Google Fonts -->
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@300;400;500;700&display=swap" rel="stylesheet">
|
||||||
|
|
||||||
|
<!-- Custom Korean Admin Styles -->
|
||||||
|
<style>
|
||||||
|
body, .content-wrapper, .main-sidebar {
|
||||||
|
font-family: 'Noto Sans KR', 'Malgun Gothic', 'Apple SD Gothic Neo', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-text {
|
||||||
|
font-weight: 700;
|
||||||
|
color: #007bff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-sidebar .nav-link {
|
||||||
|
border-radius: 8px;
|
||||||
|
margin: 2px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-sidebar .nav-link.active {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-header h1 {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #2c3e50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
border-radius: 8px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-box {
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-box-icon {
|
||||||
|
border-radius: 12px 0 0 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-light {
|
||||||
|
background: white !important;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-sidebar {
|
||||||
|
background: linear-gradient(180deg, #f8f9fa 0%, #e9ecef 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-dark-primary .nav-sidebar > .nav-item > .nav-link:hover {
|
||||||
|
background-color: rgba(255,255,255,0.1);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="hold-transition sidebar-mini layout-fixed">
|
||||||
|
<div class="wrapper">
|
||||||
|
<!-- Navbar -->
|
||||||
|
<nav class="main-header navbar navbar-expand navbar-light">
|
||||||
|
<!-- Left navbar links -->
|
||||||
|
<ul class="navbar-nav">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" data-widget="pushmenu" href="#" role="button">
|
||||||
|
<i class="fas fa-bars"></i>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item d-none d-sm-inline-block">
|
||||||
|
<a href="/" class="nav-link" target="_blank">
|
||||||
|
<i class="fas fa-external-link-alt mr-1"></i>
|
||||||
|
사이트 보기
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<!-- Right navbar links -->
|
||||||
|
<ul class="navbar-nav ml-auto">
|
||||||
|
<!-- User Menu -->
|
||||||
|
<li class="nav-item dropdown">
|
||||||
|
<a class="nav-link" data-toggle="dropdown" href="#">
|
||||||
|
<i class="far fa-user mr-1"></i>
|
||||||
|
<%= user ? user.name : '관리자' %>
|
||||||
|
</a>
|
||||||
|
<div class="dropdown-menu dropdown-menu-lg dropdown-menu-right">
|
||||||
|
<div class="dropdown-divider"></div>
|
||||||
|
<a href="/admin/settings" class="dropdown-item">
|
||||||
|
<i class="fas fa-cog mr-2"></i> 설정
|
||||||
|
</a>
|
||||||
|
<div class="dropdown-divider"></div>
|
||||||
|
<form action="/admin/logout" method="post" class="dropdown-item p-0">
|
||||||
|
<button type="submit" class="btn btn-link text-left w-100 text-danger">
|
||||||
|
<i class="fas fa-sign-out-alt mr-2"></i> 로그아웃
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<!-- Main Sidebar Container -->
|
||||||
|
<aside class="main-sidebar sidebar-light-primary elevation-4">
|
||||||
|
<!-- Brand Logo -->
|
||||||
|
<a href="/admin/dashboard" class="brand-link">
|
||||||
|
<img src="/images/icons/icon-192x192.png" alt="SmartSolTech" class="brand-image img-circle elevation-3" style="opacity: .8">
|
||||||
|
<span class="brand-text font-weight-bold">SmartSolTech</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<!-- Sidebar -->
|
||||||
|
<div class="sidebar">
|
||||||
|
<!-- Sidebar Menu -->
|
||||||
|
<nav class="mt-3">
|
||||||
|
<ul class="nav nav-pills nav-sidebar flex-column" data-widget="treeview" role="menu" data-accordion="false">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/admin/dashboard" class="nav-link <%= currentPage === 'dashboard' ? 'active' : '' %>">
|
||||||
|
<i class="nav-icon fas fa-tachometer-alt"></i>
|
||||||
|
<p>대시보드</p>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/admin/portfolio" class="nav-link <%= currentPage === 'portfolio' ? 'active' : '' %>">
|
||||||
|
<i class="nav-icon fas fa-briefcase"></i>
|
||||||
|
<p>
|
||||||
|
포트폴리오
|
||||||
|
<% if (typeof stats !== 'undefined' && stats.portfolioCount) { %>
|
||||||
|
<span class="badge badge-info right"><%= stats.portfolioCount %></span>
|
||||||
|
<% } %>
|
||||||
|
</p>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/admin/services" class="nav-link <%= currentPage === 'services' ? 'active' : '' %>">
|
||||||
|
<i class="nav-icon fas fa-cogs"></i>
|
||||||
|
<p>
|
||||||
|
서비스 관리
|
||||||
|
<% if (typeof stats !== 'undefined' && stats.servicesCount) { %>
|
||||||
|
<span class="badge badge-success right"><%= stats.servicesCount %></span>
|
||||||
|
<% } %>
|
||||||
|
</p>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/admin/contacts" class="nav-link <%= currentPage === 'contacts' ? 'active' : '' %>">
|
||||||
|
<i class="nav-icon fas fa-envelope"></i>
|
||||||
|
<p>
|
||||||
|
문의 관리
|
||||||
|
<% if (typeof stats !== 'undefined' && stats.contactsCount) { %>
|
||||||
|
<span class="badge badge-warning right"><%= stats.contactsCount %></span>
|
||||||
|
<% } %>
|
||||||
|
</p>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/admin/media" class="nav-link <%= currentPage === 'media' ? 'active' : '' %>">
|
||||||
|
<i class="nav-icon fas fa-images"></i>
|
||||||
|
<p>미디어 관리</p>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="nav-header">시스템 설정</li>
|
||||||
|
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/admin/settings" class="nav-link <%= currentPage === 'settings' ? 'active' : '' %>">
|
||||||
|
<i class="nav-icon fas fa-cog"></i>
|
||||||
|
<p>사이트 설정</p>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/admin/telegram" class="nav-link <%= currentPage === 'telegram' ? 'active' : '' %>">
|
||||||
|
<i class="nav-icon fab fa-telegram"></i>
|
||||||
|
<p>텔레그램 봇</p>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/admin/banner-editor" class="nav-link <%= currentPage === 'banner-editor' ? 'active' : '' %>">
|
||||||
|
<i class="nav-icon fas fa-paint-brush"></i>
|
||||||
|
<p>배너 편집기</p>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<!-- Content Wrapper -->
|
||||||
|
<div class="content-wrapper">
|
||||||
|
<!-- Content Header -->
|
||||||
|
<div class="content-header">
|
||||||
|
<div class="container-fluid">
|
||||||
|
<div class="row mb-2">
|
||||||
|
<div class="col-sm-6">
|
||||||
|
<h1 class="m-0"><%= title %></h1>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-6">
|
||||||
|
<ol class="breadcrumb float-sm-right">
|
||||||
|
<li class="breadcrumb-item"><a href="/admin/dashboard">홈</a></li>
|
||||||
|
<% if (currentPage !== 'dashboard') { %>
|
||||||
|
<li class="breadcrumb-item active"><%= title %></li>
|
||||||
|
<% } %>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Main content -->
|
||||||
|
<section class="content">
|
||||||
|
<div class="container-fluid">
|
||||||
|
<%- body %>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<footer class="main-footer">
|
||||||
|
<strong>© 2024 <a href="/">SmartSolTech</a></strong>
|
||||||
|
모든 권리 보유.
|
||||||
|
<div class="float-right d-none d-sm-inline-block">
|
||||||
|
<b>Version</b> 2.0.0
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- jQuery -->
|
||||||
|
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||||
|
<!-- Bootstrap 4 -->
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
<!-- AdminLTE App -->
|
||||||
|
<script src="/node_modules/admin-lte/dist/js/adminlte.min.js"></script>
|
||||||
|
|
||||||
|
<!-- Custom JavaScript -->
|
||||||
|
<script src="/js/main.js"></script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Korean localization for AdminLTE
|
||||||
|
$(document).ready(function() {
|
||||||
|
// Update any English text to Korean
|
||||||
|
$('.brand-link .brand-text').text('스마트솔테크');
|
||||||
|
|
||||||
|
// Add smooth transitions
|
||||||
|
$('.nav-link').on('click', function() {
|
||||||
|
if (!$(this).hasClass('active')) {
|
||||||
|
$('.nav-link.active').removeClass('active');
|
||||||
|
$(this).addClass('active');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
279
.history/views/admin/layout_20251026215820.ejs
Normal file
@@ -0,0 +1,279 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="<%= currentLanguage %>">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title><%= title %> - SmartSolTech Admin</title>
|
||||||
|
|
||||||
|
<!-- Bootstrap CSS (Local) -->
|
||||||
|
<link rel="stylesheet" href="/vendor/bootstrap/bootstrap.min.css">
|
||||||
|
|
||||||
|
<!-- AdminLTE CSS (Local) -->
|
||||||
|
<link rel="stylesheet" href="/vendor/adminlte/adminlte.min.css">
|
||||||
|
|
||||||
|
<!-- Font Awesome (Local) -->
|
||||||
|
<link rel="stylesheet" href="/vendor/adminlte/fontawesome.min.css">
|
||||||
|
|
||||||
|
<!-- Google Fonts -->
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@300;400;500;700&display=swap" rel="stylesheet">
|
||||||
|
|
||||||
|
<!-- Custom Korean Admin Styles -->
|
||||||
|
<style>
|
||||||
|
body, .content-wrapper, .main-sidebar {
|
||||||
|
font-family: 'Noto Sans KR', 'Malgun Gothic', 'Apple SD Gothic Neo', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-text {
|
||||||
|
font-weight: 700;
|
||||||
|
color: #007bff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-sidebar .nav-link {
|
||||||
|
border-radius: 8px;
|
||||||
|
margin: 2px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-sidebar .nav-link.active {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-header h1 {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #2c3e50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
border-radius: 8px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-box {
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-box-icon {
|
||||||
|
border-radius: 12px 0 0 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-light {
|
||||||
|
background: white !important;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-sidebar {
|
||||||
|
background: linear-gradient(180deg, #f8f9fa 0%, #e9ecef 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-dark-primary .nav-sidebar > .nav-item > .nav-link:hover {
|
||||||
|
background-color: rgba(255,255,255,0.1);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="hold-transition sidebar-mini layout-fixed">
|
||||||
|
<div class="wrapper">
|
||||||
|
<!-- Navbar -->
|
||||||
|
<nav class="main-header navbar navbar-expand navbar-light">
|
||||||
|
<!-- Left navbar links -->
|
||||||
|
<ul class="navbar-nav">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" data-widget="pushmenu" href="#" role="button">
|
||||||
|
<i class="fas fa-bars"></i>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item d-none d-sm-inline-block">
|
||||||
|
<a href="/" class="nav-link" target="_blank">
|
||||||
|
<i class="fas fa-external-link-alt mr-1"></i>
|
||||||
|
사이트 보기
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<!-- Right navbar links -->
|
||||||
|
<ul class="navbar-nav ml-auto">
|
||||||
|
<!-- User Menu -->
|
||||||
|
<li class="nav-item dropdown">
|
||||||
|
<a class="nav-link" data-toggle="dropdown" href="#">
|
||||||
|
<i class="far fa-user mr-1"></i>
|
||||||
|
<%= user ? user.name : '관리자' %>
|
||||||
|
</a>
|
||||||
|
<div class="dropdown-menu dropdown-menu-lg dropdown-menu-right">
|
||||||
|
<div class="dropdown-divider"></div>
|
||||||
|
<a href="/admin/settings" class="dropdown-item">
|
||||||
|
<i class="fas fa-cog mr-2"></i> 설정
|
||||||
|
</a>
|
||||||
|
<div class="dropdown-divider"></div>
|
||||||
|
<form action="/admin/logout" method="post" class="dropdown-item p-0">
|
||||||
|
<button type="submit" class="btn btn-link text-left w-100 text-danger">
|
||||||
|
<i class="fas fa-sign-out-alt mr-2"></i> 로그아웃
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<!-- Main Sidebar Container -->
|
||||||
|
<aside class="main-sidebar sidebar-light-primary elevation-4">
|
||||||
|
<!-- Brand Logo -->
|
||||||
|
<a href="/admin/dashboard" class="brand-link">
|
||||||
|
<img src="/images/icons/icon-192x192.png" alt="SmartSolTech" class="brand-image img-circle elevation-3" style="opacity: .8">
|
||||||
|
<span class="brand-text font-weight-bold">SmartSolTech</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<!-- Sidebar -->
|
||||||
|
<div class="sidebar">
|
||||||
|
<!-- Sidebar Menu -->
|
||||||
|
<nav class="mt-3">
|
||||||
|
<ul class="nav nav-pills nav-sidebar flex-column" data-widget="treeview" role="menu" data-accordion="false">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/admin/dashboard" class="nav-link <%= currentPage === 'dashboard' ? 'active' : '' %>">
|
||||||
|
<i class="nav-icon fas fa-tachometer-alt"></i>
|
||||||
|
<p>대시보드</p>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/admin/portfolio" class="nav-link <%= currentPage === 'portfolio' ? 'active' : '' %>">
|
||||||
|
<i class="nav-icon fas fa-briefcase"></i>
|
||||||
|
<p>
|
||||||
|
포트폴리오
|
||||||
|
<% if (typeof stats !== 'undefined' && stats.portfolioCount) { %>
|
||||||
|
<span class="badge badge-info right"><%= stats.portfolioCount %></span>
|
||||||
|
<% } %>
|
||||||
|
</p>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/admin/services" class="nav-link <%= currentPage === 'services' ? 'active' : '' %>">
|
||||||
|
<i class="nav-icon fas fa-cogs"></i>
|
||||||
|
<p>
|
||||||
|
서비스 관리
|
||||||
|
<% if (typeof stats !== 'undefined' && stats.servicesCount) { %>
|
||||||
|
<span class="badge badge-success right"><%= stats.servicesCount %></span>
|
||||||
|
<% } %>
|
||||||
|
</p>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/admin/contacts" class="nav-link <%= currentPage === 'contacts' ? 'active' : '' %>">
|
||||||
|
<i class="nav-icon fas fa-envelope"></i>
|
||||||
|
<p>
|
||||||
|
문의 관리
|
||||||
|
<% if (typeof stats !== 'undefined' && stats.contactsCount) { %>
|
||||||
|
<span class="badge badge-warning right"><%= stats.contactsCount %></span>
|
||||||
|
<% } %>
|
||||||
|
</p>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/admin/media" class="nav-link <%= currentPage === 'media' ? 'active' : '' %>">
|
||||||
|
<i class="nav-icon fas fa-images"></i>
|
||||||
|
<p>미디어 관리</p>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="nav-header">시스템 설정</li>
|
||||||
|
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/admin/settings" class="nav-link <%= currentPage === 'settings' ? 'active' : '' %>">
|
||||||
|
<i class="nav-icon fas fa-cog"></i>
|
||||||
|
<p>사이트 설정</p>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/admin/telegram" class="nav-link <%= currentPage === 'telegram' ? 'active' : '' %>">
|
||||||
|
<i class="nav-icon fab fa-telegram"></i>
|
||||||
|
<p>텔레그램 봇</p>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/admin/banner-editor" class="nav-link <%= currentPage === 'banner-editor' ? 'active' : '' %>">
|
||||||
|
<i class="nav-icon fas fa-paint-brush"></i>
|
||||||
|
<p>배너 편집기</p>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<!-- Content Wrapper -->
|
||||||
|
<div class="content-wrapper">
|
||||||
|
<!-- Content Header -->
|
||||||
|
<div class="content-header">
|
||||||
|
<div class="container-fluid">
|
||||||
|
<div class="row mb-2">
|
||||||
|
<div class="col-sm-6">
|
||||||
|
<h1 class="m-0"><%= title %></h1>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-6">
|
||||||
|
<ol class="breadcrumb float-sm-right">
|
||||||
|
<li class="breadcrumb-item"><a href="/admin/dashboard">홈</a></li>
|
||||||
|
<% if (currentPage !== 'dashboard') { %>
|
||||||
|
<li class="breadcrumb-item active"><%= title %></li>
|
||||||
|
<% } %>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Main content -->
|
||||||
|
<section class="content">
|
||||||
|
<div class="container-fluid">
|
||||||
|
<%- body %>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<footer class="main-footer">
|
||||||
|
<strong>© 2024 <a href="/">SmartSolTech</a></strong>
|
||||||
|
모든 권리 보유.
|
||||||
|
<div class="float-right d-none d-sm-inline-block">
|
||||||
|
<b>Version</b> 2.0.0
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- jQuery -->
|
||||||
|
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||||
|
<!-- Bootstrap 4 -->
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
<!-- AdminLTE App -->
|
||||||
|
<script src="/node_modules/admin-lte/dist/js/adminlte.min.js"></script>
|
||||||
|
|
||||||
|
<!-- Custom JavaScript -->
|
||||||
|
<script src="/js/main.js"></script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Korean localization for AdminLTE
|
||||||
|
$(document).ready(function() {
|
||||||
|
// Update any English text to Korean
|
||||||
|
$('.brand-link .brand-text').text('스마트솔테크');
|
||||||
|
|
||||||
|
// Add smooth transitions
|
||||||
|
$('.nav-link').on('click', function() {
|
||||||
|
if (!$(this).hasClass('active')) {
|
||||||
|
$('.nav-link.active').removeClass('active');
|
||||||
|
$(this).addClass('active');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
279
.history/views/admin/layout_20251026215830.ejs
Normal file
@@ -0,0 +1,279 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="<%= currentLanguage %>">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title><%= title %> - SmartSolTech Admin</title>
|
||||||
|
|
||||||
|
<!-- Bootstrap CSS (Local) -->
|
||||||
|
<link rel="stylesheet" href="/vendor/bootstrap/bootstrap.min.css">
|
||||||
|
|
||||||
|
<!-- AdminLTE CSS (Local) -->
|
||||||
|
<link rel="stylesheet" href="/vendor/adminlte/adminlte.min.css">
|
||||||
|
|
||||||
|
<!-- Font Awesome (Local) -->
|
||||||
|
<link rel="stylesheet" href="/vendor/adminlte/fontawesome.min.css">
|
||||||
|
|
||||||
|
<!-- Google Fonts -->
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@300;400;500;700&display=swap" rel="stylesheet">
|
||||||
|
|
||||||
|
<!-- Custom Korean Admin Styles -->
|
||||||
|
<style>
|
||||||
|
body, .content-wrapper, .main-sidebar {
|
||||||
|
font-family: 'Noto Sans KR', 'Malgun Gothic', 'Apple SD Gothic Neo', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-text {
|
||||||
|
font-weight: 700;
|
||||||
|
color: #007bff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-sidebar .nav-link {
|
||||||
|
border-radius: 8px;
|
||||||
|
margin: 2px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-sidebar .nav-link.active {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-header h1 {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #2c3e50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
border-radius: 8px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-box {
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-box-icon {
|
||||||
|
border-radius: 12px 0 0 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-light {
|
||||||
|
background: white !important;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-sidebar {
|
||||||
|
background: linear-gradient(180deg, #f8f9fa 0%, #e9ecef 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-dark-primary .nav-sidebar > .nav-item > .nav-link:hover {
|
||||||
|
background-color: rgba(255,255,255,0.1);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="hold-transition sidebar-mini layout-fixed">
|
||||||
|
<div class="wrapper">
|
||||||
|
<!-- Navbar -->
|
||||||
|
<nav class="main-header navbar navbar-expand navbar-light">
|
||||||
|
<!-- Left navbar links -->
|
||||||
|
<ul class="navbar-nav">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" data-widget="pushmenu" href="#" role="button">
|
||||||
|
<i class="fas fa-bars"></i>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item d-none d-sm-inline-block">
|
||||||
|
<a href="/" class="nav-link" target="_blank">
|
||||||
|
<i class="fas fa-external-link-alt mr-1"></i>
|
||||||
|
사이트 보기
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<!-- Right navbar links -->
|
||||||
|
<ul class="navbar-nav ml-auto">
|
||||||
|
<!-- User Menu -->
|
||||||
|
<li class="nav-item dropdown">
|
||||||
|
<a class="nav-link" data-toggle="dropdown" href="#">
|
||||||
|
<i class="far fa-user mr-1"></i>
|
||||||
|
<%= user ? user.name : '관리자' %>
|
||||||
|
</a>
|
||||||
|
<div class="dropdown-menu dropdown-menu-lg dropdown-menu-right">
|
||||||
|
<div class="dropdown-divider"></div>
|
||||||
|
<a href="/admin/settings" class="dropdown-item">
|
||||||
|
<i class="fas fa-cog mr-2"></i> 설정
|
||||||
|
</a>
|
||||||
|
<div class="dropdown-divider"></div>
|
||||||
|
<form action="/admin/logout" method="post" class="dropdown-item p-0">
|
||||||
|
<button type="submit" class="btn btn-link text-left w-100 text-danger">
|
||||||
|
<i class="fas fa-sign-out-alt mr-2"></i> 로그아웃
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<!-- Main Sidebar Container -->
|
||||||
|
<aside class="main-sidebar sidebar-light-primary elevation-4">
|
||||||
|
<!-- Brand Logo -->
|
||||||
|
<a href="/admin/dashboard" class="brand-link">
|
||||||
|
<img src="/images/icons/icon-192x192.png" alt="SmartSolTech" class="brand-image img-circle elevation-3" style="opacity: .8">
|
||||||
|
<span class="brand-text font-weight-bold">SmartSolTech</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<!-- Sidebar -->
|
||||||
|
<div class="sidebar">
|
||||||
|
<!-- Sidebar Menu -->
|
||||||
|
<nav class="mt-3">
|
||||||
|
<ul class="nav nav-pills nav-sidebar flex-column" data-widget="treeview" role="menu" data-accordion="false">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/admin/dashboard" class="nav-link <%= currentPage === 'dashboard' ? 'active' : '' %>">
|
||||||
|
<i class="nav-icon fas fa-tachometer-alt"></i>
|
||||||
|
<p>대시보드</p>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/admin/portfolio" class="nav-link <%= currentPage === 'portfolio' ? 'active' : '' %>">
|
||||||
|
<i class="nav-icon fas fa-briefcase"></i>
|
||||||
|
<p>
|
||||||
|
포트폴리오
|
||||||
|
<% if (typeof stats !== 'undefined' && stats.portfolioCount) { %>
|
||||||
|
<span class="badge badge-info right"><%= stats.portfolioCount %></span>
|
||||||
|
<% } %>
|
||||||
|
</p>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/admin/services" class="nav-link <%= currentPage === 'services' ? 'active' : '' %>">
|
||||||
|
<i class="nav-icon fas fa-cogs"></i>
|
||||||
|
<p>
|
||||||
|
서비스 관리
|
||||||
|
<% if (typeof stats !== 'undefined' && stats.servicesCount) { %>
|
||||||
|
<span class="badge badge-success right"><%= stats.servicesCount %></span>
|
||||||
|
<% } %>
|
||||||
|
</p>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/admin/contacts" class="nav-link <%= currentPage === 'contacts' ? 'active' : '' %>">
|
||||||
|
<i class="nav-icon fas fa-envelope"></i>
|
||||||
|
<p>
|
||||||
|
문의 관리
|
||||||
|
<% if (typeof stats !== 'undefined' && stats.contactsCount) { %>
|
||||||
|
<span class="badge badge-warning right"><%= stats.contactsCount %></span>
|
||||||
|
<% } %>
|
||||||
|
</p>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/admin/media" class="nav-link <%= currentPage === 'media' ? 'active' : '' %>">
|
||||||
|
<i class="nav-icon fas fa-images"></i>
|
||||||
|
<p>미디어 관리</p>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="nav-header">시스템 설정</li>
|
||||||
|
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/admin/settings" class="nav-link <%= currentPage === 'settings' ? 'active' : '' %>">
|
||||||
|
<i class="nav-icon fas fa-cog"></i>
|
||||||
|
<p>사이트 설정</p>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/admin/telegram" class="nav-link <%= currentPage === 'telegram' ? 'active' : '' %>">
|
||||||
|
<i class="nav-icon fab fa-telegram"></i>
|
||||||
|
<p>텔레그램 봇</p>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/admin/banner-editor" class="nav-link <%= currentPage === 'banner-editor' ? 'active' : '' %>">
|
||||||
|
<i class="nav-icon fas fa-paint-brush"></i>
|
||||||
|
<p>배너 편집기</p>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<!-- Content Wrapper -->
|
||||||
|
<div class="content-wrapper">
|
||||||
|
<!-- Content Header -->
|
||||||
|
<div class="content-header">
|
||||||
|
<div class="container-fluid">
|
||||||
|
<div class="row mb-2">
|
||||||
|
<div class="col-sm-6">
|
||||||
|
<h1 class="m-0"><%= title %></h1>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-6">
|
||||||
|
<ol class="breadcrumb float-sm-right">
|
||||||
|
<li class="breadcrumb-item"><a href="/admin/dashboard">홈</a></li>
|
||||||
|
<% if (currentPage !== 'dashboard') { %>
|
||||||
|
<li class="breadcrumb-item active"><%= title %></li>
|
||||||
|
<% } %>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Main content -->
|
||||||
|
<section class="content">
|
||||||
|
<div class="container-fluid">
|
||||||
|
<%- body %>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<footer class="main-footer">
|
||||||
|
<strong>© 2024 <a href="/">SmartSolTech</a></strong>
|
||||||
|
모든 권리 보유.
|
||||||
|
<div class="float-right d-none d-sm-inline-block">
|
||||||
|
<b>Version</b> 2.0.0
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- jQuery (Local) -->
|
||||||
|
<script src="/vendor/jquery/jquery-3.6.0.min.js"></script>
|
||||||
|
<!-- Bootstrap 4 (Local) -->
|
||||||
|
<script src="/vendor/bootstrap/bootstrap.bundle.min.js"></script>
|
||||||
|
<!-- AdminLTE App (Local) -->
|
||||||
|
<script src="/vendor/adminlte/adminlte.min.js"></script>
|
||||||
|
|
||||||
|
<!-- Custom JavaScript -->
|
||||||
|
<script src="/js/main.js"></script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Korean localization for AdminLTE
|
||||||
|
$(document).ready(function() {
|
||||||
|
// Update any English text to Korean
|
||||||
|
$('.brand-link .brand-text').text('스마트솔테크');
|
||||||
|
|
||||||
|
// Add smooth transitions
|
||||||
|
$('.nav-link').on('click', function() {
|
||||||
|
if (!$(this).hasClass('active')) {
|
||||||
|
$('.nav-link.active').removeClass('active');
|
||||||
|
$(this).addClass('active');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
279
.history/views/admin/layout_20251026215850.ejs
Normal file
@@ -0,0 +1,279 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="<%= currentLanguage %>">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title><%= title %> - SmartSolTech Admin</title>
|
||||||
|
|
||||||
|
<!-- Bootstrap CSS (Local) -->
|
||||||
|
<link rel="stylesheet" href="/vendor/bootstrap/bootstrap.min.css">
|
||||||
|
|
||||||
|
<!-- AdminLTE CSS (Local) -->
|
||||||
|
<link rel="stylesheet" href="/vendor/adminlte/adminlte.min.css">
|
||||||
|
|
||||||
|
<!-- Font Awesome (Local) -->
|
||||||
|
<link rel="stylesheet" href="/vendor/adminlte/fontawesome.min.css">
|
||||||
|
|
||||||
|
<!-- Google Fonts -->
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@300;400;500;700&display=swap" rel="stylesheet">
|
||||||
|
|
||||||
|
<!-- Custom Korean Admin Styles -->
|
||||||
|
<style>
|
||||||
|
body, .content-wrapper, .main-sidebar {
|
||||||
|
font-family: 'Noto Sans KR', 'Malgun Gothic', 'Apple SD Gothic Neo', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-text {
|
||||||
|
font-weight: 700;
|
||||||
|
color: #007bff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-sidebar .nav-link {
|
||||||
|
border-radius: 8px;
|
||||||
|
margin: 2px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-sidebar .nav-link.active {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-header h1 {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #2c3e50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
border-radius: 8px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-box {
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-box-icon {
|
||||||
|
border-radius: 12px 0 0 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-light {
|
||||||
|
background: white !important;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-sidebar {
|
||||||
|
background: linear-gradient(180deg, #f8f9fa 0%, #e9ecef 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-dark-primary .nav-sidebar > .nav-item > .nav-link:hover {
|
||||||
|
background-color: rgba(255,255,255,0.1);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="hold-transition sidebar-mini layout-fixed">
|
||||||
|
<div class="wrapper">
|
||||||
|
<!-- Navbar -->
|
||||||
|
<nav class="main-header navbar navbar-expand navbar-light">
|
||||||
|
<!-- Left navbar links -->
|
||||||
|
<ul class="navbar-nav">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" data-widget="pushmenu" href="#" role="button">
|
||||||
|
<i class="fas fa-bars"></i>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item d-none d-sm-inline-block">
|
||||||
|
<a href="/" class="nav-link" target="_blank">
|
||||||
|
<i class="fas fa-external-link-alt mr-1"></i>
|
||||||
|
사이트 보기
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<!-- Right navbar links -->
|
||||||
|
<ul class="navbar-nav ml-auto">
|
||||||
|
<!-- User Menu -->
|
||||||
|
<li class="nav-item dropdown">
|
||||||
|
<a class="nav-link" data-toggle="dropdown" href="#">
|
||||||
|
<i class="far fa-user mr-1"></i>
|
||||||
|
<%= user ? user.name : '관리자' %>
|
||||||
|
</a>
|
||||||
|
<div class="dropdown-menu dropdown-menu-lg dropdown-menu-right">
|
||||||
|
<div class="dropdown-divider"></div>
|
||||||
|
<a href="/admin/settings" class="dropdown-item">
|
||||||
|
<i class="fas fa-cog mr-2"></i> 설정
|
||||||
|
</a>
|
||||||
|
<div class="dropdown-divider"></div>
|
||||||
|
<form action="/admin/logout" method="post" class="dropdown-item p-0">
|
||||||
|
<button type="submit" class="btn btn-link text-left w-100 text-danger">
|
||||||
|
<i class="fas fa-sign-out-alt mr-2"></i> 로그아웃
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<!-- Main Sidebar Container -->
|
||||||
|
<aside class="main-sidebar sidebar-light-primary elevation-4">
|
||||||
|
<!-- Brand Logo -->
|
||||||
|
<a href="/admin/dashboard" class="brand-link">
|
||||||
|
<img src="/images/icons/icon-192x192.png" alt="SmartSolTech" class="brand-image img-circle elevation-3" style="opacity: .8">
|
||||||
|
<span class="brand-text font-weight-bold">SmartSolTech</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<!-- Sidebar -->
|
||||||
|
<div class="sidebar">
|
||||||
|
<!-- Sidebar Menu -->
|
||||||
|
<nav class="mt-3">
|
||||||
|
<ul class="nav nav-pills nav-sidebar flex-column" data-widget="treeview" role="menu" data-accordion="false">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/admin/dashboard" class="nav-link <%= currentPage === 'dashboard' ? 'active' : '' %>">
|
||||||
|
<i class="nav-icon fas fa-tachometer-alt"></i>
|
||||||
|
<p>대시보드</p>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/admin/portfolio" class="nav-link <%= currentPage === 'portfolio' ? 'active' : '' %>">
|
||||||
|
<i class="nav-icon fas fa-briefcase"></i>
|
||||||
|
<p>
|
||||||
|
포트폴리오
|
||||||
|
<% if (typeof stats !== 'undefined' && stats.portfolioCount) { %>
|
||||||
|
<span class="badge badge-info right"><%= stats.portfolioCount %></span>
|
||||||
|
<% } %>
|
||||||
|
</p>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/admin/services" class="nav-link <%= currentPage === 'services' ? 'active' : '' %>">
|
||||||
|
<i class="nav-icon fas fa-cogs"></i>
|
||||||
|
<p>
|
||||||
|
서비스 관리
|
||||||
|
<% if (typeof stats !== 'undefined' && stats.servicesCount) { %>
|
||||||
|
<span class="badge badge-success right"><%= stats.servicesCount %></span>
|
||||||
|
<% } %>
|
||||||
|
</p>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/admin/contacts" class="nav-link <%= currentPage === 'contacts' ? 'active' : '' %>">
|
||||||
|
<i class="nav-icon fas fa-envelope"></i>
|
||||||
|
<p>
|
||||||
|
문의 관리
|
||||||
|
<% if (typeof stats !== 'undefined' && stats.contactsCount) { %>
|
||||||
|
<span class="badge badge-warning right"><%= stats.contactsCount %></span>
|
||||||
|
<% } %>
|
||||||
|
</p>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/admin/media" class="nav-link <%= currentPage === 'media' ? 'active' : '' %>">
|
||||||
|
<i class="nav-icon fas fa-images"></i>
|
||||||
|
<p>미디어 관리</p>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="nav-header">시스템 설정</li>
|
||||||
|
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/admin/settings" class="nav-link <%= currentPage === 'settings' ? 'active' : '' %>">
|
||||||
|
<i class="nav-icon fas fa-cog"></i>
|
||||||
|
<p>사이트 설정</p>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/admin/telegram" class="nav-link <%= currentPage === 'telegram' ? 'active' : '' %>">
|
||||||
|
<i class="nav-icon fab fa-telegram"></i>
|
||||||
|
<p>텔레그램 봇</p>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/admin/banner-editor" class="nav-link <%= currentPage === 'banner-editor' ? 'active' : '' %>">
|
||||||
|
<i class="nav-icon fas fa-paint-brush"></i>
|
||||||
|
<p>배너 편집기</p>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<!-- Content Wrapper -->
|
||||||
|
<div class="content-wrapper">
|
||||||
|
<!-- Content Header -->
|
||||||
|
<div class="content-header">
|
||||||
|
<div class="container-fluid">
|
||||||
|
<div class="row mb-2">
|
||||||
|
<div class="col-sm-6">
|
||||||
|
<h1 class="m-0"><%= title %></h1>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-6">
|
||||||
|
<ol class="breadcrumb float-sm-right">
|
||||||
|
<li class="breadcrumb-item"><a href="/admin/dashboard">홈</a></li>
|
||||||
|
<% if (currentPage !== 'dashboard') { %>
|
||||||
|
<li class="breadcrumb-item active"><%= title %></li>
|
||||||
|
<% } %>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Main content -->
|
||||||
|
<section class="content">
|
||||||
|
<div class="container-fluid">
|
||||||
|
<%- body %>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<footer class="main-footer">
|
||||||
|
<strong>© 2024 <a href="/">SmartSolTech</a></strong>
|
||||||
|
모든 권리 보유.
|
||||||
|
<div class="float-right d-none d-sm-inline-block">
|
||||||
|
<b>Version</b> 2.0.0
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- jQuery (Local) -->
|
||||||
|
<script src="/vendor/jquery/jquery-3.6.0.min.js"></script>
|
||||||
|
<!-- Bootstrap 4 (Local) -->
|
||||||
|
<script src="/vendor/bootstrap/bootstrap.bundle.min.js"></script>
|
||||||
|
<!-- AdminLTE App (Local) -->
|
||||||
|
<script src="/vendor/adminlte/adminlte.min.js"></script>
|
||||||
|
|
||||||
|
<!-- Custom JavaScript -->
|
||||||
|
<script src="/js/main.js"></script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Korean localization for AdminLTE
|
||||||
|
$(document).ready(function() {
|
||||||
|
// Update any English text to Korean
|
||||||
|
$('.brand-link .brand-text').text('스마트솔테크');
|
||||||
|
|
||||||
|
// Add smooth transitions
|
||||||
|
$('.nav-link').on('click', function() {
|
||||||
|
if (!$(this).hasClass('active')) {
|
||||||
|
$('.nav-link.active').removeClass('active');
|
||||||
|
$(this).addClass('active');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
328
.history/views/admin/layout_20251026220236.ejs
Normal file
@@ -0,0 +1,328 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="<%= currentLanguage %>">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title><%= title %> - SmartSolTech Admin</title>
|
||||||
|
|
||||||
|
<!-- Bootstrap CSS (Local) -->
|
||||||
|
<link rel="stylesheet" href="/vendor/bootstrap/bootstrap.min.css">
|
||||||
|
|
||||||
|
<!-- AdminLTE CSS (Local) -->
|
||||||
|
<link rel="stylesheet" href="/vendor/adminlte/adminlte.min.css">
|
||||||
|
|
||||||
|
<!-- Font Awesome (Local) -->
|
||||||
|
<link rel="stylesheet" href="/vendor/adminlte/fontawesome.min.css">
|
||||||
|
|
||||||
|
<!-- Google Fonts -->
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@300;400;500;700&display=swap" rel="stylesheet">
|
||||||
|
|
||||||
|
<!-- Custom Korean Admin Styles -->
|
||||||
|
<style>
|
||||||
|
body, .content-wrapper, .main-sidebar {
|
||||||
|
font-family: 'Noto Sans KR', 'Malgun Gothic', 'Apple SD Gothic Neo', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-text {
|
||||||
|
font-weight: 700;
|
||||||
|
color: #007bff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sidebar navigation styles */
|
||||||
|
.main-sidebar .nav-sidebar .nav-link {
|
||||||
|
border-radius: 8px;
|
||||||
|
margin: 2px 8px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
color: #495057;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-sidebar .nav-sidebar .nav-link:hover {
|
||||||
|
background-color: rgba(0, 123, 255, 0.1);
|
||||||
|
color: #007bff;
|
||||||
|
transform: translateX(5px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-sidebar .nav-sidebar .nav-link.active {
|
||||||
|
background: linear-gradient(135deg, #007bff 0%, #0056b3 100%);
|
||||||
|
color: white !important;
|
||||||
|
box-shadow: 0 4px 8px rgba(0, 123, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-sidebar .nav-sidebar .nav-link.active i {
|
||||||
|
color: white !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-sidebar .nav-sidebar .nav-link.active .badge {
|
||||||
|
background-color: rgba(255, 255, 255, 0.2) !important;
|
||||||
|
color: white !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fix for navigation icons */
|
||||||
|
.nav-icon {
|
||||||
|
margin-right: 8px;
|
||||||
|
width: 20px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-header h1 {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #2c3e50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
border-radius: 8px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-box {
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-box-icon {
|
||||||
|
border-radius: 12px 0 0 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-light {
|
||||||
|
background: white !important;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-sidebar {
|
||||||
|
background: linear-gradient(180deg, #f8f9fa 0%, #e9ecef 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Breadcrumb styling */
|
||||||
|
.breadcrumb {
|
||||||
|
background: transparent;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumb-item a {
|
||||||
|
color: #007bff;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumb-item.active {
|
||||||
|
color: #6c757d;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Content wrapper padding */
|
||||||
|
.content-wrapper {
|
||||||
|
padding-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Badges in navigation */
|
||||||
|
.nav-sidebar .badge {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="hold-transition sidebar-mini layout-fixed">
|
||||||
|
<div class="wrapper">
|
||||||
|
<!-- Navbar -->
|
||||||
|
<nav class="main-header navbar navbar-expand navbar-light">
|
||||||
|
<!-- Left navbar links -->
|
||||||
|
<ul class="navbar-nav">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" data-widget="pushmenu" href="#" role="button">
|
||||||
|
<i class="fas fa-bars"></i>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item d-none d-sm-inline-block">
|
||||||
|
<a href="/" class="nav-link" target="_blank">
|
||||||
|
<i class="fas fa-external-link-alt mr-1"></i>
|
||||||
|
사이트 보기
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<!-- Right navbar links -->
|
||||||
|
<ul class="navbar-nav ml-auto">
|
||||||
|
<!-- User Menu -->
|
||||||
|
<li class="nav-item dropdown">
|
||||||
|
<a class="nav-link" data-toggle="dropdown" href="#">
|
||||||
|
<i class="far fa-user mr-1"></i>
|
||||||
|
<%= user ? user.name : '관리자' %>
|
||||||
|
</a>
|
||||||
|
<div class="dropdown-menu dropdown-menu-lg dropdown-menu-right">
|
||||||
|
<div class="dropdown-divider"></div>
|
||||||
|
<a href="/admin/settings" class="dropdown-item">
|
||||||
|
<i class="fas fa-cog mr-2"></i> 설정
|
||||||
|
</a>
|
||||||
|
<div class="dropdown-divider"></div>
|
||||||
|
<form action="/admin/logout" method="post" class="dropdown-item p-0">
|
||||||
|
<button type="submit" class="btn btn-link text-left w-100 text-danger">
|
||||||
|
<i class="fas fa-sign-out-alt mr-2"></i> 로그아웃
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<!-- Main Sidebar Container -->
|
||||||
|
<aside class="main-sidebar sidebar-light-primary elevation-4">
|
||||||
|
<!-- Brand Logo -->
|
||||||
|
<a href="/admin/dashboard" class="brand-link">
|
||||||
|
<img src="/images/icons/icon-192x192.png" alt="SmartSolTech" class="brand-image img-circle elevation-3" style="opacity: .8">
|
||||||
|
<span class="brand-text font-weight-bold">SmartSolTech</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<!-- Sidebar -->
|
||||||
|
<div class="sidebar">
|
||||||
|
<!-- Sidebar Menu -->
|
||||||
|
<nav class="mt-3">
|
||||||
|
<ul class="nav nav-pills nav-sidebar flex-column" data-widget="treeview" role="menu" data-accordion="false">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/admin/dashboard" class="nav-link <%= currentPage === 'dashboard' ? 'active' : '' %>">
|
||||||
|
<i class="nav-icon fas fa-tachometer-alt"></i>
|
||||||
|
<p>대시보드</p>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/admin/portfolio" class="nav-link <%= currentPage === 'portfolio' ? 'active' : '' %>">
|
||||||
|
<i class="nav-icon fas fa-briefcase"></i>
|
||||||
|
<p>
|
||||||
|
포트폴리오
|
||||||
|
<% if (typeof stats !== 'undefined' && stats.portfolioCount) { %>
|
||||||
|
<span class="badge badge-info right"><%= stats.portfolioCount %></span>
|
||||||
|
<% } %>
|
||||||
|
</p>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/admin/services" class="nav-link <%= currentPage === 'services' ? 'active' : '' %>">
|
||||||
|
<i class="nav-icon fas fa-cogs"></i>
|
||||||
|
<p>
|
||||||
|
서비스 관리
|
||||||
|
<% if (typeof stats !== 'undefined' && stats.servicesCount) { %>
|
||||||
|
<span class="badge badge-success right"><%= stats.servicesCount %></span>
|
||||||
|
<% } %>
|
||||||
|
</p>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/admin/contacts" class="nav-link <%= currentPage === 'contacts' ? 'active' : '' %>">
|
||||||
|
<i class="nav-icon fas fa-envelope"></i>
|
||||||
|
<p>
|
||||||
|
문의 관리
|
||||||
|
<% if (typeof stats !== 'undefined' && stats.contactsCount) { %>
|
||||||
|
<span class="badge badge-warning right"><%= stats.contactsCount %></span>
|
||||||
|
<% } %>
|
||||||
|
</p>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/admin/media" class="nav-link <%= currentPage === 'media' ? 'active' : '' %>">
|
||||||
|
<i class="nav-icon fas fa-images"></i>
|
||||||
|
<p>미디어 관리</p>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="nav-header">시스템 설정</li>
|
||||||
|
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/admin/settings" class="nav-link <%= currentPage === 'settings' ? 'active' : '' %>">
|
||||||
|
<i class="nav-icon fas fa-cog"></i>
|
||||||
|
<p>사이트 설정</p>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/admin/telegram" class="nav-link <%= currentPage === 'telegram' ? 'active' : '' %>">
|
||||||
|
<i class="nav-icon fab fa-telegram"></i>
|
||||||
|
<p>텔레그램 봇</p>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/admin/banner-editor" class="nav-link <%= currentPage === 'banner-editor' ? 'active' : '' %>">
|
||||||
|
<i class="nav-icon fas fa-paint-brush"></i>
|
||||||
|
<p>배너 편집기</p>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<!-- Content Wrapper -->
|
||||||
|
<div class="content-wrapper">
|
||||||
|
<!-- Content Header -->
|
||||||
|
<div class="content-header">
|
||||||
|
<div class="container-fluid">
|
||||||
|
<div class="row mb-2">
|
||||||
|
<div class="col-sm-6">
|
||||||
|
<h1 class="m-0"><%= title %></h1>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-6">
|
||||||
|
<ol class="breadcrumb float-sm-right">
|
||||||
|
<li class="breadcrumb-item"><a href="/admin/dashboard">홈</a></li>
|
||||||
|
<% if (currentPage !== 'dashboard') { %>
|
||||||
|
<li class="breadcrumb-item active"><%= title %></li>
|
||||||
|
<% } %>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Main content -->
|
||||||
|
<section class="content">
|
||||||
|
<div class="container-fluid">
|
||||||
|
<%- body %>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<footer class="main-footer">
|
||||||
|
<strong>© 2024 <a href="/">SmartSolTech</a></strong>
|
||||||
|
모든 권리 보유.
|
||||||
|
<div class="float-right d-none d-sm-inline-block">
|
||||||
|
<b>Version</b> 2.0.0
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- jQuery (Local) -->
|
||||||
|
<script src="/vendor/jquery/jquery-3.6.0.min.js"></script>
|
||||||
|
<!-- Bootstrap 4 (Local) -->
|
||||||
|
<script src="/vendor/bootstrap/bootstrap.bundle.min.js"></script>
|
||||||
|
<!-- AdminLTE App (Local) -->
|
||||||
|
<script src="/vendor/adminlte/adminlte.min.js"></script>
|
||||||
|
|
||||||
|
<!-- Custom JavaScript -->
|
||||||
|
<script src="/js/main.js"></script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Korean localization for AdminLTE
|
||||||
|
$(document).ready(function() {
|
||||||
|
// Update any English text to Korean
|
||||||
|
$('.brand-link .brand-text').text('스마트솔테크');
|
||||||
|
|
||||||
|
// Add smooth transitions
|
||||||
|
$('.nav-link').on('click', function() {
|
||||||
|
if (!$(this).hasClass('active')) {
|
||||||
|
$('.nav-link.active').removeClass('active');
|
||||||
|
$(this).addClass('active');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
353
.history/views/admin/layout_20251026220250.ejs
Normal file
@@ -0,0 +1,353 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="<%= currentLanguage %>">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title><%= title %> - SmartSolTech Admin</title>
|
||||||
|
|
||||||
|
<!-- Bootstrap CSS (Local) -->
|
||||||
|
<link rel="stylesheet" href="/vendor/bootstrap/bootstrap.min.css">
|
||||||
|
|
||||||
|
<!-- AdminLTE CSS (Local) -->
|
||||||
|
<link rel="stylesheet" href="/vendor/adminlte/adminlte.min.css">
|
||||||
|
|
||||||
|
<!-- Font Awesome (Local) -->
|
||||||
|
<link rel="stylesheet" href="/vendor/adminlte/fontawesome.min.css">
|
||||||
|
|
||||||
|
<!-- Google Fonts -->
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@300;400;500;700&display=swap" rel="stylesheet">
|
||||||
|
|
||||||
|
<!-- Custom Korean Admin Styles -->
|
||||||
|
<style>
|
||||||
|
body, .content-wrapper, .main-sidebar {
|
||||||
|
font-family: 'Noto Sans KR', 'Malgun Gothic', 'Apple SD Gothic Neo', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-text {
|
||||||
|
font-weight: 700;
|
||||||
|
color: #007bff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sidebar navigation styles */
|
||||||
|
.main-sidebar .nav-sidebar .nav-link {
|
||||||
|
border-radius: 8px;
|
||||||
|
margin: 2px 8px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
color: #495057;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-sidebar .nav-sidebar .nav-link:hover {
|
||||||
|
background-color: rgba(0, 123, 255, 0.1);
|
||||||
|
color: #007bff;
|
||||||
|
transform: translateX(5px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-sidebar .nav-sidebar .nav-link.active {
|
||||||
|
background: linear-gradient(135deg, #007bff 0%, #0056b3 100%);
|
||||||
|
color: white !important;
|
||||||
|
box-shadow: 0 4px 8px rgba(0, 123, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-sidebar .nav-sidebar .nav-link.active i {
|
||||||
|
color: white !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-sidebar .nav-sidebar .nav-link.active .badge {
|
||||||
|
background-color: rgba(255, 255, 255, 0.2) !important;
|
||||||
|
color: white !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fix for navigation icons */
|
||||||
|
.nav-icon {
|
||||||
|
margin-right: 8px;
|
||||||
|
width: 20px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-header h1 {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #2c3e50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
border-radius: 8px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-box {
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-box-icon {
|
||||||
|
border-radius: 12px 0 0 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-light {
|
||||||
|
background: white !important;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-sidebar {
|
||||||
|
background: linear-gradient(180deg, #f8f9fa 0%, #e9ecef 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Breadcrumb styling */
|
||||||
|
.breadcrumb {
|
||||||
|
background: transparent;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumb-item a {
|
||||||
|
color: #007bff;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumb-item.active {
|
||||||
|
color: #6c757d;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Content wrapper padding */
|
||||||
|
.content-wrapper {
|
||||||
|
padding-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Badges in navigation */
|
||||||
|
.nav-sidebar .badge {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="hold-transition sidebar-mini layout-fixed">
|
||||||
|
<div class="wrapper">
|
||||||
|
<!-- Navbar -->
|
||||||
|
<nav class="main-header navbar navbar-expand navbar-light">
|
||||||
|
<!-- Left navbar links -->
|
||||||
|
<ul class="navbar-nav">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" data-widget="pushmenu" href="#" role="button">
|
||||||
|
<i class="fas fa-bars"></i>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item d-none d-sm-inline-block">
|
||||||
|
<a href="/" class="nav-link" target="_blank">
|
||||||
|
<i class="fas fa-external-link-alt mr-1"></i>
|
||||||
|
사이트 보기
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<!-- Right navbar links -->
|
||||||
|
<ul class="navbar-nav ml-auto">
|
||||||
|
<!-- User Menu -->
|
||||||
|
<li class="nav-item dropdown">
|
||||||
|
<a class="nav-link" data-toggle="dropdown" href="#">
|
||||||
|
<i class="far fa-user mr-1"></i>
|
||||||
|
<%= user ? user.name : '관리자' %>
|
||||||
|
</a>
|
||||||
|
<div class="dropdown-menu dropdown-menu-lg dropdown-menu-right">
|
||||||
|
<div class="dropdown-divider"></div>
|
||||||
|
<a href="/admin/settings" class="dropdown-item">
|
||||||
|
<i class="fas fa-cog mr-2"></i> 설정
|
||||||
|
</a>
|
||||||
|
<div class="dropdown-divider"></div>
|
||||||
|
<form action="/admin/logout" method="post" class="dropdown-item p-0">
|
||||||
|
<button type="submit" class="btn btn-link text-left w-100 text-danger">
|
||||||
|
<i class="fas fa-sign-out-alt mr-2"></i> 로그아웃
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<!-- Main Sidebar Container -->
|
||||||
|
<aside class="main-sidebar sidebar-light-primary elevation-4">
|
||||||
|
<!-- Brand Logo -->
|
||||||
|
<a href="/admin/dashboard" class="brand-link">
|
||||||
|
<img src="/images/icons/icon-192x192.png" alt="SmartSolTech" class="brand-image img-circle elevation-3" style="opacity: .8">
|
||||||
|
<span class="brand-text font-weight-bold">SmartSolTech</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<!-- Sidebar -->
|
||||||
|
<div class="sidebar">
|
||||||
|
<!-- Sidebar Menu -->
|
||||||
|
<nav class="mt-3">
|
||||||
|
<ul class="nav nav-pills nav-sidebar flex-column" data-widget="treeview" role="menu" data-accordion="false">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/admin/dashboard" class="nav-link <%= currentPage === 'dashboard' ? 'active' : '' %>">
|
||||||
|
<i class="nav-icon fas fa-tachometer-alt"></i>
|
||||||
|
<p>대시보드</p>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/admin/portfolio" class="nav-link <%= currentPage === 'portfolio' ? 'active' : '' %>">
|
||||||
|
<i class="nav-icon fas fa-briefcase"></i>
|
||||||
|
<p>
|
||||||
|
포트폴리오
|
||||||
|
<% if (typeof stats !== 'undefined' && stats.portfolioCount) { %>
|
||||||
|
<span class="badge badge-info right"><%= stats.portfolioCount %></span>
|
||||||
|
<% } %>
|
||||||
|
</p>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/admin/services" class="nav-link <%= currentPage === 'services' ? 'active' : '' %>">
|
||||||
|
<i class="nav-icon fas fa-cogs"></i>
|
||||||
|
<p>
|
||||||
|
서비스 관리
|
||||||
|
<% if (typeof stats !== 'undefined' && stats.servicesCount) { %>
|
||||||
|
<span class="badge badge-success right"><%= stats.servicesCount %></span>
|
||||||
|
<% } %>
|
||||||
|
</p>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/admin/contacts" class="nav-link <%= currentPage === 'contacts' ? 'active' : '' %>">
|
||||||
|
<i class="nav-icon fas fa-envelope"></i>
|
||||||
|
<p>
|
||||||
|
문의 관리
|
||||||
|
<% if (typeof stats !== 'undefined' && stats.contactsCount) { %>
|
||||||
|
<span class="badge badge-warning right"><%= stats.contactsCount %></span>
|
||||||
|
<% } %>
|
||||||
|
</p>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/admin/media" class="nav-link <%= currentPage === 'media' ? 'active' : '' %>">
|
||||||
|
<i class="nav-icon fas fa-images"></i>
|
||||||
|
<p>미디어 관리</p>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="nav-header">시스템 설정</li>
|
||||||
|
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/admin/settings" class="nav-link <%= currentPage === 'settings' ? 'active' : '' %>">
|
||||||
|
<i class="nav-icon fas fa-cog"></i>
|
||||||
|
<p>사이트 설정</p>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/admin/telegram" class="nav-link <%= currentPage === 'telegram' ? 'active' : '' %>">
|
||||||
|
<i class="nav-icon fab fa-telegram"></i>
|
||||||
|
<p>텔레그램 봇</p>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/admin/banner-editor" class="nav-link <%= currentPage === 'banner-editor' ? 'active' : '' %>">
|
||||||
|
<i class="nav-icon fas fa-paint-brush"></i>
|
||||||
|
<p>배너 편집기</p>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<!-- Content Wrapper -->
|
||||||
|
<div class="content-wrapper">
|
||||||
|
<!-- Content Header -->
|
||||||
|
<div class="content-header">
|
||||||
|
<div class="container-fluid">
|
||||||
|
<div class="row mb-2">
|
||||||
|
<div class="col-sm-6">
|
||||||
|
<h1 class="m-0"><%= title %></h1>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-6">
|
||||||
|
<ol class="breadcrumb float-sm-right">
|
||||||
|
<li class="breadcrumb-item"><a href="/admin/dashboard">홈</a></li>
|
||||||
|
<% if (currentPage !== 'dashboard') { %>
|
||||||
|
<li class="breadcrumb-item active"><%= title %></li>
|
||||||
|
<% } %>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Main content -->
|
||||||
|
<section class="content">
|
||||||
|
<div class="container-fluid">
|
||||||
|
<%- body %>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<footer class="main-footer">
|
||||||
|
<strong>© 2024 <a href="/">SmartSolTech</a></strong>
|
||||||
|
모든 권리 보유.
|
||||||
|
<div class="float-right d-none d-sm-inline-block">
|
||||||
|
<b>Version</b> 2.0.0
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- jQuery (Local) -->
|
||||||
|
<script src="/vendor/jquery/jquery-3.6.0.min.js"></script>
|
||||||
|
<!-- Bootstrap 4 (Local) -->
|
||||||
|
<script src="/vendor/bootstrap/bootstrap.bundle.min.js"></script>
|
||||||
|
<!-- AdminLTE App (Local) -->
|
||||||
|
<script src="/vendor/adminlte/adminlte.min.js"></script>
|
||||||
|
|
||||||
|
<!-- Custom JavaScript -->
|
||||||
|
<script src="/js/main.js"></script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Korean localization for AdminLTE
|
||||||
|
$(document).ready(function() {
|
||||||
|
// Update any English text to Korean
|
||||||
|
$('.brand-link .brand-text').text('스마트솔테크');
|
||||||
|
|
||||||
|
// Get current page from URL
|
||||||
|
const currentPath = window.location.pathname;
|
||||||
|
const currentPage = currentPath.split('/').pop() || 'dashboard';
|
||||||
|
|
||||||
|
// Remove active class from all nav links
|
||||||
|
$('.nav-sidebar .nav-link').removeClass('active');
|
||||||
|
|
||||||
|
// Add active class to current page nav link
|
||||||
|
$('.nav-sidebar .nav-link').each(function() {
|
||||||
|
const href = $(this).attr('href');
|
||||||
|
if (href) {
|
||||||
|
const pageName = href.split('/').pop();
|
||||||
|
if (currentPath.includes(pageName) ||
|
||||||
|
(currentPath === '/admin' && pageName === 'dashboard') ||
|
||||||
|
(currentPath === '/admin/' && pageName === 'dashboard')) {
|
||||||
|
$(this).addClass('active');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add smooth transitions
|
||||||
|
$('.nav-link').on('click', function() {
|
||||||
|
if (!$(this).hasClass('active')) {
|
||||||
|
$('.nav-link.active').removeClass('active');
|
||||||
|
$(this).addClass('active');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initialize AdminLTE components
|
||||||
|
if (typeof AdminLTE !== 'undefined') {
|
||||||
|
AdminLTE.init();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
353
.history/views/admin/layout_20251026220452.ejs
Normal file
@@ -0,0 +1,353 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="<%= currentLanguage %>">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title><%= title %> - SmartSolTech Admin</title>
|
||||||
|
|
||||||
|
<!-- Bootstrap CSS (Local) -->
|
||||||
|
<link rel="stylesheet" href="/vendor/bootstrap/bootstrap.min.css">
|
||||||
|
|
||||||
|
<!-- AdminLTE CSS (Local) -->
|
||||||
|
<link rel="stylesheet" href="/vendor/adminlte/adminlte.min.css">
|
||||||
|
|
||||||
|
<!-- Font Awesome (Local) -->
|
||||||
|
<link rel="stylesheet" href="/vendor/adminlte/fontawesome.min.css">
|
||||||
|
|
||||||
|
<!-- Google Fonts -->
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@300;400;500;700&display=swap" rel="stylesheet">
|
||||||
|
|
||||||
|
<!-- Custom Korean Admin Styles -->
|
||||||
|
<style>
|
||||||
|
body, .content-wrapper, .main-sidebar {
|
||||||
|
font-family: 'Noto Sans KR', 'Malgun Gothic', 'Apple SD Gothic Neo', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-text {
|
||||||
|
font-weight: 700;
|
||||||
|
color: #007bff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sidebar navigation styles */
|
||||||
|
.main-sidebar .nav-sidebar .nav-link {
|
||||||
|
border-radius: 8px;
|
||||||
|
margin: 2px 8px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
color: #495057;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-sidebar .nav-sidebar .nav-link:hover {
|
||||||
|
background-color: rgba(0, 123, 255, 0.1);
|
||||||
|
color: #007bff;
|
||||||
|
transform: translateX(5px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-sidebar .nav-sidebar .nav-link.active {
|
||||||
|
background: linear-gradient(135deg, #007bff 0%, #0056b3 100%);
|
||||||
|
color: white !important;
|
||||||
|
box-shadow: 0 4px 8px rgba(0, 123, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-sidebar .nav-sidebar .nav-link.active i {
|
||||||
|
color: white !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-sidebar .nav-sidebar .nav-link.active .badge {
|
||||||
|
background-color: rgba(255, 255, 255, 0.2) !important;
|
||||||
|
color: white !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fix for navigation icons */
|
||||||
|
.nav-icon {
|
||||||
|
margin-right: 8px;
|
||||||
|
width: 20px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-header h1 {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #2c3e50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
border-radius: 8px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-box {
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-box-icon {
|
||||||
|
border-radius: 12px 0 0 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-light {
|
||||||
|
background: white !important;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-sidebar {
|
||||||
|
background: linear-gradient(180deg, #f8f9fa 0%, #e9ecef 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Breadcrumb styling */
|
||||||
|
.breadcrumb {
|
||||||
|
background: transparent;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumb-item a {
|
||||||
|
color: #007bff;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumb-item.active {
|
||||||
|
color: #6c757d;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Content wrapper padding */
|
||||||
|
.content-wrapper {
|
||||||
|
padding-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Badges in navigation */
|
||||||
|
.nav-sidebar .badge {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="hold-transition sidebar-mini layout-fixed">
|
||||||
|
<div class="wrapper">
|
||||||
|
<!-- Navbar -->
|
||||||
|
<nav class="main-header navbar navbar-expand navbar-light">
|
||||||
|
<!-- Left navbar links -->
|
||||||
|
<ul class="navbar-nav">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" data-widget="pushmenu" href="#" role="button">
|
||||||
|
<i class="fas fa-bars"></i>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item d-none d-sm-inline-block">
|
||||||
|
<a href="/" class="nav-link" target="_blank">
|
||||||
|
<i class="fas fa-external-link-alt mr-1"></i>
|
||||||
|
사이트 보기
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<!-- Right navbar links -->
|
||||||
|
<ul class="navbar-nav ml-auto">
|
||||||
|
<!-- User Menu -->
|
||||||
|
<li class="nav-item dropdown">
|
||||||
|
<a class="nav-link" data-toggle="dropdown" href="#">
|
||||||
|
<i class="far fa-user mr-1"></i>
|
||||||
|
<%= user ? user.name : '관리자' %>
|
||||||
|
</a>
|
||||||
|
<div class="dropdown-menu dropdown-menu-lg dropdown-menu-right">
|
||||||
|
<div class="dropdown-divider"></div>
|
||||||
|
<a href="/admin/settings" class="dropdown-item">
|
||||||
|
<i class="fas fa-cog mr-2"></i> 설정
|
||||||
|
</a>
|
||||||
|
<div class="dropdown-divider"></div>
|
||||||
|
<form action="/admin/logout" method="post" class="dropdown-item p-0">
|
||||||
|
<button type="submit" class="btn btn-link text-left w-100 text-danger">
|
||||||
|
<i class="fas fa-sign-out-alt mr-2"></i> 로그아웃
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<!-- Main Sidebar Container -->
|
||||||
|
<aside class="main-sidebar sidebar-light-primary elevation-4">
|
||||||
|
<!-- Brand Logo -->
|
||||||
|
<a href="/admin/dashboard" class="brand-link">
|
||||||
|
<img src="/images/icons/icon-192x192.png" alt="SmartSolTech" class="brand-image img-circle elevation-3" style="opacity: .8">
|
||||||
|
<span class="brand-text font-weight-bold">SmartSolTech</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<!-- Sidebar -->
|
||||||
|
<div class="sidebar">
|
||||||
|
<!-- Sidebar Menu -->
|
||||||
|
<nav class="mt-3">
|
||||||
|
<ul class="nav nav-pills nav-sidebar flex-column" data-widget="treeview" role="menu" data-accordion="false">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/admin/dashboard" class="nav-link <%= currentPage === 'dashboard' ? 'active' : '' %>">
|
||||||
|
<i class="nav-icon fas fa-tachometer-alt"></i>
|
||||||
|
<p>대시보드</p>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/admin/portfolio" class="nav-link <%= currentPage === 'portfolio' ? 'active' : '' %>">
|
||||||
|
<i class="nav-icon fas fa-briefcase"></i>
|
||||||
|
<p>
|
||||||
|
포트폴리오
|
||||||
|
<% if (typeof stats !== 'undefined' && stats.portfolioCount) { %>
|
||||||
|
<span class="badge badge-info right"><%= stats.portfolioCount %></span>
|
||||||
|
<% } %>
|
||||||
|
</p>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/admin/services" class="nav-link <%= currentPage === 'services' ? 'active' : '' %>">
|
||||||
|
<i class="nav-icon fas fa-cogs"></i>
|
||||||
|
<p>
|
||||||
|
서비스 관리
|
||||||
|
<% if (typeof stats !== 'undefined' && stats.servicesCount) { %>
|
||||||
|
<span class="badge badge-success right"><%= stats.servicesCount %></span>
|
||||||
|
<% } %>
|
||||||
|
</p>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/admin/contacts" class="nav-link <%= currentPage === 'contacts' ? 'active' : '' %>">
|
||||||
|
<i class="nav-icon fas fa-envelope"></i>
|
||||||
|
<p>
|
||||||
|
문의 관리
|
||||||
|
<% if (typeof stats !== 'undefined' && stats.contactsCount) { %>
|
||||||
|
<span class="badge badge-warning right"><%= stats.contactsCount %></span>
|
||||||
|
<% } %>
|
||||||
|
</p>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/admin/media" class="nav-link <%= currentPage === 'media' ? 'active' : '' %>">
|
||||||
|
<i class="nav-icon fas fa-images"></i>
|
||||||
|
<p>미디어 관리</p>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="nav-header">시스템 설정</li>
|
||||||
|
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/admin/settings" class="nav-link <%= currentPage === 'settings' ? 'active' : '' %>">
|
||||||
|
<i class="nav-icon fas fa-cog"></i>
|
||||||
|
<p>사이트 설정</p>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/admin/telegram" class="nav-link <%= currentPage === 'telegram' ? 'active' : '' %>">
|
||||||
|
<i class="nav-icon fab fa-telegram"></i>
|
||||||
|
<p>텔레그램 봇</p>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/admin/banner-editor" class="nav-link <%= currentPage === 'banner-editor' ? 'active' : '' %>">
|
||||||
|
<i class="nav-icon fas fa-paint-brush"></i>
|
||||||
|
<p>배너 편집기</p>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<!-- Content Wrapper -->
|
||||||
|
<div class="content-wrapper">
|
||||||
|
<!-- Content Header -->
|
||||||
|
<div class="content-header">
|
||||||
|
<div class="container-fluid">
|
||||||
|
<div class="row mb-2">
|
||||||
|
<div class="col-sm-6">
|
||||||
|
<h1 class="m-0"><%= title %></h1>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-6">
|
||||||
|
<ol class="breadcrumb float-sm-right">
|
||||||
|
<li class="breadcrumb-item"><a href="/admin/dashboard">홈</a></li>
|
||||||
|
<% if (currentPage !== 'dashboard') { %>
|
||||||
|
<li class="breadcrumb-item active"><%= title %></li>
|
||||||
|
<% } %>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Main content -->
|
||||||
|
<section class="content">
|
||||||
|
<div class="container-fluid">
|
||||||
|
<%- body %>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<footer class="main-footer">
|
||||||
|
<strong>© 2024 <a href="/">SmartSolTech</a></strong>
|
||||||
|
모든 권리 보유.
|
||||||
|
<div class="float-right d-none d-sm-inline-block">
|
||||||
|
<b>Version</b> 2.0.0
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- jQuery (Local) -->
|
||||||
|
<script src="/vendor/jquery/jquery-3.6.0.min.js"></script>
|
||||||
|
<!-- Bootstrap 4 (Local) -->
|
||||||
|
<script src="/vendor/bootstrap/bootstrap.bundle.min.js"></script>
|
||||||
|
<!-- AdminLTE App (Local) -->
|
||||||
|
<script src="/vendor/adminlte/adminlte.min.js"></script>
|
||||||
|
|
||||||
|
<!-- Custom JavaScript -->
|
||||||
|
<script src="/js/main.js"></script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Korean localization for AdminLTE
|
||||||
|
$(document).ready(function() {
|
||||||
|
// Update any English text to Korean
|
||||||
|
$('.brand-link .brand-text').text('스마트솔테크');
|
||||||
|
|
||||||
|
// Get current page from URL
|
||||||
|
const currentPath = window.location.pathname;
|
||||||
|
const currentPage = currentPath.split('/').pop() || 'dashboard';
|
||||||
|
|
||||||
|
// Remove active class from all nav links
|
||||||
|
$('.nav-sidebar .nav-link').removeClass('active');
|
||||||
|
|
||||||
|
// Add active class to current page nav link
|
||||||
|
$('.nav-sidebar .nav-link').each(function() {
|
||||||
|
const href = $(this).attr('href');
|
||||||
|
if (href) {
|
||||||
|
const pageName = href.split('/').pop();
|
||||||
|
if (currentPath.includes(pageName) ||
|
||||||
|
(currentPath === '/admin' && pageName === 'dashboard') ||
|
||||||
|
(currentPath === '/admin/' && pageName === 'dashboard')) {
|
||||||
|
$(this).addClass('active');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add smooth transitions
|
||||||
|
$('.nav-link').on('click', function() {
|
||||||
|
if (!$(this).hasClass('active')) {
|
||||||
|
$('.nav-link.active').removeClass('active');
|
||||||
|
$(this).addClass('active');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initialize AdminLTE components
|
||||||
|
if (typeof AdminLTE !== 'undefined') {
|
||||||
|
AdminLTE.init();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
788
.history/views/admin/media_20251026214204.ejs
Normal file
@@ -0,0 +1,788 @@
|
|||||||
|
<!-- Content Header (Page header) -->
|
||||||
|
<section class="content-header">
|
||||||
|
<div class="container-fluid">
|
||||||
|
<div class="row mb-2">
|
||||||
|
<div class="col-sm-6">
|
||||||
|
<h1><i class="fas fa-images mr-2"></i>Медиа Галерея</h1>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-6">
|
||||||
|
<div class="float-sm-right">
|
||||||
|
<button id="refresh-btn" class="btn btn-secondary">
|
||||||
|
<i class="fas fa-sync-alt mr-1"></i>Обновить
|
||||||
|
</button>
|
||||||
|
<button id="upload-btn" class="btn btn-primary">
|
||||||
|
<i class="fas fa-upload mr-1"></i>Загрузить файлы
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Main content -->
|
||||||
|
<section class="content">
|
||||||
|
<div class="container-fluid">
|
||||||
|
<!-- Upload Zone -->
|
||||||
|
<div id="upload-zone" class="card" style="display: none;">
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<div class="mb-4">
|
||||||
|
<i class="fas fa-cloud-upload-alt fa-6x text-muted mb-4"></i>
|
||||||
|
<p class="h5 text-muted mb-2">Перетащите файлы сюда или нажмите для выбора</p>
|
||||||
|
<p class="text-muted">Поддерживаются: JPG, PNG, GIF, SVG (максимум 10MB каждый)</p>
|
||||||
|
</div>
|
||||||
|
<input type="file" id="file-input" multiple accept="image/*" class="d-none">
|
||||||
|
<button type="button" onclick="document.getElementById('file-input').click()" class="btn btn-primary">
|
||||||
|
Выбрать файлы
|
||||||
|
</button>
|
||||||
|
<button id="cancel-upload" class="btn btn-secondary ml-3">
|
||||||
|
Отмена
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Upload Progress -->
|
||||||
|
<div id="upload-progress" class="card" style="display: none;">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title">Загрузка файлов</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div id="progress-list">
|
||||||
|
<!-- Progress items will be added here -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Filter and Search -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Тип файла</label>
|
||||||
|
<select id="file-type-filter" class="form-control">
|
||||||
|
<option value="">Все типы</option>
|
||||||
|
<option value="image/jpeg">JPEG</option>
|
||||||
|
<option value="image/png">PNG</option>
|
||||||
|
<option value="image/gif">GIF</option>
|
||||||
|
<option value="image/svg+xml">SVG</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Размер</label>
|
||||||
|
<select id="size-filter" class="form-control">
|
||||||
|
<option value="">Любой размер</option>
|
||||||
|
<option value="small">Маленький (< 1MB)</option>
|
||||||
|
<option value="medium">Средний (1-5MB)</option>
|
||||||
|
<option value="large">Большой (> 5MB)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Поиск</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<input type="text" id="search-input" placeholder="Поиск по имени файла..." class="form-control">
|
||||||
|
<div class="input-group-append">
|
||||||
|
<span class="input-group-text"><i class="fas fa-search"></i></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Media Grid -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title">Файлы</h3>
|
||||||
|
<div class="card-tools">
|
||||||
|
<span id="file-count" class="badge badge-secondary">Загрузка...</span>
|
||||||
|
<div class="btn-group ml-2">
|
||||||
|
<button id="grid-view" class="btn btn-sm btn-default">
|
||||||
|
<i class="fas fa-th-large"></i>
|
||||||
|
</button>
|
||||||
|
<button id="list-view" class="btn btn-sm btn-default">
|
||||||
|
<i class="fas fa-list"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<!-- Loading State -->
|
||||||
|
<div id="loading" class="text-center py-5">
|
||||||
|
<div class="spinner-border text-primary" role="status">
|
||||||
|
<span class="sr-only">Загрузка...</span>
|
||||||
|
</div>
|
||||||
|
<p class="mt-2 text-muted">Загрузка медиа файлов...</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Empty State -->
|
||||||
|
<div id="empty-state" class="text-center py-5" style="display: none;">
|
||||||
|
<i class="fas fa-images fa-6x text-muted mb-4"></i>
|
||||||
|
<h4 class="text-muted mb-2">Нет загруженных файлов</h4>
|
||||||
|
<p class="text-muted mb-4">Начните с загрузки ваших первых изображений</p>
|
||||||
|
<button onclick="document.getElementById('upload-btn').click()" class="btn btn-primary">
|
||||||
|
<i class="fas fa-upload mr-2"></i>Загрузить файлы
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Media Grid -->
|
||||||
|
<div id="media-grid" class="row">
|
||||||
|
<!-- Media items will be loaded here -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Media List -->
|
||||||
|
<div id="media-list" style="display: none;">
|
||||||
|
<!-- List items will be loaded here -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Pagination -->
|
||||||
|
<div class="card-footer" id="pagination" style="display: none;">
|
||||||
|
<nav aria-label="Page navigation">
|
||||||
|
<ul class="pagination justify-content-center m-0">
|
||||||
|
<li class="page-item">
|
||||||
|
<button id="prev-page" class="page-link">
|
||||||
|
<i class="fas fa-chevron-left"></i>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<div id="page-numbers" class="d-flex">
|
||||||
|
<!-- Page numbers will be added here -->
|
||||||
|
</div>
|
||||||
|
<li class="page-item">
|
||||||
|
<button id="next-page" class="page-link">
|
||||||
|
<i class="fas fa-chevron-right"></i>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Media Preview Modal -->
|
||||||
|
<div class="modal fade" id="preview-modal" tabindex="-1" role="dialog">
|
||||||
|
<div class="modal-dialog modal-xl" role="document">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h4 id="modal-title" class="modal-title">Предпросмотр файла</h4>
|
||||||
|
<button type="button" class="close" data-dismiss="modal">
|
||||||
|
<span>×</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-8">
|
||||||
|
<img id="modal-image" src="" alt="" class="img-fluid rounded">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Имя файла</label>
|
||||||
|
<input id="modal-filename" type="text" class="form-control" readonly>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>URL</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<input id="modal-url" type="text" class="form-control" readonly>
|
||||||
|
<div class="input-group-append">
|
||||||
|
<button onclick="copyToClipboard()" class="btn btn-outline-secondary" type="button">
|
||||||
|
<i class="fas fa-copy"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-6">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Размер</label>
|
||||||
|
<p id="modal-size" class="form-control-plaintext">-</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-6">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Тип</label>
|
||||||
|
<p id="modal-type" class="form-control-plaintext">-</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-6">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Ширина</label>
|
||||||
|
<p id="modal-width" class="form-control-plaintext">-</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-6">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Высота</label>
|
||||||
|
<p id="modal-height" class="form-control-plaintext">-</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Загружено</label>
|
||||||
|
<p id="modal-date" class="form-control-plaintext">-</p>
|
||||||
|
</div>
|
||||||
|
<div class="btn-group d-flex">
|
||||||
|
<button onclick="downloadFile()" class="btn btn-primary">
|
||||||
|
<i class="fas fa-download mr-1"></i>Скачать
|
||||||
|
</button>
|
||||||
|
<button onclick="deleteFile()" class="btn btn-danger">
|
||||||
|
<i class="fas fa-trash mr-1"></i>Удалить
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
class MediaGallery {
|
||||||
|
constructor() {
|
||||||
|
this.currentFiles = [];
|
||||||
|
this.filteredFiles = [];
|
||||||
|
this.currentView = 'grid';
|
||||||
|
this.currentPage = 1;
|
||||||
|
this.itemsPerPage = 24;
|
||||||
|
this.currentFile = null;
|
||||||
|
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this.setupEventListeners();
|
||||||
|
this.loadMedia();
|
||||||
|
}
|
||||||
|
|
||||||
|
setupEventListeners() {
|
||||||
|
// Upload button
|
||||||
|
document.getElementById('upload-btn').addEventListener('click', () => {
|
||||||
|
this.showUploadZone();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Cancel upload
|
||||||
|
document.getElementById('cancel-upload').addEventListener('click', () => {
|
||||||
|
this.hideUploadZone();
|
||||||
|
});
|
||||||
|
|
||||||
|
// File input
|
||||||
|
document.getElementById('file-input').addEventListener('change', (e) => {
|
||||||
|
this.handleFiles(e.target.files);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Refresh button
|
||||||
|
document.getElementById('refresh-btn').addEventListener('click', () => {
|
||||||
|
this.loadMedia();
|
||||||
|
});
|
||||||
|
|
||||||
|
// View toggle
|
||||||
|
document.getElementById('grid-view').addEventListener('click', () => {
|
||||||
|
this.setView('grid');
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('list-view').addEventListener('click', () => {
|
||||||
|
this.setView('list');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Filters
|
||||||
|
document.getElementById('file-type-filter').addEventListener('change', () => {
|
||||||
|
this.applyFilters();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('size-filter').addEventListener('change', () => {
|
||||||
|
this.applyFilters();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('search-input').addEventListener('input', () => {
|
||||||
|
this.applyFilters();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Modal
|
||||||
|
document.getElementById('close-modal').addEventListener('click', () => {
|
||||||
|
this.closeModal();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Upload zone drag and drop
|
||||||
|
const uploadZone = document.getElementById('upload-zone');
|
||||||
|
uploadZone.addEventListener('dragover', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
uploadZone.classList.add('border-blue-500', 'bg-blue-50');
|
||||||
|
});
|
||||||
|
|
||||||
|
uploadZone.addEventListener('dragleave', () => {
|
||||||
|
uploadZone.classList.remove('border-blue-500', 'bg-blue-50');
|
||||||
|
});
|
||||||
|
|
||||||
|
uploadZone.addEventListener('drop', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
uploadZone.classList.remove('border-blue-500', 'bg-blue-50');
|
||||||
|
this.handleFiles(e.dataTransfer.files);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadMedia() {
|
||||||
|
try {
|
||||||
|
document.getElementById('loading').style.display = 'block';
|
||||||
|
document.getElementById('empty-state').style.display = 'none';
|
||||||
|
document.getElementById('media-grid').style.display = 'none';
|
||||||
|
|
||||||
|
const response = await fetch('/api/media/list');
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
this.currentFiles = data.images || [];
|
||||||
|
this.applyFilters();
|
||||||
|
} else {
|
||||||
|
throw new Error(data.message || 'Failed to load media');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading media:', error);
|
||||||
|
this.showError('Ошибка загрузки медиа файлов');
|
||||||
|
} finally {
|
||||||
|
document.getElementById('loading').style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
applyFilters() {
|
||||||
|
const typeFilter = document.getElementById('file-type-filter').value;
|
||||||
|
const sizeFilter = document.getElementById('size-filter').value;
|
||||||
|
const searchQuery = document.getElementById('search-input').value.toLowerCase();
|
||||||
|
|
||||||
|
this.filteredFiles = this.currentFiles.filter(file => {
|
||||||
|
// Type filter
|
||||||
|
if (typeFilter && file.mimetype !== typeFilter) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Size filter
|
||||||
|
if (sizeFilter) {
|
||||||
|
const sizeInMB = file.size / (1024 * 1024);
|
||||||
|
if (sizeFilter === 'small' && sizeInMB >= 1) return false;
|
||||||
|
if (sizeFilter === 'medium' && (sizeInMB < 1 || sizeInMB > 5)) return false;
|
||||||
|
if (sizeFilter === 'large' && sizeInMB <= 5) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search filter
|
||||||
|
if (searchQuery && !file.filename.toLowerCase().includes(searchQuery)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.updateFileCount();
|
||||||
|
this.renderMedia();
|
||||||
|
}
|
||||||
|
|
||||||
|
updateFileCount() {
|
||||||
|
const total = this.currentFiles.length;
|
||||||
|
const filtered = this.filteredFiles.length;
|
||||||
|
const countText = filtered === total ?
|
||||||
|
`${total} файлов` :
|
||||||
|
`${filtered} из ${total} файлов`;
|
||||||
|
|
||||||
|
document.getElementById('file-count').textContent = countText;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderMedia() {
|
||||||
|
if (this.filteredFiles.length === 0) {
|
||||||
|
document.getElementById('empty-state').style.display = 'block';
|
||||||
|
document.getElementById('media-grid').style.display = 'none';
|
||||||
|
document.getElementById('media-list').style.display = 'none';
|
||||||
|
document.getElementById('pagination').style.display = 'none';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('empty-state').style.display = 'none';
|
||||||
|
|
||||||
|
const startIndex = (this.currentPage - 1) * this.itemsPerPage;
|
||||||
|
const endIndex = startIndex + this.itemsPerPage;
|
||||||
|
const pageFiles = this.filteredFiles.slice(startIndex, endIndex);
|
||||||
|
|
||||||
|
if (this.currentView === 'grid') {
|
||||||
|
this.renderGrid(pageFiles);
|
||||||
|
} else {
|
||||||
|
this.renderList(pageFiles);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updatePagination();
|
||||||
|
}
|
||||||
|
|
||||||
|
renderGrid(files) {
|
||||||
|
document.getElementById('media-grid').style.display = 'grid';
|
||||||
|
document.getElementById('media-list').style.display = 'none';
|
||||||
|
|
||||||
|
const grid = document.getElementById('media-grid');
|
||||||
|
grid.innerHTML = files.map(file => `
|
||||||
|
<div class="group relative bg-gray-100 rounded-lg overflow-hidden cursor-pointer hover:shadow-lg transition-shadow"
|
||||||
|
onclick="mediaGallery.openModal('${file.filename}')">
|
||||||
|
<div class="aspect-square">
|
||||||
|
<img src="${file.url}" alt="${file.filename}"
|
||||||
|
class="w-full h-full object-cover">
|
||||||
|
</div>
|
||||||
|
<div class="absolute inset-0 bg-black bg-opacity-0 group-hover:bg-opacity-25 transition-opacity flex items-center justify-center">
|
||||||
|
<div class="opacity-0 group-hover:opacity-100 transition-opacity">
|
||||||
|
<button class="bg-white bg-opacity-90 text-gray-800 px-3 py-2 rounded-lg mr-2"
|
||||||
|
onclick="event.stopPropagation(); mediaGallery.downloadFile('${file.filename}')">
|
||||||
|
<i class="fas fa-download"></i>
|
||||||
|
</button>
|
||||||
|
<button class="bg-red-500 bg-opacity-90 text-white px-3 py-2 rounded-lg"
|
||||||
|
onclick="event.stopPropagation(); mediaGallery.deleteFile('${file.filename}')">
|
||||||
|
<i class="fas fa-trash"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="absolute bottom-0 left-0 right-0 bg-black bg-opacity-75 text-white p-2">
|
||||||
|
<p class="text-xs truncate">${file.filename}</p>
|
||||||
|
<p class="text-xs text-gray-300">${this.formatFileSize(file.size)}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
renderList(files) {
|
||||||
|
document.getElementById('media-grid').style.display = 'none';
|
||||||
|
document.getElementById('media-list').style.display = 'block';
|
||||||
|
|
||||||
|
const list = document.getElementById('media-list');
|
||||||
|
list.innerHTML = files.map(file => `
|
||||||
|
<div class="flex items-center p-4 bg-gray-50 rounded-lg hover:bg-gray-100 cursor-pointer"
|
||||||
|
onclick="mediaGallery.openModal('${file.filename}')">
|
||||||
|
<div class="w-16 h-16 flex-shrink-0 mr-4">
|
||||||
|
<img src="${file.url}" alt="${file.filename}"
|
||||||
|
class="w-full h-full object-cover rounded">
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
<h4 class="text-sm font-medium text-gray-900 truncate">${file.filename}</h4>
|
||||||
|
<p class="text-sm text-gray-500">${this.formatFileSize(file.size)} • ${file.mimetype}</p>
|
||||||
|
<p class="text-xs text-gray-400">${new Date(file.uploadedAt).toLocaleDateString('ru-RU')}</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex space-x-2">
|
||||||
|
<button class="text-blue-600 hover:text-blue-800 p-2"
|
||||||
|
onclick="event.stopPropagation(); mediaGallery.downloadFile('${file.filename}')">
|
||||||
|
<i class="fas fa-download"></i>
|
||||||
|
</button>
|
||||||
|
<button class="text-red-600 hover:text-red-800 p-2"
|
||||||
|
onclick="event.stopPropagation(); mediaGallery.deleteFile('${file.filename}')">
|
||||||
|
<i class="fas fa-trash"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
setView(view) {
|
||||||
|
this.currentView = view;
|
||||||
|
|
||||||
|
// Update button states
|
||||||
|
document.getElementById('grid-view').classList.toggle('bg-blue-600', view === 'grid');
|
||||||
|
document.getElementById('grid-view').classList.toggle('text-white', view === 'grid');
|
||||||
|
document.getElementById('list-view').classList.toggle('bg-blue-600', view === 'list');
|
||||||
|
document.getElementById('list-view').classList.toggle('text-white', view === 'list');
|
||||||
|
|
||||||
|
this.renderMedia();
|
||||||
|
}
|
||||||
|
|
||||||
|
showUploadZone() {
|
||||||
|
document.getElementById('upload-zone').style.display = 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
hideUploadZone() {
|
||||||
|
document.getElementById('upload-zone').style.display = 'none';
|
||||||
|
document.getElementById('file-input').value = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleFiles(files) {
|
||||||
|
const validFiles = Array.from(files).filter(file => {
|
||||||
|
if (!file.type.startsWith('image/')) {
|
||||||
|
this.showError(`${file.name} не является изображением`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (file.size > 10 * 1024 * 1024) {
|
||||||
|
this.showError(`${file.name} слишком большой (максимум 10MB)`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (validFiles.length === 0) return;
|
||||||
|
|
||||||
|
this.hideUploadZone();
|
||||||
|
await this.uploadFiles(validFiles);
|
||||||
|
}
|
||||||
|
|
||||||
|
async uploadFiles(files) {
|
||||||
|
const progressContainer = document.getElementById('upload-progress');
|
||||||
|
const progressList = document.getElementById('progress-list');
|
||||||
|
|
||||||
|
progressContainer.style.display = 'block';
|
||||||
|
progressList.innerHTML = '';
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
const progressItem = this.createProgressItem(file);
|
||||||
|
progressList.appendChild(progressItem);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.uploadSingleFile(file, progressItem);
|
||||||
|
} catch (error) {
|
||||||
|
this.updateProgressItem(progressItem, 'error', error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
progressContainer.style.display = 'none';
|
||||||
|
this.loadMedia();
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
createProgressItem(file) {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.className = 'flex items-center justify-between p-3 bg-gray-50 rounded';
|
||||||
|
div.innerHTML = `
|
||||||
|
<div class="flex items-center space-x-3">
|
||||||
|
<i class="fas fa-image text-gray-400"></i>
|
||||||
|
<span class="text-sm text-gray-900">${file.name}</span>
|
||||||
|
<span class="text-xs text-gray-500">${this.formatFileSize(file.size)}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center space-x-3">
|
||||||
|
<div class="w-32 bg-gray-200 rounded-full h-2">
|
||||||
|
<div class="bg-blue-600 h-2 rounded-full progress-bar" style="width: 0%"></div>
|
||||||
|
</div>
|
||||||
|
<span class="text-sm text-gray-600 status">0%</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
return div;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateProgressItem(item, status, message = '') {
|
||||||
|
const statusElement = item.querySelector('.status');
|
||||||
|
const progressBar = item.querySelector('.progress-bar');
|
||||||
|
|
||||||
|
if (status === 'error') {
|
||||||
|
statusElement.textContent = 'Ошибка';
|
||||||
|
statusElement.className = 'text-sm text-red-600 status';
|
||||||
|
progressBar.className = 'bg-red-600 h-2 rounded-full progress-bar';
|
||||||
|
progressBar.style.width = '100%';
|
||||||
|
} else if (status === 'success') {
|
||||||
|
statusElement.textContent = 'Готово';
|
||||||
|
statusElement.className = 'text-sm text-green-600 status';
|
||||||
|
progressBar.className = 'bg-green-600 h-2 rounded-full progress-bar';
|
||||||
|
progressBar.style.width = '100%';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async uploadSingleFile(file, progressItem) {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('images', file);
|
||||||
|
|
||||||
|
const xhr = new XMLHttpRequest();
|
||||||
|
const progressBar = progressItem.querySelector('.progress-bar');
|
||||||
|
const status = progressItem.querySelector('.status');
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
xhr.upload.addEventListener('progress', (e) => {
|
||||||
|
if (e.lengthComputable) {
|
||||||
|
const percentComplete = (e.loaded / e.total) * 100;
|
||||||
|
progressBar.style.width = percentComplete + '%';
|
||||||
|
status.textContent = Math.round(percentComplete) + '%';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
xhr.addEventListener('load', () => {
|
||||||
|
if (xhr.status === 200) {
|
||||||
|
const response = JSON.parse(xhr.responseText);
|
||||||
|
if (response.success) {
|
||||||
|
this.updateProgressItem(progressItem, 'success');
|
||||||
|
resolve();
|
||||||
|
} else {
|
||||||
|
reject(new Error(response.message));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
reject(new Error('Upload failed'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
xhr.addEventListener('error', () => {
|
||||||
|
reject(new Error('Network error'));
|
||||||
|
});
|
||||||
|
|
||||||
|
xhr.open('POST', '/api/media/upload-multiple');
|
||||||
|
xhr.send(formData);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
openModal(filename) {
|
||||||
|
const file = this.currentFiles.find(f => f.filename === filename);
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
this.currentFile = file;
|
||||||
|
|
||||||
|
document.getElementById('modal-title').textContent = file.filename;
|
||||||
|
document.getElementById('modal-image').src = file.url;
|
||||||
|
document.getElementById('modal-filename').value = file.filename;
|
||||||
|
document.getElementById('modal-url').value = window.location.origin + file.url;
|
||||||
|
document.getElementById('modal-size').textContent = this.formatFileSize(file.size);
|
||||||
|
document.getElementById('modal-type').textContent = file.mimetype;
|
||||||
|
document.getElementById('modal-date').textContent = new Date(file.uploadedAt).toLocaleDateString('ru-RU');
|
||||||
|
|
||||||
|
// Load image to get dimensions
|
||||||
|
const img = new Image();
|
||||||
|
img.onload = () => {
|
||||||
|
document.getElementById('modal-width').textContent = img.width + 'px';
|
||||||
|
document.getElementById('modal-height').textContent = img.height + 'px';
|
||||||
|
};
|
||||||
|
img.src = file.url;
|
||||||
|
|
||||||
|
document.getElementById('preview-modal').style.display = 'flex';
|
||||||
|
}
|
||||||
|
|
||||||
|
closeModal() {
|
||||||
|
document.getElementById('preview-modal').style.display = 'none';
|
||||||
|
this.currentFile = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteFile(filename) {
|
||||||
|
if (!confirm(`Вы уверены, что хотите удалить файл "${filename}"?`)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/media/${filename}`, {
|
||||||
|
method: 'DELETE'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
this.showSuccess('Файл удален');
|
||||||
|
this.loadMedia();
|
||||||
|
if (this.currentFile && this.currentFile.filename === filename) {
|
||||||
|
this.closeModal();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error('Failed to delete file');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error deleting file:', error);
|
||||||
|
this.showError('Ошибка удаления файла');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadFile(filename) {
|
||||||
|
const file = filename ?
|
||||||
|
this.currentFiles.find(f => f.filename === filename) :
|
||||||
|
this.currentFile;
|
||||||
|
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = file.url;
|
||||||
|
link.download = file.filename;
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
}
|
||||||
|
|
||||||
|
updatePagination() {
|
||||||
|
const totalPages = Math.ceil(this.filteredFiles.length / this.itemsPerPage);
|
||||||
|
|
||||||
|
if (totalPages <= 1) {
|
||||||
|
document.getElementById('pagination').style.display = 'none';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('pagination').style.display = 'block';
|
||||||
|
|
||||||
|
// Update prev/next buttons
|
||||||
|
document.getElementById('prev-page').disabled = this.currentPage === 1;
|
||||||
|
document.getElementById('next-page').disabled = this.currentPage === totalPages;
|
||||||
|
|
||||||
|
// Update page numbers
|
||||||
|
const pageNumbers = document.getElementById('page-numbers');
|
||||||
|
pageNumbers.innerHTML = '';
|
||||||
|
|
||||||
|
for (let i = 1; i <= totalPages; i++) {
|
||||||
|
if (i === 1 || i === totalPages || (i >= this.currentPage - 2 && i <= this.currentPage + 2)) {
|
||||||
|
const button = document.createElement('button');
|
||||||
|
button.className = `px-3 py-2 rounded ${
|
||||||
|
i === this.currentPage ? 'bg-blue-600 text-white' : 'bg-gray-200 text-gray-600 hover:bg-gray-300'
|
||||||
|
}`;
|
||||||
|
button.textContent = i;
|
||||||
|
button.onclick = () => this.goToPage(i);
|
||||||
|
pageNumbers.appendChild(button);
|
||||||
|
} else if (i === this.currentPage - 3 || i === this.currentPage + 3) {
|
||||||
|
const span = document.createElement('span');
|
||||||
|
span.className = 'px-2 py-2 text-gray-400';
|
||||||
|
span.textContent = '...';
|
||||||
|
pageNumbers.appendChild(span);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
goToPage(page) {
|
||||||
|
this.currentPage = page;
|
||||||
|
this.renderMedia();
|
||||||
|
}
|
||||||
|
|
||||||
|
formatFileSize(bytes) {
|
||||||
|
if (bytes === 0) return '0 B';
|
||||||
|
const k = 1024;
|
||||||
|
const sizes = ['B', 'KB', 'MB', 'GB'];
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
showError(message) {
|
||||||
|
this.showNotification(message, 'error');
|
||||||
|
}
|
||||||
|
|
||||||
|
showSuccess(message) {
|
||||||
|
this.showNotification(message, 'success');
|
||||||
|
}
|
||||||
|
|
||||||
|
showNotification(message, type = 'info') {
|
||||||
|
const notification = document.createElement('div');
|
||||||
|
notification.className = `fixed top-4 right-4 px-6 py-3 rounded-lg text-white z-50 ${
|
||||||
|
type === 'success' ? 'bg-green-500' :
|
||||||
|
type === 'error' ? 'bg-red-500' : 'bg-blue-500'
|
||||||
|
}`;
|
||||||
|
notification.textContent = message;
|
||||||
|
|
||||||
|
document.body.appendChild(notification);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
notification.remove();
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Global functions for modal
|
||||||
|
function copyToClipboard() {
|
||||||
|
const urlInput = document.getElementById('modal-url');
|
||||||
|
urlInput.select();
|
||||||
|
document.execCommand('copy');
|
||||||
|
mediaGallery.showSuccess('URL скопирован в буфер обмена');
|
||||||
|
}
|
||||||
|
|
||||||
|
function downloadFile() {
|
||||||
|
mediaGallery.downloadFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteFile() {
|
||||||
|
if (mediaGallery.currentFile) {
|
||||||
|
mediaGallery.deleteFile(mediaGallery.currentFile.filename);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize
|
||||||
|
let mediaGallery;
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
mediaGallery = new MediaGallery();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||