AdminLTE3

This commit is contained in:
2025-10-26 22:14:47 +09:00
parent 291fc63a4c
commit 9974811a3e
226 changed files with 88284 additions and 3406 deletions

View 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 теперь полностью функциональна с корейской локализацией
- Все зависимости загружаются правильно

View 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 теперь полностью функциональна с корейской локализацией
- Все зависимости загружаются правильно

View 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 интегрирована и работает со всеми существующими функциями сайта.

View 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 интегрирована и работает со всеми существующими функциями сайта.

View 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` и при желании интегрируйте в основную админку!

View 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` и при желании интегрируйте в основную админку!

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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);
}

View 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);
}

View 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

View 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

View File

@@ -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

View File

@@ -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

View 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

View 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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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">&lt;/&gt;</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

View File

@@ -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">&lt;/&gt;</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

View 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
});

View 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
});

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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();

View 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();

View 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();

View 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();

View 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();

View 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();

View 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();

View 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();

View 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();

View 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();

View 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();

View 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 rolebased access.",
shortDescription: "Realtime analytics with rolebased 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>

View 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>

View 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>&times;</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>

View 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>&times;</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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>&copy; 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>

View 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>&copy; 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>

View 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>

View 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>

View 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>&copy; 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>

View 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>&copy; 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>

View 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>&copy; 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>

View 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>&copy; 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>

View 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>&copy; 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>

View 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>&copy; 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>

View 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>&copy; 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>

View 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>&copy; 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>

View 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">Маленький (&lt; 1MB)</option>
<option value="medium">Средний (1-5MB)</option>
<option value="large">Большой (&gt; 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>&times;</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>

Some files were not shown because too many files have changed in this diff Show More