feat: Реализован полный CRUD для админ-панели и улучшена функциональность
- Portfolio CRUD: добавление, редактирование, удаление, переключение публикации - Services CRUD: полное управление услугами с возможностью активации/деактивации - Banner system: новая модель Banner с CRUD операциями и аналитикой кликов - Telegram integration: расширенные настройки бота, обнаружение чатов, отправка сообщений - Media management: улучшенная загрузка файлов с оптимизацией изображений и превью - UI improvements: обновлённые админ-панели с rich-text редактором и drag&drop загрузкой - Database: добавлена таблица banners с полями для баннеров и аналитики
This commit is contained in:
@@ -18,14 +18,14 @@
|
||||
<body class="font-sans dark:bg-gray-900 dark:text-gray-100">
|
||||
<%- include('partials/navigation') %>
|
||||
|
||||
<!-- Hero Section -->
|
||||
<section class="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 py-20 hero-section">
|
||||
<!-- Hero Section - Компактный -->
|
||||
<section class="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 hero-section-compact">
|
||||
<div class="absolute inset-0 bg-black opacity-50 dark:opacity-70"></div>
|
||||
<div class="relative max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
|
||||
<h1 class="text-5xl md:text-6xl font-bold text-white mb-6">
|
||||
<h1 class="text-4xl md:text-5xl font-bold text-white mb-4">
|
||||
<%- __('about.hero.title') %>
|
||||
</h1>
|
||||
<p class="text-xl text-gray-300 dark:text-gray-200 max-w-3xl mx-auto">
|
||||
<p class="text-lg text-gray-300 dark:text-gray-200 max-w-2xl mx-auto">
|
||||
<%- __('about.hero.subtitle') %>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
664
views/admin/banner-editor.ejs
Normal file
664
views/admin/banner-editor.ejs
Normal file
@@ -0,0 +1,664 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Редактор Баннеров - SmartSolTech Admin</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.4.0/css/all.min.css">
|
||||
|
||||
<!-- Custom CSS -->
|
||||
<link rel="stylesheet" href="/css/main.css">
|
||||
<link rel="stylesheet" href="/css/fixes.css">
|
||||
|
||||
<style>
|
||||
.upload-zone {
|
||||
border: 2px dashed #d1d5db;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.upload-zone.dragover {
|
||||
border-color: #3b82f6;
|
||||
background-color: #eff6ff;
|
||||
}
|
||||
.image-preview {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border-radius: 8px;
|
||||
}
|
||||
.image-preview:hover .overlay {
|
||||
opacity: 1;
|
||||
}
|
||||
.overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-gray-100">
|
||||
<!-- Admin Header -->
|
||||
<header class="bg-white shadow-sm border-b">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex justify-between items-center h-16">
|
||||
<div class="flex items-center">
|
||||
<h1 class="text-xl font-semibold text-gray-900">
|
||||
<i class="fas fa-cogs mr-2"></i>
|
||||
SmartSolTech Admin
|
||||
</h1>
|
||||
</div>
|
||||
<div class="flex items-center space-x-4">
|
||||
<span class="text-sm text-gray-600">
|
||||
Добро пожаловать, <%= user ? user.name : 'Admin' %>!
|
||||
</span>
|
||||
<a href="/" class="text-gray-500 hover:text-gray-700">
|
||||
<i class="fas fa-external-link-alt mr-1"></i>
|
||||
Посмотреть сайт
|
||||
</a>
|
||||
<form action="/admin/logout" method="post" class="inline">
|
||||
<button type="submit" class="text-red-600 hover:text-red-800">
|
||||
<i class="fas fa-sign-out-alt mr-1"></i>
|
||||
Выход
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="flex">
|
||||
<!-- Admin Sidebar -->
|
||||
<aside class="w-64 bg-white shadow-sm admin-sidebar min-h-screen">
|
||||
<nav class="mt-5 px-2">
|
||||
<div class="space-y-1">
|
||||
<a href="/admin/dashboard" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-600 hover:bg-gray-50 hover:text-gray-900">
|
||||
<i class="fas fa-tachometer-alt mr-3"></i>
|
||||
Панель управления
|
||||
</a>
|
||||
<a href="/admin/portfolio" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-600 hover:bg-gray-50 hover:text-gray-900">
|
||||
<i class="fas fa-briefcase mr-3"></i>
|
||||
Портфолио
|
||||
</a>
|
||||
<a href="/admin/services" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-600 hover:bg-gray-50 hover:text-gray-900">
|
||||
<i class="fas fa-cog mr-3"></i>
|
||||
Услуги
|
||||
</a>
|
||||
<a href="/admin/contacts" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-600 hover:bg-gray-50 hover:text-gray-900">
|
||||
<i class="fas fa-envelope mr-3"></i>
|
||||
Сообщения
|
||||
</a>
|
||||
<a href="/admin/media" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-600 hover:bg-gray-50 hover:text-gray-900">
|
||||
<i class="fas fa-images mr-3"></i>
|
||||
Медиа
|
||||
</a>
|
||||
<a href="/admin/settings" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-600 hover:bg-gray-50 hover:text-gray-900">
|
||||
<i class="fas fa-cogs mr-3"></i>
|
||||
Настройки
|
||||
</a>
|
||||
<a href="/admin/telegram" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-600 hover:bg-gray-50 hover:text-gray-900">
|
||||
<i class="fab fa-telegram mr-3"></i>
|
||||
Telegram Bot
|
||||
</a>
|
||||
<a href="/admin/banner-editor" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md bg-blue-100 text-blue-700">
|
||||
<i class="fas fa-paint-brush mr-3"></i>
|
||||
Редактор баннеров
|
||||
</a>
|
||||
</div>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="flex-1 p-8">
|
||||
<div class="space-y-6">
|
||||
<!-- Header -->
|
||||
<div class="bg-white shadow rounded-lg p-6">
|
||||
<h1 class="text-2xl font-bold text-gray-900 flex items-center">
|
||||
<i class="fas fa-paint-brush mr-3 text-blue-600"></i>
|
||||
Редактор Баннеров
|
||||
</h1>
|
||||
<p class="mt-2 text-gray-600">Создание и редактирование баннеров для сайта</p>
|
||||
</div>
|
||||
|
||||
<!-- Controls -->
|
||||
<div class="bg-white shadow rounded-lg p-6">
|
||||
<div class="flex justify-between items-center">
|
||||
<h3 class="text-lg font-medium text-gray-900">Инструменты</h3>
|
||||
<div class="flex space-x-4">
|
||||
<button id="refresh-images" class="px-4 py-2 bg-gray-500 text-white rounded hover:bg-gray-600 transition-colors">
|
||||
<i class="fas fa-sync-alt mr-2"></i>
|
||||
Обновить
|
||||
</button>
|
||||
<button id="upload-modal-btn" class="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors">
|
||||
<i class="fas fa-plus mr-2"></i>
|
||||
Загрузить Изображения
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Banner Editor -->
|
||||
<div class="bg-white shadow rounded-lg p-6">
|
||||
<!-- Banner Types Tabs -->
|
||||
<div class="mb-8">
|
||||
<div class="border-b border-gray-200 dark:border-gray-700">
|
||||
<nav class="-mb-px flex space-x-8">
|
||||
<button class="banner-tab active py-2 px-1 border-b-2 border-blue-500 font-medium text-sm text-blue-600" data-page="home">
|
||||
Главная страница
|
||||
</button>
|
||||
<button class="banner-tab py-2 px-1 border-b-2 border-transparent font-medium text-sm text-gray-500 hover:text-gray-700 hover:border-gray-300" data-page="about">
|
||||
О нас
|
||||
</button>
|
||||
<button class="banner-tab py-2 px-1 border-b-2 border-transparent font-medium text-sm text-gray-500 hover:text-gray-700 hover:border-gray-300" data-page="services">
|
||||
Услуги
|
||||
</button>
|
||||
<button class="banner-tab py-2 px-1 border-b-2 border-transparent font-medium text-sm text-gray-500 hover:text-gray-700 hover:border-gray-300" data-page="portfolio">
|
||||
Портфолио
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Current Banner Display -->
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-lg p-6 mb-8">
|
||||
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-4">
|
||||
Текущий баннер: <span id="current-page">Главная страница</span>
|
||||
</h2>
|
||||
<div id="current-banner" class="relative">
|
||||
<div class="w-full h-64 bg-gradient-to-br from-blue-900 via-purple-900 to-indigo-900 rounded-lg flex items-center justify-center">
|
||||
<div class="text-center text-white">
|
||||
<h3 class="text-4xl font-bold mb-2">Текущий Баннер</h3>
|
||||
<p class="text-xl opacity-90">Нажмите на изображение ниже, чтобы заменить</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4 flex justify-between items-center">
|
||||
<div class="text-sm text-gray-600 dark:text-gray-400">
|
||||
<span id="banner-info">Используется CSS градиент</span>
|
||||
</div>
|
||||
<button id="remove-banner" class="px-3 py-1 bg-red-500 text-white rounded hover:bg-red-600 transition-colors text-sm" style="display: none;">
|
||||
<i class="fas fa-trash mr-1"></i>
|
||||
Удалить
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Image Gallery -->
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-lg p-6">
|
||||
<h2 class="text-xl font-semibold text-gray-900 dark:text-white mb-6">
|
||||
Галерея изображений
|
||||
</h2>
|
||||
|
||||
<div id="loading" class="text-center py-8" style="display: none;">
|
||||
<div class="inline-block animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500"></div>
|
||||
<p class="mt-2 text-gray-600 dark:text-gray-400">Загрузка...</p>
|
||||
</div>
|
||||
|
||||
<div id="images-grid" class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
|
||||
<!-- Изображения будут загружены динамически -->
|
||||
</div>
|
||||
|
||||
<div id="no-images" class="text-center py-8" style="display: none;">
|
||||
<i class="fas fa-images text-4xl text-gray-400 mb-4"></i>
|
||||
<p class="text-gray-600 dark:text-gray-400">Нет загруженных изображений</p>
|
||||
<button class="mt-4 px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors" onclick="document.getElementById('upload-modal-btn').click()">
|
||||
Загрузить первое изображение
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Upload Modal -->
|
||||
<div id="upload-modal" class="fixed inset-0 bg-black bg-opacity-50 z-50 hidden">
|
||||
<div class="flex items-center justify-center min-h-screen p-4">
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-lg max-w-2xl w-full max-h-[90vh] overflow-y-auto">
|
||||
<div class="p-6">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h3 class="text-xl font-semibold text-gray-900 dark:text-white">Загрузить изображения</h3>
|
||||
<button id="close-modal" class="text-gray-400 hover:text-gray-600">
|
||||
<i class="fas fa-times text-xl"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Upload Zone -->
|
||||
<div id="upload-zone" class="upload-zone rounded-lg p-8 text-center mb-6">
|
||||
<div class="mb-4">
|
||||
<i class="fas fa-cloud-upload-alt text-4xl text-gray-400 mb-2"></i>
|
||||
<p class="text-lg text-gray-600 dark:text-gray-400 mb-2">
|
||||
Перетащите изображения сюда или нажмите для выбора
|
||||
</p>
|
||||
<p class="text-sm text-gray-500">
|
||||
Поддерживаются: JPG, PNG, GIF, WebP (максимум 10MB каждое)
|
||||
</p>
|
||||
</div>
|
||||
<input type="file" id="file-input" multiple accept="image/*" class="hidden">
|
||||
<button type="button" onclick="document.getElementById('file-input').click()" class="px-6 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors">
|
||||
Выбрать файлы
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Upload Progress -->
|
||||
<div id="upload-progress" class="hidden mb-6">
|
||||
<div class="bg-gray-200 rounded-full h-2 mb-2">
|
||||
<div id="progress-bar" class="bg-blue-500 h-2 rounded-full transition-all duration-300" style="width: 0%"></div>
|
||||
</div>
|
||||
<p id="progress-text" class="text-sm text-gray-600 dark:text-gray-400 text-center">Загрузка...</p>
|
||||
</div>
|
||||
|
||||
<!-- Preview Area -->
|
||||
<div id="preview-area" class="grid grid-cols-2 md:grid-cols-3 gap-4 mb-6" style="display: none;">
|
||||
<!-- Previews will be added here -->
|
||||
</div>
|
||||
|
||||
<!-- Upload Button -->
|
||||
<div class="flex justify-end">
|
||||
<button id="upload-btn" class="px-6 py-2 bg-green-500 text-white rounded hover:bg-green-600 transition-colors disabled:opacity-50 disabled:cursor-not-allowed" disabled>
|
||||
<i class="fas fa-upload mr-2"></i>
|
||||
Загрузить
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Scripts -->
|
||||
<script>
|
||||
class BannerEditor {
|
||||
constructor() {
|
||||
this.currentPage = 'home';
|
||||
this.selectedFiles = [];
|
||||
this.bannerSettings = {
|
||||
home: { type: 'gradient', image: null },
|
||||
about: { type: 'gradient', image: null },
|
||||
services: { type: 'gradient', image: null },
|
||||
portfolio: { type: 'gradient', image: null }
|
||||
};
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.loadImages();
|
||||
this.setupEventListeners();
|
||||
this.loadBannerSettings();
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
// Tab switching
|
||||
document.querySelectorAll('.banner-tab').forEach(tab => {
|
||||
tab.addEventListener('click', (e) => {
|
||||
const page = e.target.dataset.page;
|
||||
this.switchPage(page);
|
||||
});
|
||||
});
|
||||
|
||||
// Upload modal
|
||||
document.getElementById('upload-modal-btn').addEventListener('click', () => {
|
||||
document.getElementById('upload-modal').classList.remove('hidden');
|
||||
});
|
||||
|
||||
document.getElementById('close-modal').addEventListener('click', () => {
|
||||
this.closeModal();
|
||||
});
|
||||
|
||||
// File upload
|
||||
const fileInput = document.getElementById('file-input');
|
||||
const uploadZone = document.getElementById('upload-zone');
|
||||
|
||||
fileInput.addEventListener('change', (e) => {
|
||||
this.handleFiles(e.target.files);
|
||||
});
|
||||
|
||||
// Drag and drop
|
||||
uploadZone.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
uploadZone.classList.add('dragover');
|
||||
});
|
||||
|
||||
uploadZone.addEventListener('dragleave', () => {
|
||||
uploadZone.classList.remove('dragover');
|
||||
});
|
||||
|
||||
uploadZone.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
uploadZone.classList.remove('dragover');
|
||||
this.handleFiles(e.dataTransfer.files);
|
||||
});
|
||||
|
||||
// Upload button
|
||||
document.getElementById('upload-btn').addEventListener('click', () => {
|
||||
this.uploadFiles();
|
||||
});
|
||||
|
||||
// Refresh images
|
||||
document.getElementById('refresh-images').addEventListener('click', () => {
|
||||
this.loadImages();
|
||||
});
|
||||
}
|
||||
|
||||
switchPage(page) {
|
||||
this.currentPage = page;
|
||||
|
||||
// Update active tab
|
||||
document.querySelectorAll('.banner-tab').forEach(tab => {
|
||||
tab.classList.remove('active', 'border-blue-500', 'text-blue-600');
|
||||
tab.classList.add('border-transparent', 'text-gray-500');
|
||||
});
|
||||
|
||||
document.querySelector(`[data-page="${page}"]`).classList.add('active', 'border-blue-500', 'text-blue-600');
|
||||
document.querySelector(`[data-page="${page}"]`).classList.remove('border-transparent', 'text-gray-500');
|
||||
|
||||
// Update current page display
|
||||
const pageNames = {
|
||||
home: 'Главная страница',
|
||||
about: 'О нас',
|
||||
services: 'Услуги',
|
||||
portfolio: 'Портфолио'
|
||||
};
|
||||
|
||||
document.getElementById('current-page').textContent = pageNames[page];
|
||||
this.updateCurrentBanner();
|
||||
}
|
||||
|
||||
updateCurrentBanner() {
|
||||
const banner = this.bannerSettings[this.currentPage];
|
||||
const bannerElement = document.getElementById('current-banner').querySelector('div');
|
||||
const infoElement = document.getElementById('banner-info');
|
||||
const removeBtn = document.getElementById('remove-banner');
|
||||
|
||||
if (banner.image) {
|
||||
bannerElement.style.backgroundImage = `url(${banner.image})`;
|
||||
bannerElement.style.backgroundSize = 'cover';
|
||||
bannerElement.style.backgroundPosition = 'center';
|
||||
bannerElement.innerHTML = '';
|
||||
infoElement.textContent = `Изображение: ${banner.image.split('/').pop()}`;
|
||||
removeBtn.style.display = 'block';
|
||||
} else {
|
||||
bannerElement.style.backgroundImage = '';
|
||||
bannerElement.className = 'w-full h-64 bg-gradient-to-br from-blue-900 via-purple-900 to-indigo-900 rounded-lg flex items-center justify-center';
|
||||
bannerElement.innerHTML = `
|
||||
<div class="text-center text-white">
|
||||
<h3 class="text-4xl font-bold mb-2">Текущий Баннер</h3>
|
||||
<p class="text-xl opacity-90">Нажмите на изображение ниже, чтобы заменить</p>
|
||||
</div>
|
||||
`;
|
||||
infoElement.textContent = 'Используется CSS градиент';
|
||||
removeBtn.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
async loadImages() {
|
||||
const loading = document.getElementById('loading');
|
||||
const grid = document.getElementById('images-grid');
|
||||
const noImages = document.getElementById('no-images');
|
||||
|
||||
loading.style.display = 'block';
|
||||
grid.innerHTML = '';
|
||||
noImages.style.display = 'none';
|
||||
|
||||
try {
|
||||
const response = await fetch('/media/list');
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success && data.images.length > 0) {
|
||||
grid.innerHTML = '';
|
||||
data.images.forEach(image => {
|
||||
this.addImageToGrid(image);
|
||||
});
|
||||
} else {
|
||||
noImages.style.display = 'block';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading images:', error);
|
||||
this.showNotification('Ошибка загрузки изображений', 'error');
|
||||
} finally {
|
||||
loading.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
addImageToGrid(image) {
|
||||
const grid = document.getElementById('images-grid');
|
||||
const imageElement = document.createElement('div');
|
||||
imageElement.className = 'image-preview bg-gray-200 rounded-lg cursor-pointer';
|
||||
|
||||
imageElement.innerHTML = `
|
||||
<img src="${image.url}" alt="${image.filename}" class="w-full h-32 object-cover rounded-lg">
|
||||
<div class="overlay">
|
||||
<div class="flex space-x-2">
|
||||
<button class="use-image px-3 py-1 bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors text-sm" data-url="${image.url}">
|
||||
<i class="fas fa-check mr-1"></i>
|
||||
Использовать
|
||||
</button>
|
||||
<button class="delete-image px-3 py-1 bg-red-500 text-white rounded hover:bg-red-600 transition-colors text-sm" data-filename="${image.filename}">
|
||||
<i class="fas fa-trash mr-1"></i>
|
||||
Удалить
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-2">
|
||||
<p class="text-xs text-gray-600 truncate">${image.filename}</p>
|
||||
<p class="text-xs text-gray-500">${this.formatFileSize(image.size)}</p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Add event listeners
|
||||
imageElement.querySelector('.use-image').addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
this.setImageAsBanner(image.url);
|
||||
});
|
||||
|
||||
imageElement.querySelector('.delete-image').addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
this.deleteImage(image.filename);
|
||||
});
|
||||
|
||||
grid.appendChild(imageElement);
|
||||
}
|
||||
|
||||
setImageAsBanner(imageUrl) {
|
||||
this.bannerSettings[this.currentPage] = {
|
||||
type: 'image',
|
||||
image: imageUrl
|
||||
};
|
||||
this.updateCurrentBanner();
|
||||
this.saveBannerSettings();
|
||||
this.showNotification('Баннер обновлен!', 'success');
|
||||
}
|
||||
|
||||
async deleteImage(filename) {
|
||||
if (!confirm('Вы уверены, что хотите удалить это изображение?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/media/${filename}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
this.loadImages();
|
||||
this.showNotification('Изображение удалено', 'success');
|
||||
} else {
|
||||
throw new Error('Failed to delete image');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error deleting image:', error);
|
||||
this.showNotification('Ошибка удаления изображения', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
handleFiles(files) {
|
||||
this.selectedFiles = Array.from(files).filter(file => {
|
||||
if (file.type.startsWith('image/')) {
|
||||
if (file.size <= 10 * 1024 * 1024) { // 10MB
|
||||
return true;
|
||||
} else {
|
||||
this.showNotification(`Файл ${file.name} слишком большой (максимум 10MB)`, 'error');
|
||||
}
|
||||
} else {
|
||||
this.showNotification(`Файл ${file.name} не является изображением`, 'error');
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
this.showPreviews();
|
||||
document.getElementById('upload-btn').disabled = this.selectedFiles.length === 0;
|
||||
}
|
||||
|
||||
showPreviews() {
|
||||
const previewArea = document.getElementById('preview-area');
|
||||
previewArea.innerHTML = '';
|
||||
previewArea.style.display = this.selectedFiles.length > 0 ? 'grid' : 'none';
|
||||
|
||||
this.selectedFiles.forEach((file, index) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
const preview = document.createElement('div');
|
||||
preview.className = 'relative';
|
||||
preview.innerHTML = `
|
||||
<img src="${e.target.result}" alt="${file.name}" class="w-full h-24 object-cover rounded">
|
||||
<button class="absolute top-1 right-1 bg-red-500 text-white rounded-full w-6 h-6 flex items-center justify-center text-xs hover:bg-red-600" onclick="bannerEditor.removePreview(${index})">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
<p class="text-xs text-gray-600 mt-1 truncate">${file.name}</p>
|
||||
`;
|
||||
previewArea.appendChild(preview);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
}
|
||||
|
||||
removePreview(index) {
|
||||
this.selectedFiles.splice(index, 1);
|
||||
this.showPreviews();
|
||||
document.getElementById('upload-btn').disabled = this.selectedFiles.length === 0;
|
||||
}
|
||||
|
||||
async uploadFiles() {
|
||||
if (this.selectedFiles.length === 0) return;
|
||||
|
||||
const progressContainer = document.getElementById('upload-progress');
|
||||
const progressBar = document.getElementById('progress-bar');
|
||||
const progressText = document.getElementById('progress-text');
|
||||
const uploadBtn = document.getElementById('upload-btn');
|
||||
|
||||
progressContainer.classList.remove('hidden');
|
||||
uploadBtn.disabled = true;
|
||||
|
||||
const formData = new FormData();
|
||||
this.selectedFiles.forEach(file => {
|
||||
formData.append('images', file);
|
||||
});
|
||||
|
||||
try {
|
||||
const xhr = new XMLHttpRequest();
|
||||
|
||||
xhr.upload.addEventListener('progress', (e) => {
|
||||
if (e.lengthComputable) {
|
||||
const percentComplete = (e.loaded / e.total) * 100;
|
||||
progressBar.style.width = percentComplete + '%';
|
||||
progressText.textContent = `Загрузка... ${Math.round(percentComplete)}%`;
|
||||
}
|
||||
});
|
||||
|
||||
xhr.addEventListener('load', () => {
|
||||
if (xhr.status === 200) {
|
||||
const response = JSON.parse(xhr.responseText);
|
||||
if (response.success) {
|
||||
this.showNotification('Изображения загружены успешно!', 'success');
|
||||
this.closeModal();
|
||||
this.loadImages();
|
||||
} else {
|
||||
throw new Error(response.message);
|
||||
}
|
||||
} else {
|
||||
throw new Error('Upload failed');
|
||||
}
|
||||
});
|
||||
|
||||
xhr.addEventListener('error', () => {
|
||||
throw new Error('Network error');
|
||||
});
|
||||
|
||||
xhr.open('POST', '/media/upload-multiple');
|
||||
xhr.send(formData);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Upload error:', error);
|
||||
this.showNotification('Ошибка загрузки изображений', 'error');
|
||||
} finally {
|
||||
progressContainer.classList.add('hidden');
|
||||
uploadBtn.disabled = false;
|
||||
progressBar.style.width = '0%';
|
||||
}
|
||||
}
|
||||
|
||||
closeModal() {
|
||||
document.getElementById('upload-modal').classList.add('hidden');
|
||||
this.selectedFiles = [];
|
||||
document.getElementById('preview-area').style.display = 'none';
|
||||
document.getElementById('upload-btn').disabled = true;
|
||||
document.getElementById('upload-progress').classList.add('hidden');
|
||||
}
|
||||
|
||||
loadBannerSettings() {
|
||||
const saved = localStorage.getItem('bannerSettings');
|
||||
if (saved) {
|
||||
this.bannerSettings = JSON.parse(saved);
|
||||
}
|
||||
this.updateCurrentBanner();
|
||||
}
|
||||
|
||||
saveBannerSettings() {
|
||||
localStorage.setItem('bannerSettings', JSON.stringify(this.bannerSettings));
|
||||
}
|
||||
|
||||
formatFileSize(bytes) {
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
const k = 1024;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
showNotification(message, type = 'info') {
|
||||
// Simple notification - you can enhance this with a proper notification system
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize when page loads
|
||||
let bannerEditor;
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
bannerEditor = new BannerEditor();
|
||||
});
|
||||
</script>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
117
views/admin/contacts/list.ejs
Normal file
117
views/admin/contacts/list.ejs
Normal file
@@ -0,0 +1,117 @@
|
||||
<!-- Contacts List -->
|
||||
<div class="bg-white shadow rounded-lg">
|
||||
<div class="px-4 py-5 sm:px-6 border-b border-gray-200">
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="text-lg leading-6 font-medium text-gray-900">
|
||||
<i class="fas fa-envelope mr-2"></i>
|
||||
Управление сообщениями
|
||||
</h3>
|
||||
<div class="flex items-center space-x-2">
|
||||
<!-- Status Filter -->
|
||||
<select id="statusFilter" class="border-gray-300 rounded-md shadow-sm text-sm">
|
||||
<option value="all" <%= currentStatus === 'all' ? 'selected' : '' %>>Все статусы</option>
|
||||
<option value="new" <%= currentStatus === 'new' ? 'selected' : '' %>>Новые</option>
|
||||
<option value="in_progress" <%= currentStatus === 'in_progress' ? 'selected' : '' %>>В работе</option>
|
||||
<option value="completed" <%= currentStatus === 'completed' ? 'selected' : '' %>>Завершенные</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white shadow overflow-hidden sm:rounded-md">
|
||||
<ul role="list" class="divide-y divide-gray-200">
|
||||
<% if (contacts && contacts.length > 0) { %>
|
||||
<% contacts.forEach(contact => { %>
|
||||
<li>
|
||||
<a href="/admin/contacts/<%= contact.id %>" class="block hover:bg-gray-50">
|
||||
<div class="px-4 py-4 flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<% if (!contact.isRead) { %>
|
||||
<div class="h-2 w-2 bg-blue-600 rounded-full"></div>
|
||||
<% } else { %>
|
||||
<div class="h-2 w-2"></div>
|
||||
<% } %>
|
||||
</div>
|
||||
<div class="ml-4 min-w-0 flex-1">
|
||||
<div class="flex items-center">
|
||||
<p class="text-sm font-medium text-gray-900 truncate">
|
||||
<%= contact.name %>
|
||||
</p>
|
||||
<% if (contact.serviceInterest) { %>
|
||||
<span class="ml-2 inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
|
||||
<%= contact.serviceInterest %>
|
||||
</span>
|
||||
<% } %>
|
||||
</div>
|
||||
<div class="flex items-center mt-1">
|
||||
<p class="text-sm text-gray-500 truncate">
|
||||
<%= contact.email %>
|
||||
</p>
|
||||
<span class="mx-2 text-gray-300">•</span>
|
||||
<p class="text-sm text-gray-500">
|
||||
<%= new Date(contact.createdAt).toLocaleDateString('ru-RU') %>
|
||||
</p>
|
||||
</div>
|
||||
<p class="text-sm text-gray-500 mt-1 truncate">
|
||||
<%= contact.message.substring(0, 100) %>...
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<!-- Status Badge -->
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium
|
||||
<%= contact.status === 'new' ? 'bg-green-100 text-green-800' :
|
||||
contact.status === 'in_progress' ? 'bg-yellow-100 text-yellow-800' :
|
||||
'bg-gray-100 text-gray-800' %>">
|
||||
<%= contact.status === 'new' ? 'Новое' :
|
||||
contact.status === 'in_progress' ? 'В работе' : 'Завершено' %>
|
||||
</span>
|
||||
<!-- Priority -->
|
||||
<% if (contact.priority === 'high') { %>
|
||||
<i class="fas fa-exclamation-triangle text-red-500"></i>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
<% }) %>
|
||||
<% } else { %>
|
||||
<li>
|
||||
<div class="px-4 py-8 text-center">
|
||||
<i class="fas fa-envelope text-4xl text-gray-300 mb-4"></i>
|
||||
<p class="text-gray-500">Сообщения не найдены</p>
|
||||
</div>
|
||||
</li>
|
||||
<% } %>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<% if (pagination && pagination.total > 1) { %>
|
||||
<div class="bg-white px-4 py-3 flex items-center justify-between border-t border-gray-200 sm:px-6">
|
||||
<div class="flex-1 flex justify-between sm:hidden">
|
||||
<% if (pagination.hasPrev) { %>
|
||||
<a href="?page=<%= pagination.current - 1 %>&status=<%= currentStatus %>" class="relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50">
|
||||
Предыдущая
|
||||
</a>
|
||||
<% } %>
|
||||
<% if (pagination.hasNext) { %>
|
||||
<a href="?page=<%= pagination.current + 1 %>&status=<%= currentStatus %>" class="ml-3 relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50">
|
||||
Следующая
|
||||
</a>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.getElementById('statusFilter').addEventListener('change', function() {
|
||||
const status = this.value;
|
||||
const url = new URL(window.location);
|
||||
url.searchParams.set('status', status);
|
||||
url.searchParams.delete('page'); // Reset to first page
|
||||
window.location.href = url.toString();
|
||||
});
|
||||
</script>
|
||||
219
views/admin/contacts/view.ejs
Normal file
219
views/admin/contacts/view.ejs
Normal file
@@ -0,0 +1,219 @@
|
||||
<!-- Contact Details -->
|
||||
<div class="bg-white shadow rounded-lg">
|
||||
<div class="px-4 py-5 sm:px-6 border-b border-gray-200">
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="text-lg leading-6 font-medium text-gray-900">
|
||||
<i class="fas fa-envelope mr-2"></i>
|
||||
Детали сообщения
|
||||
</h3>
|
||||
<a href="/admin/contacts" class="bg-gray-300 hover:bg-gray-400 text-gray-800 font-bold py-2 px-4 rounded">
|
||||
<i class="fas fa-arrow-left mr-1"></i>
|
||||
Назад к списку
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-6">
|
||||
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2">
|
||||
<!-- Contact Information -->
|
||||
<div class="bg-gray-50 p-4 rounded-lg">
|
||||
<h4 class="text-lg font-medium text-gray-900 mb-4">Информация о контакте</h4>
|
||||
|
||||
<dl class="space-y-3">
|
||||
<div>
|
||||
<dt class="text-sm font-medium text-gray-500">Имя</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900"><%= contact.name %></dd>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<dt class="text-sm font-medium text-gray-500">Email</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900">
|
||||
<a href="mailto:<%= contact.email %>" class="text-blue-600 hover:text-blue-800">
|
||||
<%= contact.email %>
|
||||
</a>
|
||||
</dd>
|
||||
</div>
|
||||
|
||||
<% if (contact.phone) { %>
|
||||
<div>
|
||||
<dt class="text-sm font-medium text-gray-500">Телефон</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900">
|
||||
<a href="tel:<%= contact.phone %>" class="text-blue-600 hover:text-blue-800">
|
||||
<%= contact.phone %>
|
||||
</a>
|
||||
</dd>
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
<div>
|
||||
<dt class="text-sm font-medium text-gray-500">Дата создания</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900">
|
||||
<%= new Date(contact.createdAt).toLocaleString('ru-RU') %>
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
<!-- Project Details -->
|
||||
<div class="bg-gray-50 p-4 rounded-lg">
|
||||
<h4 class="text-lg font-medium text-gray-900 mb-4">Детали проекта</h4>
|
||||
|
||||
<dl class="space-y-3">
|
||||
<% if (contact.serviceInterest) { %>
|
||||
<div>
|
||||
<dt class="text-sm font-medium text-gray-500">Интересующая услуга</dt>
|
||||
<dd class="mt-1">
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
|
||||
<%= contact.serviceInterest %>
|
||||
</span>
|
||||
</dd>
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
<% if (contact.budget) { %>
|
||||
<div>
|
||||
<dt class="text-sm font-medium text-gray-500">Бюджет</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900"><%= contact.budget %></dd>
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
<% if (contact.timeline) { %>
|
||||
<div>
|
||||
<dt class="text-sm font-medium text-gray-500">Временные рамки</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900"><%= contact.timeline %></dd>
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
<div>
|
||||
<dt class="text-sm font-medium text-gray-500">Статус</dt>
|
||||
<dd class="mt-1">
|
||||
<select id="statusSelect" data-contact-id="<%= contact.id %>"
|
||||
class="border-gray-300 rounded-md shadow-sm text-sm">
|
||||
<option value="new" <%= contact.status === 'new' ? 'selected' : '' %>>Новое</option>
|
||||
<option value="in_progress" <%= contact.status === 'in_progress' ? 'selected' : '' %>>В работе</option>
|
||||
<option value="completed" <%= contact.status === 'completed' ? 'selected' : '' %>>Завершено</option>
|
||||
</select>
|
||||
</dd>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<dt class="text-sm font-medium text-gray-500">Приоритет</dt>
|
||||
<dd class="mt-1">
|
||||
<select id="prioritySelect" data-contact-id="<%= contact.id %>"
|
||||
class="border-gray-300 rounded-md shadow-sm text-sm">
|
||||
<option value="low" <%= contact.priority === 'low' ? 'selected' : '' %>>Низкий</option>
|
||||
<option value="medium" <%= contact.priority === 'medium' ? 'selected' : '' %>>Средний</option>
|
||||
<option value="high" <%= contact.priority === 'high' ? 'selected' : '' %>>Высокий</option>
|
||||
</select>
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Message -->
|
||||
<div class="mt-6">
|
||||
<h4 class="text-lg font-medium text-gray-900 mb-3">Сообщение</h4>
|
||||
<div class="bg-gray-50 p-4 rounded-lg">
|
||||
<p class="text-sm text-gray-700 whitespace-pre-wrap"><%= contact.message %></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="mt-6 flex space-x-3">
|
||||
<button onclick="sendTelegramNotification('<%= contact.id %>')"
|
||||
class="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded-md text-sm font-medium">
|
||||
<i class="fab fa-telegram-plane mr-1"></i>
|
||||
Отправить в Telegram
|
||||
</button>
|
||||
|
||||
<a href="mailto:<%= contact.email %>?subject=Re: <%= encodeURIComponent(contact.serviceInterest || 'Ваш запрос') %>"
|
||||
class="bg-green-500 hover:bg-green-600 text-white px-4 py-2 rounded-md text-sm font-medium">
|
||||
<i class="fas fa-reply mr-1"></i>
|
||||
Ответить по email
|
||||
</a>
|
||||
|
||||
<button onclick="deleteContact('<%= contact.id %>')"
|
||||
class="bg-red-500 hover:bg-red-600 text-white px-4 py-2 rounded-md text-sm font-medium">
|
||||
<i class="fas fa-trash mr-1"></i>
|
||||
Удалить
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Update status
|
||||
document.getElementById('statusSelect').addEventListener('change', function() {
|
||||
updateContactField('status', this.value, this.dataset.contactId);
|
||||
});
|
||||
|
||||
// Update priority
|
||||
document.getElementById('prioritySelect').addEventListener('change', function() {
|
||||
updateContactField('priority', this.value, this.dataset.contactId);
|
||||
});
|
||||
|
||||
function updateContactField(field, value, contactId) {
|
||||
fetch(`/api/admin/contacts/${contactId}`, {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ [field]: value })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (!data.success) {
|
||||
alert('Ошибка при обновлении контакта');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
alert('Ошибка при обновлении контакта');
|
||||
});
|
||||
}
|
||||
|
||||
function sendTelegramNotification(contactId) {
|
||||
fetch(`/api/admin/contacts/${contactId}/telegram`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
alert('Уведомление отправлено в Telegram');
|
||||
} else {
|
||||
alert('Ошибка при отправке уведомления');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
alert('Ошибка при отправке уведомления');
|
||||
});
|
||||
}
|
||||
|
||||
function deleteContact(contactId) {
|
||||
if (confirm('Вы уверены, что хотите удалить это сообщение?')) {
|
||||
fetch(`/api/admin/contacts/${contactId}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
window.location.href = '/admin/contacts';
|
||||
} else {
|
||||
alert('Ошибка при удалении сообщения');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
alert('Ошибка при удалении сообщения');
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
323
views/admin/dashboard.ejs
Normal file
323
views/admin/dashboard.ejs
Normal file
@@ -0,0 +1,323 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title><%= title %> - SmartSolTech Admin</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.4.0/css/all.min.css">
|
||||
|
||||
<!-- Custom CSS -->
|
||||
<link rel="stylesheet" href="/css/main.css">
|
||||
<link rel="stylesheet" href="/css/fixes.css">
|
||||
</head>
|
||||
<body class="bg-gray-100">
|
||||
<!-- Admin Header -->
|
||||
<header class="bg-white shadow-sm border-b">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex justify-between items-center h-16">
|
||||
<div class="flex items-center">
|
||||
<h1 class="text-xl font-semibold text-gray-900">
|
||||
<i class="fas fa-cogs mr-2"></i>
|
||||
SmartSolTech Admin
|
||||
</h1>
|
||||
</div>
|
||||
<div class="flex items-center space-x-4">
|
||||
<span class="text-sm text-gray-600">
|
||||
Добро пожаловать, <%= user ? user.name : 'Admin' %>!
|
||||
</span>
|
||||
<a href="/" class="text-gray-500 hover:text-gray-700">
|
||||
<i class="fas fa-external-link-alt mr-1"></i>
|
||||
Посмотреть сайт
|
||||
</a>
|
||||
<form action="/admin/logout" method="post" class="inline">
|
||||
<button type="submit" class="text-red-600 hover:text-red-800">
|
||||
<i class="fas fa-sign-out-alt mr-1"></i>
|
||||
Выход
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="flex">
|
||||
<!-- Admin Sidebar -->
|
||||
<aside class="w-64 bg-white shadow-sm admin-sidebar min-h-screen">
|
||||
<nav class="mt-5 px-2">
|
||||
<div class="space-y-1">
|
||||
<a href="/admin/dashboard" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md bg-blue-100 text-blue-700">
|
||||
<i class="fas fa-tachometer-alt mr-3"></i>
|
||||
Панель управления
|
||||
</a>
|
||||
<a href="/admin/portfolio" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-600 hover:bg-gray-50 hover:text-gray-900">
|
||||
<i class="fas fa-briefcase mr-3"></i>
|
||||
Портфолио
|
||||
</a>
|
||||
<a href="/admin/services" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-600 hover:bg-gray-50 hover:text-gray-900">
|
||||
<i class="fas fa-cog mr-3"></i>
|
||||
Услуги
|
||||
</a>
|
||||
<a href="/admin/contacts" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-600 hover:bg-gray-50 hover:text-gray-900">
|
||||
<i class="fas fa-envelope mr-3"></i>
|
||||
Сообщения
|
||||
</a>
|
||||
<a href="/admin/media" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-600 hover:bg-gray-50 hover:text-gray-900">
|
||||
<i class="fas fa-images mr-3"></i>
|
||||
Медиа
|
||||
</a>
|
||||
<a href="/admin/settings" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-600 hover:bg-gray-50 hover:text-gray-900">
|
||||
<i class="fas fa-cogs mr-3"></i>
|
||||
Настройки
|
||||
</a>
|
||||
<a href="/admin/telegram" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-600 hover:bg-gray-50 hover:text-gray-900">
|
||||
<i class="fab fa-telegram mr-3"></i>
|
||||
Telegram Bot
|
||||
</a>
|
||||
<a href="/admin/banner-editor" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-600 hover:bg-gray-50 hover:text-gray-900">
|
||||
<i class="fas fa-paint-brush mr-3"></i>
|
||||
Редактор баннеров
|
||||
</a>
|
||||
</div>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="flex-1 p-8">
|
||||
<div class="space-y-6">
|
||||
<!-- Header -->
|
||||
<div class="bg-white shadow rounded-lg p-6">
|
||||
<h1 class="text-2xl font-bold text-gray-900 flex items-center">
|
||||
<i class="fas fa-tachometer-alt mr-3 text-blue-600"></i>
|
||||
Панель управления
|
||||
</h1>
|
||||
<p class="mt-2 text-gray-600">Обзор основных показателей сайта</p>
|
||||
</div>
|
||||
|
||||
<!-- Stats Grid -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
<!-- Portfolio Projects -->
|
||||
<div class="bg-white overflow-hidden shadow rounded-lg">
|
||||
<div class="p-5">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<i class="fas fa-briefcase text-blue-600 text-2xl"></i>
|
||||
</div>
|
||||
<div class="ml-5 w-0 flex-1">
|
||||
<dl>
|
||||
<dt class="text-sm font-medium text-gray-500 truncate">
|
||||
Проекты
|
||||
</dt>
|
||||
<dd class="text-lg font-medium text-gray-900">
|
||||
<%= stats.portfolioCount || 0 %>
|
||||
</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-gray-50 px-5 py-3">
|
||||
<div class="text-sm">
|
||||
<a href="/admin/portfolio" class="font-medium text-blue-600 hover:text-blue-500">
|
||||
Посмотреть всё
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Services -->
|
||||
<div class="bg-white overflow-hidden shadow rounded-lg">
|
||||
<div class="p-5">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<i class="fas fa-cog text-green-600 text-2xl"></i>
|
||||
</div>
|
||||
<div class="ml-5 w-0 flex-1">
|
||||
<dl>
|
||||
<dt class="text-sm font-medium text-gray-500 truncate">
|
||||
Услуги
|
||||
</dt>
|
||||
<dd class="text-lg font-medium text-gray-900">
|
||||
<%= stats.servicesCount || 0 %>
|
||||
</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-gray-50 px-5 py-3">
|
||||
<div class="text-sm">
|
||||
<a href="/admin/services" class="font-medium text-green-600 hover:text-green-500">
|
||||
Посмотреть всё
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Contact Messages -->
|
||||
<div class="bg-white overflow-hidden shadow rounded-lg">
|
||||
<div class="p-5">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<i class="fas fa-envelope text-purple-600 text-2xl"></i>
|
||||
</div>
|
||||
<div class="ml-5 w-0 flex-1">
|
||||
<dl>
|
||||
<dt class="text-sm font-medium text-gray-500 truncate">
|
||||
Сообщения
|
||||
</dt>
|
||||
<dd class="text-lg font-medium text-gray-900">
|
||||
<%= stats.contactsCount || 0 %>
|
||||
</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-gray-50 px-5 py-3">
|
||||
<div class="text-sm">
|
||||
<a href="/admin/contacts" class="font-medium text-purple-600 hover:text-purple-500">
|
||||
Посмотреть всё
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Users -->
|
||||
<div class="bg-white overflow-hidden shadow rounded-lg">
|
||||
<div class="p-5">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<i class="fas fa-users text-orange-600 text-2xl"></i>
|
||||
</div>
|
||||
<div class="ml-5 w-0 flex-1">
|
||||
<dl>
|
||||
<dt class="text-sm font-medium text-gray-500 truncate">
|
||||
Пользователи
|
||||
</dt>
|
||||
<dd class="text-lg font-medium text-gray-900">
|
||||
<%= stats.usersCount || 0 %>
|
||||
</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-gray-50 px-5 py-3">
|
||||
<div class="text-sm">
|
||||
<a href="/admin/users" class="font-medium text-orange-600 hover:text-orange-500">
|
||||
Посмотреть всё
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Recent Activity -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<!-- Recent Portfolio Projects -->
|
||||
<div class="bg-white shadow rounded-lg">
|
||||
<div class="px-6 py-4 border-b border-gray-200">
|
||||
<h3 class="text-lg font-medium text-gray-900">
|
||||
Последние проекты
|
||||
</h3>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<% if (recentPortfolio && recentPortfolio.length > 0) { %>
|
||||
<div class="space-y-4">
|
||||
<% recentPortfolio.forEach(function(project) { %>
|
||||
<div class="flex items-center space-x-4">
|
||||
<div class="flex-shrink-0">
|
||||
<i class="fas fa-briefcase text-blue-600"></i>
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="text-sm font-medium text-gray-900 truncate">
|
||||
<%= project.title %>
|
||||
</p>
|
||||
<p class="text-sm text-gray-500">
|
||||
<%= project.category %>
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex-shrink-0">
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
|
||||
<%= project.status %>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<% }); %>
|
||||
</div>
|
||||
<% } else { %>
|
||||
<p class="text-gray-500 text-sm">Нет недавних проектов</p>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Recent Contact Messages -->
|
||||
<div class="bg-white shadow rounded-lg">
|
||||
<div class="px-6 py-4 border-b border-gray-200">
|
||||
<h3 class="text-lg font-medium text-gray-900">
|
||||
Последние сообщения
|
||||
</h3>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<% if (recentContacts && recentContacts.length > 0) { %>
|
||||
<div class="space-y-4">
|
||||
<% recentContacts.forEach(function(contact) { %>
|
||||
<div class="flex items-center space-x-4">
|
||||
<div class="flex-shrink-0">
|
||||
<i class="fas fa-envelope text-purple-600"></i>
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="text-sm font-medium text-gray-900 truncate">
|
||||
<%= contact.name %>
|
||||
</p>
|
||||
<p class="text-sm text-gray-500 truncate">
|
||||
<%= contact.email %>
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex-shrink-0">
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800">
|
||||
<%= contact.status %>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<% }); %>
|
||||
</div>
|
||||
<% } else { %>
|
||||
<p class="text-gray-500 text-sm">Нет недавних сообщений</p>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick Actions -->
|
||||
<div class="bg-white shadow rounded-lg p-6">
|
||||
<h3 class="text-lg font-medium text-gray-900 mb-4">
|
||||
Быстрые действия
|
||||
</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<a href="/admin/portfolio/new"
|
||||
class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700">
|
||||
<i class="fas fa-plus mr-2"></i>
|
||||
Добавить проект
|
||||
</a>
|
||||
<a href="/admin/services/new"
|
||||
class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-green-600 hover:bg-green-700">
|
||||
<i class="fas fa-plus mr-2"></i>
|
||||
Добавить услугу
|
||||
</a>
|
||||
<a href="/admin/settings"
|
||||
class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-gray-600 hover:bg-gray-700">
|
||||
<i class="fas fa-cogs mr-2"></i>
|
||||
Настройки сайта
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- JavaScript -->
|
||||
<script src="/js/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
23
views/admin/error.ejs
Normal file
23
views/admin/error.ejs
Normal file
@@ -0,0 +1,23 @@
|
||||
<%- include('layout', { title: title, user: user }) %>
|
||||
|
||||
<div class="max-w-lg mx-auto text-center">
|
||||
<div class="mb-8">
|
||||
<i class="fas fa-exclamation-triangle text-red-500 text-6xl mb-4"></i>
|
||||
<h1 class="text-2xl font-bold text-gray-900 mb-2">Ошибка</h1>
|
||||
<p class="text-gray-600"><%= message %></p>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<a href="/admin/dashboard"
|
||||
class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700">
|
||||
<i class="fas fa-arrow-left mr-2"></i>
|
||||
Вернуться к панели управления
|
||||
</a>
|
||||
|
||||
<div>
|
||||
<a href="/" class="text-blue-600 hover:text-blue-500 text-sm">
|
||||
Перейти на главную страницу сайта
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
104
views/admin/layout.ejs
Normal file
104
views/admin/layout.ejs
Normal file
@@ -0,0 +1,104 @@
|
||||
<!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>
|
||||
|
||||
<!-- 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">
|
||||
|
||||
<!-- Custom CSS -->
|
||||
<link rel="stylesheet" href="/css/main.css">
|
||||
<link rel="stylesheet" href="/css/fixes.css">
|
||||
|
||||
<style>
|
||||
.admin-sidebar {
|
||||
min-height: calc(100vh - 64px);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-gray-100">
|
||||
<!-- Admin Header -->
|
||||
<header class="bg-white shadow-sm border-b">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex justify-between items-center h-16">
|
||||
<div class="flex items-center">
|
||||
<h1 class="text-xl font-semibold text-gray-900">
|
||||
<i class="fas fa-cogs mr-2"></i>
|
||||
SmartSolTech Admin
|
||||
</h1>
|
||||
</div>
|
||||
<div class="flex items-center space-x-4">
|
||||
<span class="text-sm text-gray-600">
|
||||
Добро пожаловать, <%= user ? user.name : 'Admin' %>!
|
||||
</span>
|
||||
<a href="/" class="text-gray-500 hover:text-gray-700">
|
||||
<i class="fas fa-external-link-alt mr-1"></i>
|
||||
Посмотреть сайт
|
||||
</a>
|
||||
<form action="/admin/logout" method="post" class="inline">
|
||||
<button type="submit" class="text-red-600 hover:text-red-800">
|
||||
<i class="fas fa-sign-out-alt mr-1"></i>
|
||||
Выход
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="flex">
|
||||
<!-- Admin Sidebar -->
|
||||
<aside class="w-64 bg-white shadow-sm admin-sidebar">
|
||||
<nav class="mt-5 px-2">
|
||||
<div class="space-y-1">
|
||||
<a href="/admin/dashboard" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-600 hover:bg-gray-50 hover:text-gray-900">
|
||||
<i class="fas fa-tachometer-alt mr-3"></i>
|
||||
Панель управления
|
||||
</a>
|
||||
<a href="/admin/portfolio" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-600 hover:bg-gray-50 hover:text-gray-900">
|
||||
<i class="fas fa-briefcase mr-3"></i>
|
||||
Портфолио
|
||||
</a>
|
||||
<a href="/admin/services" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-600 hover:bg-gray-50 hover:text-gray-900">
|
||||
<i class="fas fa-cog mr-3"></i>
|
||||
Услуги
|
||||
</a>
|
||||
<a href="/admin/contacts" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-600 hover:bg-gray-50 hover:text-gray-900">
|
||||
<i class="fas fa-envelope mr-3"></i>
|
||||
Сообщения
|
||||
</a>
|
||||
<a href="/admin/media" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-600 hover:bg-gray-50 hover:text-gray-900">
|
||||
<i class="fas fa-images mr-3"></i>
|
||||
Медиа
|
||||
</a>
|
||||
<a href="/admin/settings" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-600 hover:bg-gray-50 hover:text-gray-900">
|
||||
<i class="fas fa-cogs mr-3"></i>
|
||||
Настройки
|
||||
</a>
|
||||
<a href="/admin/telegram" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-600 hover:bg-gray-50 hover:text-gray-900">
|
||||
<i class="fab fa-telegram mr-3"></i>
|
||||
Telegram Bot
|
||||
</a>
|
||||
<a href="/admin/banner-editor" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-600 hover:bg-gray-50 hover:text-gray-900">
|
||||
<i class="fas fa-paint-brush mr-3"></i>
|
||||
Редактор баннеров
|
||||
</a>
|
||||
</div>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="flex-1 p-8">
|
||||
<%- body %>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- JavaScript -->
|
||||
<script src="/js/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
81
views/admin/login.ejs
Normal file
81
views/admin/login.ejs
Normal file
@@ -0,0 +1,81 @@
|
||||
о<!DOCTYPE html>
|
||||
<html lang="<%= currentLanguage %>">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Вход в админ панель - 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">
|
||||
|
||||
<!-- Custom CSS -->
|
||||
<link rel="stylesheet" href="/css/main.css">
|
||||
<link rel="stylesheet" href="/css/fixes.css">
|
||||
</head>
|
||||
<body class="bg-gray-50 flex items-center justify-center min-h-screen">
|
||||
<div class="max-w-md w-full space-y-8">
|
||||
<div>
|
||||
<div class="mx-auto h-12 w-12 flex items-center justify-center rounded-full bg-blue-100">
|
||||
<i class="fas fa-lock text-blue-600 text-xl"></i>
|
||||
</div>
|
||||
<h2 class="mt-6 text-center text-3xl font-extrabold text-gray-900">
|
||||
Вход в админ панель
|
||||
</h2>
|
||||
<p class="mt-2 text-center text-sm text-gray-600">
|
||||
Войдите в свой аккаунт для управления сайтом
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form class="mt-8 space-y-6" action="/admin/login" method="POST">
|
||||
<% if (typeof error !== 'undefined') { %>
|
||||
<div class="bg-red-50 border border-red-200 text-red-600 px-4 py-3 rounded">
|
||||
<i class="fas fa-exclamation-triangle mr-2"></i>
|
||||
<%= error %>
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
<div class="rounded-md shadow-sm space-y-4">
|
||||
<div>
|
||||
<label for="email" class="block text-sm font-medium text-gray-700">
|
||||
Email
|
||||
</label>
|
||||
<input id="email" name="email" type="email" required
|
||||
class="mt-1 appearance-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 focus:z-10 sm:text-sm"
|
||||
placeholder="admin@smartsoltech.com">
|
||||
</div>
|
||||
<div>
|
||||
<label for="password" class="block text-sm font-medium text-gray-700">
|
||||
Пароль
|
||||
</label>
|
||||
<input id="password" name="password" type="password" required
|
||||
class="mt-1 appearance-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 focus:z-10 sm:text-sm"
|
||||
placeholder="Введите пароль">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button type="submit"
|
||||
class="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
||||
<span class="absolute left-0 inset-y-0 flex items-center pl-3">
|
||||
<i class="fas fa-sign-in-alt text-blue-500 group-hover:text-blue-400"></i>
|
||||
</span>
|
||||
Войти
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="text-center">
|
||||
<a href="/" class="text-blue-600 hover:text-blue-500 text-sm">
|
||||
<i class="fas fa-arrow-left mr-1"></i>
|
||||
Вернуться на сайт
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- JavaScript -->
|
||||
<script src="/js/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
848
views/admin/media.ejs
Normal file
848
views/admin/media.ejs
Normal file
@@ -0,0 +1,848 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Медиа Галерея - SmartSolTech Admin</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.4.0/css/all.min.css">
|
||||
|
||||
<!-- Custom CSS -->
|
||||
<link rel="stylesheet" href="/css/main.css">
|
||||
<link rel="stylesheet" href="/css/fixes.css">
|
||||
</head>
|
||||
<body class="bg-gray-100">
|
||||
<!-- Admin Header -->
|
||||
<header class="bg-white shadow-sm border-b">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex justify-between items-center h-16">
|
||||
<div class="flex items-center">
|
||||
<h1 class="text-xl font-semibold text-gray-900">
|
||||
<i class="fas fa-cogs mr-2"></i>
|
||||
SmartSolTech Admin
|
||||
</h1>
|
||||
</div>
|
||||
<div class="flex items-center space-x-4">
|
||||
<span class="text-sm text-gray-600">
|
||||
Добро пожаловать, <%= user ? user.name : 'Admin' %>!
|
||||
</span>
|
||||
<a href="/" class="text-gray-500 hover:text-gray-700">
|
||||
<i class="fas fa-external-link-alt mr-1"></i>
|
||||
Посмотреть сайт
|
||||
</a>
|
||||
<form action="/admin/logout" method="post" class="inline">
|
||||
<button type="submit" class="text-red-600 hover:text-red-800">
|
||||
<i class="fas fa-sign-out-alt mr-1"></i>
|
||||
Выход
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="flex">
|
||||
<!-- Admin Sidebar -->
|
||||
<aside class="w-64 bg-white shadow-sm admin-sidebar min-h-screen">
|
||||
<nav class="mt-5 px-2">
|
||||
<div class="space-y-1">
|
||||
<a href="/admin/dashboard" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-600 hover:bg-gray-50 hover:text-gray-900">
|
||||
<i class="fas fa-tachometer-alt mr-3"></i>
|
||||
Панель управления
|
||||
</a>
|
||||
<a href="/admin/portfolio" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-600 hover:bg-gray-50 hover:text-gray-900">
|
||||
<i class="fas fa-briefcase mr-3"></i>
|
||||
Портфолио
|
||||
</a>
|
||||
<a href="/admin/services" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-600 hover:bg-gray-50 hover:text-gray-900">
|
||||
<i class="fas fa-cog mr-3"></i>
|
||||
Услуги
|
||||
</a>
|
||||
<a href="/admin/contacts" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-600 hover:bg-gray-50 hover:text-gray-900">
|
||||
<i class="fas fa-envelope mr-3"></i>
|
||||
Сообщения
|
||||
</a>
|
||||
<a href="/admin/media" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md bg-blue-100 text-blue-700">
|
||||
<i class="fas fa-images mr-3"></i>
|
||||
Медиа
|
||||
</a>
|
||||
<a href="/admin/settings" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-600 hover:bg-gray-50 hover:text-gray-900">
|
||||
<i class="fas fa-cogs mr-3"></i>
|
||||
Настройки
|
||||
</a>
|
||||
<a href="/admin/banner-editor" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-600 hover:bg-gray-50 hover:text-gray-900">
|
||||
<i class="fas fa-paint-brush mr-3"></i>
|
||||
Редактор баннеров
|
||||
</a>
|
||||
</div>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="flex-1 p-8">
|
||||
<div class="space-y-6">
|
||||
<!-- Header -->
|
||||
<div class="bg-white shadow rounded-lg p-6">
|
||||
<div class="flex justify-between items-center">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-gray-900 flex items-center">
|
||||
<i class="fas fa-images mr-3 text-blue-600"></i>
|
||||
Медиа Галерея
|
||||
</h1>
|
||||
<p class="mt-2 text-gray-600">Управление изображениями и файлами сайта</p>
|
||||
</div>
|
||||
<div class="flex space-x-3">
|
||||
<button id="refresh-btn" class="bg-gray-500 hover:bg-gray-600 text-white px-4 py-2 rounded-lg transition-colors">
|
||||
<i class="fas fa-sync-alt mr-2"></i>
|
||||
Обновить
|
||||
</button>
|
||||
<button id="upload-btn" class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg transition-colors">
|
||||
<i class="fas fa-upload mr-2"></i>
|
||||
Загрузить файлы
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Upload Zone -->
|
||||
<div id="upload-zone" class="bg-white shadow rounded-lg p-8 border-2 border-dashed border-gray-300 text-center" style="display: none;">
|
||||
<div class="mb-4">
|
||||
<i class="fas fa-cloud-upload-alt text-6xl text-gray-400 mb-4"></i>
|
||||
<p class="text-xl text-gray-600 mb-2">Перетащите файлы сюда или нажмите для выбора</p>
|
||||
<p class="text-gray-500">Поддерживаются: JPG, PNG, GIF, SVG (максимум 10MB каждый)</p>
|
||||
</div>
|
||||
<input type="file" id="file-input" multiple accept="image/*" class="hidden">
|
||||
<button type="button" onclick="document.getElementById('file-input').click()" class="bg-blue-600 hover:bg-blue-700 text-white px-6 py-3 rounded-lg">
|
||||
Выбрать файлы
|
||||
</button>
|
||||
<button id="cancel-upload" class="bg-gray-500 hover:bg-gray-600 text-white px-6 py-3 rounded-lg ml-3">
|
||||
Отмена
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Upload Progress -->
|
||||
<div id="upload-progress" class="bg-white shadow rounded-lg p-6" style="display: none;">
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-4">Загрузка файлов</h3>
|
||||
<div class="space-y-3" id="progress-list">
|
||||
<!-- Progress items will be added here -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filter and Search -->
|
||||
<div class="bg-white shadow rounded-lg p-6">
|
||||
<div class="flex flex-col md:flex-row md:items-center md:justify-between space-y-4 md:space-y-0">
|
||||
<div class="flex space-x-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Тип файла</label>
|
||||
<select id="file-type-filter" class="border border-gray-300 rounded-lg px-3 py-2">
|
||||
<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>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Размер</label>
|
||||
<select id="size-filter" class="border border-gray-300 rounded-lg px-3 py-2">
|
||||
<option value="">Любой размер</option>
|
||||
<option value="small">Маленький (< 1MB)</option>
|
||||
<option value="medium">Средний (1-5MB)</option>
|
||||
<option value="large">Большой (> 5MB)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Поиск</label>
|
||||
<div class="relative">
|
||||
<input type="text" id="search-input" placeholder="Поиск по имени файла..." class="border border-gray-300 rounded-lg px-3 py-2 pr-10 w-64">
|
||||
<i class="fas fa-search absolute right-3 top-3 text-gray-400"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Media Grid -->
|
||||
<div class="bg-white shadow rounded-lg p-6">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h3 class="text-lg font-semibold text-gray-900">Файлы</h3>
|
||||
<div class="flex items-center space-x-4">
|
||||
<span id="file-count" class="text-sm text-gray-600">Загрузка...</span>
|
||||
<div class="flex space-x-2">
|
||||
<button id="grid-view" class="p-2 text-gray-600 hover:text-gray-900 border border-gray-300 rounded">
|
||||
<i class="fas fa-th-large"></i>
|
||||
</button>
|
||||
<button id="list-view" class="p-2 text-gray-600 hover:text-gray-900 border border-gray-300 rounded">
|
||||
<i class="fas fa-list"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Loading State -->
|
||||
<div id="loading" class="text-center py-12">
|
||||
<div class="inline-block animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
|
||||
<p class="mt-2 text-gray-600">Загрузка медиа файлов...</p>
|
||||
</div>
|
||||
|
||||
<!-- Empty State -->
|
||||
<div id="empty-state" class="text-center py-12" style="display: none;">
|
||||
<i class="fas fa-images text-6xl text-gray-400 mb-4"></i>
|
||||
<h3 class="text-xl font-semibold text-gray-900 mb-2">Нет загруженных файлов</h3>
|
||||
<p class="text-gray-600 mb-6">Начните с загрузки ваших первых изображений</p>
|
||||
<button onclick="document.getElementById('upload-btn').click()" class="bg-blue-600 hover:bg-blue-700 text-white px-6 py-3 rounded-lg">
|
||||
<i class="fas fa-upload mr-2"></i>
|
||||
Загрузить файлы
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Media Grid -->
|
||||
<div id="media-grid" class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-6 gap-4">
|
||||
<!-- Media items will be loaded here -->
|
||||
</div>
|
||||
|
||||
<!-- Media List -->
|
||||
<div id="media-list" class="space-y-4" style="display: none;">
|
||||
<!-- List items will be loaded here -->
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<div id="pagination" class="mt-8 flex justify-center" style="display: none;">
|
||||
<nav class="flex space-x-2">
|
||||
<button id="prev-page" class="px-3 py-2 bg-gray-200 text-gray-600 rounded hover:bg-gray-300 disabled:opacity-50">
|
||||
<i class="fas fa-chevron-left"></i>
|
||||
</button>
|
||||
<div id="page-numbers" class="flex space-x-2">
|
||||
<!-- Page numbers will be added here -->
|
||||
</div>
|
||||
<button id="next-page" class="px-3 py-2 bg-gray-200 text-gray-600 rounded hover:bg-gray-300 disabled:opacity-50">
|
||||
<i class="fas fa-chevron-right"></i>
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- Media Preview Modal -->
|
||||
<div id="preview-modal" class="fixed inset-0 bg-black bg-opacity-75 z-50 flex items-center justify-center" style="display: none;">
|
||||
<div class="bg-white rounded-lg shadow-lg max-w-4xl max-h-[90vh] w-full mx-4 overflow-hidden">
|
||||
<div class="p-4 border-b flex justify-between items-center">
|
||||
<h3 id="modal-title" class="text-lg font-semibold text-gray-900">Предпросмотр файла</h3>
|
||||
<button id="close-modal" class="text-gray-400 hover:text-gray-600">
|
||||
<i class="fas fa-times text-xl"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<div class="flex flex-col lg:flex-row space-y-6 lg:space-y-0 lg:space-x-6">
|
||||
<div class="flex-1">
|
||||
<img id="modal-image" src="" alt="" class="w-full h-auto rounded-lg shadow">
|
||||
</div>
|
||||
<div class="w-full lg:w-80">
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Имя файла</label>
|
||||
<input id="modal-filename" type="text" class="w-full border border-gray-300 rounded-lg px-3 py-2" readonly>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">URL</label>
|
||||
<div class="flex">
|
||||
<input id="modal-url" type="text" class="flex-1 border border-gray-300 rounded-l-lg px-3 py-2" readonly>
|
||||
<button onclick="copyToClipboard()" class="bg-gray-100 border border-l-0 border-gray-300 rounded-r-lg px-3 py-2 hover:bg-gray-200">
|
||||
<i class="fas fa-copy"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Размер</label>
|
||||
<p id="modal-size" class="text-sm text-gray-600">-</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Тип</label>
|
||||
<p id="modal-type" class="text-sm text-gray-600">-</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Ширина</label>
|
||||
<p id="modal-width" class="text-sm text-gray-600">-</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Высота</label>
|
||||
<p id="modal-height" class="text-sm text-gray-600">-</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Загружено</label>
|
||||
<p id="modal-date" class="text-sm text-gray-600">-</p>
|
||||
</div>
|
||||
<div class="border-t pt-4 space-y-3">
|
||||
<button onclick="downloadFile()" class="w-full bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg">
|
||||
<i class="fas fa-download mr-2"></i>
|
||||
Скачать
|
||||
</button>
|
||||
<button onclick="deleteFile()" class="w-full bg-red-600 hover:bg-red-700 text-white px-4 py-2 rounded-lg">
|
||||
<i class="fas fa-trash mr-2"></i>
|
||||
Удалить
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- JavaScript -->
|
||||
<script src="/js/main.js"></script>
|
||||
|
||||
<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>
|
||||
776
views/admin/portfolio/add.ejs
Normal file
776
views/admin/portfolio/add.ejs
Normal file
@@ -0,0 +1,776 @@
|
||||
<!-- Add Portfolio Item -->
|
||||
<div class="max-w-5xl mx-auto">
|
||||
<div class="bg-white shadow rounded-lg">
|
||||
<div class="px-6 py-4 border-b border-gray-200">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h3 class="text-xl font-semibold text-gray-900">
|
||||
<i class="fas fa-plus-circle mr-2 text-blue-600"></i>
|
||||
Добавить новый проект
|
||||
</h3>
|
||||
<p class="mt-1 text-sm text-gray-500">Заполните информацию о проекте для добавления в портфолио</p>
|
||||
</div>
|
||||
<div class="flex space-x-3">
|
||||
<button type="button" onclick="saveDraft()" class="inline-flex items-center px-3 py-2 border border-gray-300 shadow-sm text-sm leading-4 font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
||||
<i class="fas fa-save mr-2"></i>
|
||||
Сохранить черновик
|
||||
</button>
|
||||
<a href="/admin/portfolio" class="inline-flex items-center px-3 py-2 border border-gray-300 shadow-sm text-sm leading-4 font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50">
|
||||
<i class="fas fa-arrow-left mr-2"></i>
|
||||
Назад к списку
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form id="portfolioForm" class="p-6">
|
||||
<!-- Progress indicator -->
|
||||
<div class="mb-8">
|
||||
<div class="flex items-center">
|
||||
<div class="flex items-center text-blue-600 relative">
|
||||
<div class="rounded-full h-8 w-8 bg-blue-600 text-white flex items-center justify-center text-sm font-medium">1</div>
|
||||
<span class="ml-3 text-sm font-medium text-blue-600">Основная информация</span>
|
||||
</div>
|
||||
<div class="flex-1 mx-4 h-1 bg-gray-200"></div>
|
||||
<div class="flex items-center text-gray-500">
|
||||
<div class="rounded-full h-8 w-8 bg-gray-200 text-gray-600 flex items-center justify-center text-sm font-medium">2</div>
|
||||
<span class="ml-3 text-sm font-medium text-gray-500">Медиа и изображения</span>
|
||||
</div>
|
||||
<div class="flex-1 mx-4 h-1 bg-gray-200"></div>
|
||||
<div class="flex items-center text-gray-500">
|
||||
<div class="rounded-full h-8 w-8 bg-gray-200 text-gray-600 flex items-center justify-center text-sm font-medium">3</div>
|
||||
<span class="ml-3 text-sm font-medium text-gray-500">Публикация</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-8">
|
||||
<!-- Шаг 1: Основная информация -->
|
||||
<div class="bg-gray-50 rounded-lg p-6">
|
||||
<h4 class="text-lg font-medium text-gray-900 mb-6">
|
||||
<i class="fas fa-info-circle mr-2 text-blue-600"></i>
|
||||
Основная информация о проекте
|
||||
</h4>
|
||||
|
||||
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2">
|
||||
<!-- Title -->
|
||||
<div class="sm:col-span-2">
|
||||
<label for="title" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
Название проекта *
|
||||
<span class="text-gray-500 font-normal">(отображается в портфолио)</span>
|
||||
</label>
|
||||
<input type="text" name="title" id="title" required
|
||||
placeholder="Например: Интернет-магазин электроники TechStore"
|
||||
class="block w-full border-gray-300 rounded-lg shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm">
|
||||
<p class="mt-1 text-sm text-gray-500">Используйте описательное название, которое четко передает суть проекта</p>
|
||||
</div>
|
||||
|
||||
<!-- Short Description -->
|
||||
<div class="sm:col-span-2">
|
||||
<label for="shortDescription" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
Краткое описание *
|
||||
<span class="text-gray-500 font-normal">(1-2 предложения)</span>
|
||||
</label>
|
||||
<textarea name="shortDescription" id="shortDescription" rows="2" required
|
||||
placeholder="Современный интернет-магазин с удобной системой каталогов и интеграцией платежных систем"
|
||||
class="block w-full border-gray-300 rounded-lg shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
|
||||
maxlength="200"></textarea>
|
||||
<div class="mt-1 flex justify-between text-sm text-gray-500">
|
||||
<span>Отображается в превью проекта</span>
|
||||
<span id="shortDescCount">0/200</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Category -->
|
||||
<div>
|
||||
<label for="category" class="block text-sm font-medium text-gray-700 mb-2">Категория *</label>
|
||||
<select name="category" id="category" required
|
||||
class="block w-full border-gray-300 rounded-lg shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm">
|
||||
<option value="">Выберите категорию</option>
|
||||
<% categories.forEach(cat => { %>
|
||||
<option value="<%= cat %>"><%= getCategoryName(cat) %></option>
|
||||
<% }); %>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Duration -->
|
||||
<div>
|
||||
<label for="duration" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
Длительность проекта
|
||||
<span class="text-gray-500 font-normal">(в днях)</span>
|
||||
</label>
|
||||
<input type="number" name="duration" id="duration" min="1" max="365"
|
||||
placeholder="30"
|
||||
class="block w-full border-gray-300 rounded-lg shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm">
|
||||
</div>
|
||||
|
||||
<!-- Client Name -->
|
||||
<div>
|
||||
<label for="clientName" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
Название клиента
|
||||
<span class="text-gray-500 font-normal">(необязательно)</span>
|
||||
</label>
|
||||
<input type="text" name="clientName" id="clientName"
|
||||
placeholder="ООО «ТехноСтор»"
|
||||
class="block w-full border-gray-300 rounded-lg shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm">
|
||||
</div>
|
||||
|
||||
<!-- Demo URL -->
|
||||
<div>
|
||||
<label for="demoUrl" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
Ссылка на демо
|
||||
<span class="text-gray-500 font-normal">(живая версия)</span>
|
||||
</label>
|
||||
<input type="url" name="demoUrl" id="demoUrl"
|
||||
placeholder="https://techstore.example.com"
|
||||
class="block w-full border-gray-300 rounded-lg shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm">
|
||||
</div>
|
||||
|
||||
<!-- GitHub URL -->
|
||||
<div>
|
||||
<label for="githubUrl" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
GitHub репозиторий
|
||||
<span class="text-gray-500 font-normal">(если публичный)</span>
|
||||
</label>
|
||||
<input type="url" name="githubUrl" id="githubUrl"
|
||||
placeholder="https://github.com/username/project"
|
||||
class="block w-full border-gray-300 rounded-lg shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Шаг 2: Детальное описание и технологии -->
|
||||
<div class="bg-gray-50 rounded-lg p-6">
|
||||
<h4 class="text-lg font-medium text-gray-900 mb-6">
|
||||
<i class="fas fa-align-left mr-2 text-blue-600"></i>
|
||||
Описание и технические детали
|
||||
</h4>
|
||||
|
||||
<!-- Description -->
|
||||
<div class="mb-6">
|
||||
<label for="description" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
Подробное описание проекта *
|
||||
</label>
|
||||
<div id="descriptionEditor" class="min-h-40 border border-gray-300 rounded-lg"></div>
|
||||
<textarea name="description" id="description" style="display: none;" required></textarea>
|
||||
<p class="mt-2 text-sm text-gray-500">Опишите задачи, решения и результаты проекта</p>
|
||||
</div>
|
||||
|
||||
<!-- Technologies -->
|
||||
<div>
|
||||
<label for="technologies" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
Используемые технологии *
|
||||
</label>
|
||||
<div class="relative">
|
||||
<input type="text" id="technologyInput"
|
||||
placeholder="Введите технологию и нажмите Enter"
|
||||
class="block w-full border-gray-300 rounded-lg shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm">
|
||||
<div class="absolute right-2 top-2">
|
||||
<button type="button" onclick="addTechnology()" class="text-blue-600 hover:text-blue-800">
|
||||
<i class="fas fa-plus"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="technologiesList" class="mt-3 flex flex-wrap gap-2"></div>
|
||||
<input type="hidden" name="technologies" id="technologiesHidden">
|
||||
|
||||
<!-- Популярные технологии для быстрого добавления -->
|
||||
<div class="mt-4">
|
||||
<p class="text-sm text-gray-600 mb-2">Популярные технологии:</p>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<% const popularTechs = ['React', 'Vue.js', 'Node.js', 'Express.js', 'MongoDB', 'PostgreSQL', 'MySQL', 'JavaScript', 'TypeScript', 'HTML5', 'CSS3', 'Tailwind CSS', 'Bootstrap', 'Webpack', 'Docker', 'Git', 'AWS', 'Figma', 'Photoshop']; %>
|
||||
<% popularTechs.forEach(tech => { %>
|
||||
<button type="button" onclick="addTechnologyFromList('<%= tech %>')"
|
||||
class="px-2 py-1 text-xs border border-gray-300 rounded-md text-gray-600 hover:bg-gray-100">
|
||||
<%= tech %>
|
||||
</button>
|
||||
<% }); %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Шаг 3: Медиа контент -->
|
||||
<div class="bg-gray-50 rounded-lg p-6">
|
||||
<h4 class="text-lg font-medium text-gray-900 mb-6">
|
||||
<i class="fas fa-images mr-2 text-blue-600"></i>
|
||||
Изображения и медиа контент
|
||||
</h4>
|
||||
|
||||
<div class="space-y-6">
|
||||
<!-- Main Image Upload -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-3">
|
||||
Изображения проекта
|
||||
<span class="text-red-500">*</span>
|
||||
</label>
|
||||
<div id="dropZone" class="relative border-2 border-dashed border-gray-300 rounded-lg p-8 text-center hover:border-blue-500 transition-colors">
|
||||
<div class="space-y-4">
|
||||
<div class="mx-auto w-16 h-16 text-gray-400">
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<label for="images" class="cursor-pointer">
|
||||
<span class="text-blue-600 font-medium hover:text-blue-500">Загрузить изображения</span>
|
||||
<input id="images" name="images" type="file" class="sr-only" multiple accept="image/*">
|
||||
</label>
|
||||
<span class="text-gray-600"> или перетащите файлы сюда</span>
|
||||
</div>
|
||||
<p class="text-sm text-gray-500">
|
||||
PNG, JPG, WEBP до 10MB каждый. Первое изображение будет использоваться как главное.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Image Preview and Management -->
|
||||
<div id="imagePreviewContainer" style="display: none;">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h5 class="text-sm font-medium text-gray-900">Загруженные изображения</h5>
|
||||
<button type="button" onclick="clearAllImages()" class="text-red-600 hover:text-red-800 text-sm">
|
||||
<i class="fas fa-trash mr-1"></i>
|
||||
Удалить все
|
||||
</button>
|
||||
</div>
|
||||
<div id="imagePreview" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4"></div>
|
||||
<p class="mt-2 text-sm text-gray-500">
|
||||
<i class="fas fa-info-circle mr-1"></i>
|
||||
Перетаскивайте изображения для изменения порядка. Первое изображение будет главным.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Шаг 4: Настройки публикации -->
|
||||
<div class="bg-gray-50 rounded-lg p-6">
|
||||
<h4 class="text-lg font-medium text-gray-900 mb-6">
|
||||
<i class="fas fa-cog mr-2 text-blue-600"></i>
|
||||
Настройки публикации
|
||||
</h4>
|
||||
|
||||
<div class="space-y-6">
|
||||
<!-- Publication Options -->
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-6">
|
||||
<div class="flex items-start">
|
||||
<div class="flex items-center h-5">
|
||||
<input id="isPublished" name="isPublished" type="checkbox"
|
||||
class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded">
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<label for="isPublished" class="text-sm font-medium text-gray-900">Опубликовать проект</label>
|
||||
<p class="text-sm text-gray-500">Проект будет виден посетителям сайта</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-start">
|
||||
<div class="flex items-center h-5">
|
||||
<input id="featured" name="featured" type="checkbox"
|
||||
class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded">
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<label for="featured" class="text-sm font-medium text-gray-900">Рекомендуемый проект</label>
|
||||
<p class="text-sm text-gray-500">Проект будет показан в топе портфолио</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SEO Fields -->
|
||||
<div class="border-t border-gray-200 pt-6">
|
||||
<h5 class="text-sm font-medium text-gray-900 mb-4">SEO настройки (необязательно)</h5>
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label for="seoTitle" class="block text-sm font-medium text-gray-700 mb-1">SEO заголовок</label>
|
||||
<input type="text" name="seoTitle" id="seoTitle" maxlength="60"
|
||||
placeholder="Будет использован заголовок проекта"
|
||||
class="block w-full border-gray-300 rounded-lg shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm">
|
||||
<p class="mt-1 text-sm text-gray-500">Рекомендуется до 60 символов</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="seoDescription" class="block text-sm font-medium text-gray-700 mb-1">SEO описание</label>
|
||||
<textarea name="seoDescription" id="seoDescription" rows="2" maxlength="160"
|
||||
placeholder="Будет использовано краткое описание"
|
||||
class="block w-full border-gray-300 rounded-lg shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm"></textarea>
|
||||
<p class="mt-1 text-sm text-gray-500">Рекомендуется до 160 символов</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Submit Buttons -->
|
||||
<div class="flex justify-between items-center pt-6 border-t border-gray-200">
|
||||
<div class="flex space-x-3">
|
||||
<button type="button" onclick="previewProject()" class="inline-flex items-center px-4 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50">
|
||||
<i class="fas fa-eye mr-2"></i>
|
||||
Предпросмотр
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex space-x-3">
|
||||
<a href="/admin/portfolio" class="inline-flex items-center px-4 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50">
|
||||
Отмена
|
||||
</a>
|
||||
<button type="button" onclick="saveDraft()" class="inline-flex items-center px-4 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50">
|
||||
<i class="fas fa-save mr-2"></i>
|
||||
Сохранить как черновик
|
||||
</button>
|
||||
<button type="submit" id="submitBtn" class="inline-flex items-center px-6 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
||||
<i class="fas fa-check mr-2"></i>
|
||||
Сохранить проект
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Include Rich Text Editor -->
|
||||
<link href="https://cdn.quilljs.com/1.3.6/quill.snow.css" rel="stylesheet">
|
||||
<script src="https://cdn.quilljs.com/1.3.6/quill.min.js"></script>
|
||||
|
||||
<script>
|
||||
let quillEditor;
|
||||
let selectedTechnologies = [];
|
||||
let uploadedImages = [];
|
||||
|
||||
// Initialize page
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
initializeRichTextEditor();
|
||||
initializeImageUpload();
|
||||
initializeTechnologyInput();
|
||||
initializeFormValidation();
|
||||
setupCharacterCounters();
|
||||
});
|
||||
|
||||
// Initialize Rich Text Editor
|
||||
function initializeRichTextEditor() {
|
||||
quillEditor = new Quill('#descriptionEditor', {
|
||||
theme: 'snow',
|
||||
placeholder: 'Опишите проект: цели, задачи, решения, результаты...',
|
||||
modules: {
|
||||
toolbar: [
|
||||
[{ 'header': [1, 2, 3, false] }],
|
||||
['bold', 'italic', 'underline', 'strike'],
|
||||
['blockquote', 'code-block'],
|
||||
[{ 'list': 'ordered'}, { 'list': 'bullet' }],
|
||||
[{ 'script': 'sub'}, { 'script': 'super' }],
|
||||
[{ 'indent': '-1'}, { 'indent': '+1' }],
|
||||
['link', 'image'],
|
||||
['clean']
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
// Sync with hidden textarea
|
||||
quillEditor.on('text-change', function() {
|
||||
document.getElementById('description').value = quillEditor.root.innerHTML;
|
||||
});
|
||||
}
|
||||
|
||||
// Technology management
|
||||
function initializeTechnologyInput() {
|
||||
const input = document.getElementById('technologyInput');
|
||||
|
||||
input.addEventListener('keypress', function(e) {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
addTechnology();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function addTechnology() {
|
||||
const input = document.getElementById('technologyInput');
|
||||
const technology = input.value.trim();
|
||||
|
||||
if (technology && !selectedTechnologies.includes(technology)) {
|
||||
selectedTechnologies.push(technology);
|
||||
input.value = '';
|
||||
updateTechnologiesList();
|
||||
updateTechnologiesInput();
|
||||
}
|
||||
}
|
||||
|
||||
function addTechnologyFromList(tech) {
|
||||
if (!selectedTechnologies.includes(tech)) {
|
||||
selectedTechnologies.push(tech);
|
||||
updateTechnologiesList();
|
||||
updateTechnologiesInput();
|
||||
}
|
||||
}
|
||||
|
||||
function removeTechnology(index) {
|
||||
selectedTechnologies.splice(index, 1);
|
||||
updateTechnologiesList();
|
||||
updateTechnologiesInput();
|
||||
}
|
||||
|
||||
function updateTechnologiesList() {
|
||||
const container = document.getElementById('technologiesList');
|
||||
container.innerHTML = selectedTechnologies.map((tech, index) => `
|
||||
<span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-blue-100 text-blue-800">
|
||||
${tech}
|
||||
<button type="button" onclick="removeTechnology(${index})" class="ml-2 text-blue-600 hover:text-blue-800">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</span>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
function updateTechnologiesInput() {
|
||||
document.getElementById('technologiesHidden').value = JSON.stringify(selectedTechnologies);
|
||||
}
|
||||
|
||||
// Image upload functionality
|
||||
function initializeImageUpload() {
|
||||
const dropZone = document.getElementById('dropZone');
|
||||
const fileInput = document.getElementById('images');
|
||||
|
||||
// Drag and drop functionality
|
||||
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
|
||||
dropZone.addEventListener(eventName, preventDefaults, false);
|
||||
document.body.addEventListener(eventName, preventDefaults, false);
|
||||
});
|
||||
|
||||
['dragenter', 'dragover'].forEach(eventName => {
|
||||
dropZone.addEventListener(eventName, () => dropZone.classList.add('border-blue-500', 'bg-blue-50'), false);
|
||||
});
|
||||
|
||||
['dragleave', 'drop'].forEach(eventName => {
|
||||
dropZone.addEventListener(eventName, () => dropZone.classList.remove('border-blue-500', 'bg-blue-50'), false);
|
||||
});
|
||||
|
||||
dropZone.addEventListener('drop', handleDrop, false);
|
||||
fileInput.addEventListener('change', (e) => handleFiles(e.target.files), false);
|
||||
}
|
||||
|
||||
function preventDefaults(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
function handleDrop(e) {
|
||||
const dt = e.dataTransfer;
|
||||
const files = dt.files;
|
||||
handleFiles(files);
|
||||
}
|
||||
|
||||
async function handleFiles(files) {
|
||||
const validFiles = Array.from(files).filter(file => {
|
||||
if (!file.type.startsWith('image/')) {
|
||||
showNotification('Можно загружать только изображения', 'error');
|
||||
return false;
|
||||
}
|
||||
if (file.size > 10 * 1024 * 1024) {
|
||||
showNotification(`Файл ${file.name} слишком большой (максимум 10MB)`, 'error');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
if (validFiles.length === 0) return;
|
||||
|
||||
// Show loading state
|
||||
showLoadingState();
|
||||
|
||||
for (const file of validFiles) {
|
||||
await uploadSingleImage(file);
|
||||
}
|
||||
|
||||
hideLoadingState();
|
||||
updateImagePreview();
|
||||
}
|
||||
|
||||
async function uploadSingleImage(file) {
|
||||
return new Promise((resolve) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = function(e) {
|
||||
const imageData = {
|
||||
file: file,
|
||||
url: e.target.result,
|
||||
name: file.name,
|
||||
size: file.size
|
||||
};
|
||||
uploadedImages.push(imageData);
|
||||
resolve();
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
}
|
||||
|
||||
function updateImagePreview() {
|
||||
const container = document.getElementById('imagePreview');
|
||||
const previewContainer = document.getElementById('imagePreviewContainer');
|
||||
|
||||
if (uploadedImages.length === 0) {
|
||||
previewContainer.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
previewContainer.style.display = 'block';
|
||||
container.innerHTML = uploadedImages.map((img, index) => `
|
||||
<div class="relative group" data-index="${index}">
|
||||
<div class="aspect-w-16 aspect-h-9 rounded-lg overflow-hidden border-2 ${index === 0 ? 'border-blue-500' : 'border-gray-200'}">
|
||||
<img src="${img.url}" alt="${img.name}" class="w-full h-32 object-cover">
|
||||
</div>
|
||||
${index === 0 ? '<div class="absolute top-2 left-2 bg-blue-600 text-white text-xs px-2 py-1 rounded">Главное</div>' : ''}
|
||||
<div class="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity space-x-1">
|
||||
${index > 0 ? '<button type="button" onclick="moveImage(' + index + ', -1)" class="bg-white text-gray-600 rounded-full w-6 h-6 flex items-center justify-center text-xs shadow hover:bg-gray-50" title="Переместить влево"><i class="fas fa-chevron-left"></i></button>' : ''}
|
||||
${index < uploadedImages.length - 1 ? '<button type="button" onclick="moveImage(' + index + ', 1)" class="bg-white text-gray-600 rounded-full w-6 h-6 flex items-center justify-center text-xs shadow hover:bg-gray-50" title="Переместить вправо"><i class="fas fa-chevron-right"></i></button>' : ''}
|
||||
<button type="button" onclick="removeImage(${index})" class="bg-red-600 text-white rounded-full w-6 h-6 flex items-center justify-center text-xs shadow hover:bg-red-700" title="Удалить">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="mt-2 text-xs text-gray-600 truncate">${img.name}</div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
function moveImage(index, direction) {
|
||||
const newIndex = index + direction;
|
||||
if (newIndex >= 0 && newIndex < uploadedImages.length) {
|
||||
[uploadedImages[index], uploadedImages[newIndex]] = [uploadedImages[newIndex], uploadedImages[index]];
|
||||
updateImagePreview();
|
||||
}
|
||||
}
|
||||
|
||||
function removeImage(index) {
|
||||
uploadedImages.splice(index, 1);
|
||||
updateImagePreview();
|
||||
}
|
||||
|
||||
function clearAllImages() {
|
||||
if (confirm('Удалить все загруженные изображения?')) {
|
||||
uploadedImages = [];
|
||||
updateImagePreview();
|
||||
}
|
||||
}
|
||||
|
||||
// Character counters
|
||||
function setupCharacterCounters() {
|
||||
const shortDescInput = document.getElementById('shortDescription');
|
||||
const shortDescCounter = document.getElementById('shortDescCount');
|
||||
|
||||
shortDescInput.addEventListener('input', function() {
|
||||
shortDescCounter.textContent = `${this.value.length}/200`;
|
||||
});
|
||||
}
|
||||
|
||||
// Form validation and submission
|
||||
function initializeFormValidation() {
|
||||
const form = document.getElementById('portfolioForm');
|
||||
form.addEventListener('submit', handleFormSubmit);
|
||||
}
|
||||
|
||||
async function handleFormSubmit(e) {
|
||||
e.preventDefault();
|
||||
|
||||
// Validate required fields
|
||||
if (!validateForm()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
|
||||
// Add text fields
|
||||
const textFields = ['title', 'shortDescription', 'category', 'clientName', 'demoUrl', 'githubUrl', 'seoTitle', 'seoDescription', 'duration'];
|
||||
textFields.forEach(field => {
|
||||
const value = document.getElementById(field)?.value;
|
||||
if (value) formData.append(field, value);
|
||||
});
|
||||
|
||||
// Add description from Quill editor
|
||||
formData.append('description', quillEditor.root.innerHTML);
|
||||
|
||||
// Add technologies
|
||||
formData.append('technologies', JSON.stringify(selectedTechnologies));
|
||||
|
||||
// Add checkboxes
|
||||
formData.append('featured', document.getElementById('featured').checked);
|
||||
formData.append('isPublished', document.getElementById('isPublished').checked);
|
||||
|
||||
// Add images
|
||||
uploadedImages.forEach((img, index) => {
|
||||
formData.append('images', img.file);
|
||||
});
|
||||
|
||||
try {
|
||||
showSubmitLoading();
|
||||
|
||||
const response = await fetch('/admin/portfolio/add', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
showNotification('Проект успешно создан!', 'success');
|
||||
setTimeout(() => {
|
||||
window.location.href = '/admin/portfolio';
|
||||
}, 1500);
|
||||
} else {
|
||||
showNotification(data.message || 'Ошибка при создании проекта', 'error');
|
||||
hideSubmitLoading();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
showNotification('Ошибка при создании проекта', 'error');
|
||||
hideSubmitLoading();
|
||||
}
|
||||
}
|
||||
|
||||
function validateForm() {
|
||||
const errors = [];
|
||||
|
||||
// Required fields validation
|
||||
const title = document.getElementById('title').value.trim();
|
||||
const shortDescription = document.getElementById('shortDescription').value.trim();
|
||||
const category = document.getElementById('category').value;
|
||||
const description = quillEditor.getText().trim();
|
||||
|
||||
if (!title) errors.push('Введите название проекта');
|
||||
if (!shortDescription) errors.push('Введите краткое описание');
|
||||
if (!category) errors.push('Выберите категорию');
|
||||
if (!description) errors.push('Введите подробное описание');
|
||||
if (selectedTechnologies.length === 0) errors.push('Добавьте хотя бы одну технологию');
|
||||
if (uploadedImages.length === 0) errors.push('Загрузите хотя бы одно изображение');
|
||||
|
||||
if (errors.length > 0) {
|
||||
showNotification('Заполните все обязательные поля:\n' + errors.join('\n'), 'error');
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Save as draft
|
||||
async function saveDraft() {
|
||||
// Set isPublished to false
|
||||
document.getElementById('isPublished').checked = false;
|
||||
|
||||
// Submit form
|
||||
await handleFormSubmit({ preventDefault: () => {} });
|
||||
}
|
||||
|
||||
// Preview functionality
|
||||
function previewProject() {
|
||||
const title = document.getElementById('title').value;
|
||||
const shortDescription = document.getElementById('shortDescription').value;
|
||||
const category = document.getElementById('category').value;
|
||||
|
||||
if (!title || !shortDescription || !category) {
|
||||
showNotification('Заполните основную информацию для предпросмотра', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
// Open preview in new window/modal
|
||||
const previewData = {
|
||||
title,
|
||||
shortDescription,
|
||||
description: quillEditor.root.innerHTML,
|
||||
category,
|
||||
technologies: selectedTechnologies,
|
||||
images: uploadedImages.map(img => img.url)
|
||||
};
|
||||
|
||||
// Here you can implement preview modal or new window
|
||||
console.log('Preview data:', previewData);
|
||||
showNotification('Функция предпросмотра будет реализована позже', 'info');
|
||||
}
|
||||
|
||||
// UI helpers
|
||||
function showSubmitLoading() {
|
||||
const btn = document.getElementById('submitBtn');
|
||||
btn.innerHTML = '<i class="fas fa-spinner fa-spin mr-2"></i>Сохранение...';
|
||||
btn.disabled = true;
|
||||
}
|
||||
|
||||
function hideSubmitLoading() {
|
||||
const btn = document.getElementById('submitBtn');
|
||||
btn.innerHTML = '<i class="fas fa-check mr-2"></i>Сохранить проект';
|
||||
btn.disabled = false;
|
||||
}
|
||||
|
||||
function showLoadingState() {
|
||||
const dropZone = document.getElementById('dropZone');
|
||||
dropZone.innerHTML = '<div class="text-center"><i class="fas fa-spinner fa-spin text-2xl text-blue-600 mb-2"></i><p class="text-blue-600">Обработка изображений...</p></div>';
|
||||
}
|
||||
|
||||
function hideLoadingState() {
|
||||
const dropZone = document.getElementById('dropZone');
|
||||
dropZone.innerHTML = `
|
||||
<div class="space-y-4">
|
||||
<div class="mx-auto w-16 h-16 text-gray-400">
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<label for="images" class="cursor-pointer">
|
||||
<span class="text-blue-600 font-medium hover:text-blue-500">Загрузить изображения</span>
|
||||
<input id="images" name="images" type="file" class="sr-only" multiple accept="image/*">
|
||||
</label>
|
||||
<span class="text-gray-600"> или перетащите файлы сюда</span>
|
||||
</div>
|
||||
<p class="text-sm text-gray-500">
|
||||
PNG, JPG, WEBP до 10MB каждый. Первое изображение будет использоваться как главное.
|
||||
</p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Re-initialize file input
|
||||
document.getElementById('images').addEventListener('change', (e) => handleFiles(e.target.files), false);
|
||||
}
|
||||
|
||||
function showNotification(message, type = 'info') {
|
||||
// Create notification
|
||||
const notification = document.createElement('div');
|
||||
notification.className = `fixed top-4 right-4 z-50 px-6 py-4 rounded-lg shadow-lg text-white max-w-md transform transition-all duration-300 translate-x-full`;
|
||||
|
||||
switch(type) {
|
||||
case 'success':
|
||||
notification.classList.add('bg-green-600');
|
||||
break;
|
||||
case 'error':
|
||||
notification.classList.add('bg-red-600');
|
||||
break;
|
||||
case 'warning':
|
||||
notification.classList.add('bg-yellow-600');
|
||||
break;
|
||||
case 'info':
|
||||
default:
|
||||
notification.classList.add('bg-blue-600');
|
||||
break;
|
||||
}
|
||||
|
||||
notification.innerHTML = `
|
||||
<div class="flex items-start">
|
||||
<i class="fas ${type === 'success' ? 'fa-check-circle' : type === 'error' ? 'fa-exclamation-circle' : type === 'warning' ? 'fa-exclamation-triangle' : 'fa-info-circle'} mr-3 mt-0.5"></i>
|
||||
<div>
|
||||
<div class="font-medium">${type.charAt(0).toUpperCase() + type.slice(1)}</div>
|
||||
<div class="mt-1 text-sm opacity-90 whitespace-pre-line">${message}</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(notification);
|
||||
|
||||
// Show notification
|
||||
setTimeout(() => {
|
||||
notification.classList.remove('translate-x-full');
|
||||
}, 100);
|
||||
|
||||
// Hide notification
|
||||
setTimeout(() => {
|
||||
notification.classList.add('translate-x-full');
|
||||
setTimeout(() => {
|
||||
if (document.body.contains(notification)) {
|
||||
document.body.removeChild(notification);
|
||||
}
|
||||
}, 300);
|
||||
}, 5000);
|
||||
}
|
||||
</script>
|
||||
358
views/admin/portfolio/list.ejs
Normal file
358
views/admin/portfolio/list.ejs
Normal file
@@ -0,0 +1,358 @@
|
||||
<!-- Portfolio List -->
|
||||
<div class="bg-white shadow rounded-lg">
|
||||
<div class="px-4 py-5 sm:px-6 border-b border-gray-200">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h3 class="text-lg leading-6 font-medium text-gray-900">
|
||||
<i class="fas fa-briefcase mr-2"></i>
|
||||
Управление портфолио
|
||||
</h3>
|
||||
<p class="mt-1 text-sm text-gray-500">
|
||||
Всего проектов: <%= portfolio ? portfolio.length : 0 %>
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex space-x-3">
|
||||
<div class="flex rounded-md shadow-sm">
|
||||
<input type="text" id="searchInput" placeholder="Поиск проектов..."
|
||||
class="block w-full rounded-l-md border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm">
|
||||
<button type="button" onclick="searchProjects()"
|
||||
class="relative -ml-px inline-flex items-center px-3 py-2 rounded-r-md border border-l-0 border-gray-300 bg-gray-50 text-gray-500 hover:bg-gray-100 focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500">
|
||||
<i class="fas fa-search"></i>
|
||||
</button>
|
||||
</div>
|
||||
<select id="categoryFilter" onchange="filterByCategory()" class="rounded-md border-gray-300 py-2 pl-3 pr-10 text-base focus:border-indigo-500 focus:outline-none focus:ring-indigo-500 sm:text-sm">
|
||||
<option value="">Все категории</option>
|
||||
<option value="web-development">Веб-разработка</option>
|
||||
<option value="mobile-app">Мобильное приложение</option>
|
||||
<option value="ui-ux-design">UI/UX дизайн</option>
|
||||
<option value="e-commerce">E-commerce</option>
|
||||
<option value="enterprise">Корпоративное</option>
|
||||
<option value="other">Другое</option>
|
||||
</select>
|
||||
<a href="/admin/portfolio/add" class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md text-sm font-medium inline-flex items-center">
|
||||
<i class="fas fa-plus mr-2"></i>
|
||||
Добавить проект
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white shadow overflow-hidden sm:rounded-md">
|
||||
<ul role="list" class="divide-y divide-gray-200">
|
||||
<% if (portfolio && portfolio.length > 0) { %>
|
||||
<% portfolio.forEach(item => { %>
|
||||
<li class="portfolio-item" data-category="<%= item.category %>" data-title="<%= item.title.toLowerCase() %>">
|
||||
<div class="px-4 py-4">
|
||||
<div class="flex items-start justify-between">
|
||||
<div class="flex items-start space-x-4">
|
||||
<div class="flex-shrink-0">
|
||||
<% if (item.images && item.images.length > 0) { %>
|
||||
<img class="h-16 w-16 rounded-lg object-cover border border-gray-200" src="<%= item.images[0].url %>" alt="<%= item.title %>">
|
||||
<% } else { %>
|
||||
<div class="h-16 w-16 rounded-lg bg-gray-100 border border-gray-200 flex items-center justify-center">
|
||||
<i class="fas fa-image text-gray-400 text-xl"></i>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="flex items-center">
|
||||
<h4 class="text-base font-medium text-gray-900 truncate"><%= item.title %></h4>
|
||||
<div class="ml-3 flex items-center space-x-2">
|
||||
<% if (item.featured) { %>
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800">
|
||||
<i class="fas fa-star mr-1"></i>
|
||||
Рекомендуемое
|
||||
</span>
|
||||
<% } %>
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium <%= item.isPublished ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800' %>">
|
||||
<i class="fas <%= item.isPublished ? 'fa-check-circle' : 'fa-clock' %> mr-1"></i>
|
||||
<%= item.isPublished ? 'Опубликовано' : 'Черновик' %>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<p class="mt-1 text-sm text-gray-600 line-clamp-2"><%= item.shortDescription || 'Описание не указано' %></p>
|
||||
<div class="mt-2 flex items-center text-sm text-gray-500 space-x-4">
|
||||
<div class="flex items-center">
|
||||
<i class="fas fa-folder mr-1"></i>
|
||||
<span class="capitalize"><%= item.category.replace('-', ' ') %></span>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<i class="fas fa-calendar mr-1"></i>
|
||||
<%= new Date(item.createdAt).toLocaleDateString('ru-RU') %>
|
||||
</div>
|
||||
<% if (item.viewCount && item.viewCount > 0) { %>
|
||||
<div class="flex items-center">
|
||||
<i class="fas fa-eye mr-1"></i>
|
||||
<%= item.viewCount %> просмотров
|
||||
</div>
|
||||
<% } %>
|
||||
<% if (item.technologies && item.technologies.length > 0) { %>
|
||||
<div class="flex items-center">
|
||||
<i class="fas fa-code mr-1"></i>
|
||||
<%= item.technologies.slice(0, 2).join(', ') %><%= item.technologies.length > 2 ? '...' : '' %>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center space-x-1 ml-4">
|
||||
<% if (item.isPublished) { %>
|
||||
<a href="/portfolio/<%= item.id %>" target="_blank"
|
||||
class="inline-flex items-center justify-center w-8 h-8 rounded-md text-gray-400 hover:text-blue-600 hover:bg-blue-50 transition-colors">
|
||||
<i class="fas fa-external-link-alt text-sm"></i>
|
||||
</a>
|
||||
<% } %>
|
||||
<button onclick="togglePublish('<%= item.id %>', '<%= item.isPublished %>')"
|
||||
class="inline-flex items-center justify-center w-8 h-8 rounded-md text-gray-400 hover:text-green-600 hover:bg-green-50 transition-colors"
|
||||
title="<%= item.isPublished ? 'Скрыть' : 'Опубликовать' %>">
|
||||
<i class="fas <%= item.isPublished ? 'fa-eye-slash' : 'fa-eye' %> text-sm"></i>
|
||||
</button>
|
||||
<a href="/admin/portfolio/edit/<%= item.id %>"
|
||||
class="inline-flex items-center justify-center w-8 h-8 rounded-md text-gray-400 hover:text-indigo-600 hover:bg-indigo-50 transition-colors"
|
||||
title="Редактировать">
|
||||
<i class="fas fa-edit text-sm"></i>
|
||||
</a>
|
||||
<button onclick="duplicatePortfolio('<%= item.id %>')"
|
||||
class="inline-flex items-center justify-center w-8 h-8 rounded-md text-gray-400 hover:text-yellow-600 hover:bg-yellow-50 transition-colors"
|
||||
title="Дублировать">
|
||||
<i class="fas fa-copy text-sm"></i>
|
||||
</button>
|
||||
<button onclick="deletePortfolio('<%= item.id %>', '<%= item.title %>')"
|
||||
class="inline-flex items-center justify-center w-8 h-8 rounded-md text-gray-400 hover:text-red-600 hover:bg-red-50 transition-colors"
|
||||
title="Удалить">
|
||||
<i class="fas fa-trash text-sm"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<% }) %>
|
||||
<% } else { %>
|
||||
<li>
|
||||
<div class="px-4 py-8 text-center">
|
||||
<i class="fas fa-briefcase text-4xl text-gray-300 mb-4"></i>
|
||||
<p class="text-gray-500">Проекты не найдены</p>
|
||||
<a href="/admin/portfolio/add" class="mt-2 inline-block bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md text-sm font-medium">
|
||||
Добавить первый проект
|
||||
</a>
|
||||
</div>
|
||||
</li>
|
||||
<% } %>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<% if (pagination && pagination.total > 1) { %>
|
||||
<div class="bg-white px-4 py-3 flex items-center justify-between border-t border-gray-200 sm:px-6">
|
||||
<div class="flex-1 flex justify-between sm:hidden">
|
||||
<% if (pagination.hasPrev) { %>
|
||||
<a href="?page=<%= pagination.current - 1 %>" class="relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50">
|
||||
Предыдущая
|
||||
</a>
|
||||
<% } %>
|
||||
<% if (pagination.hasNext) { %>
|
||||
<a href="?page=<%= pagination.current + 1 %>" class="ml-3 relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50">
|
||||
Следующая
|
||||
</a>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function deletePortfolio(id, title) {
|
||||
if (confirm(`Вы уверены, что хотите удалить проект "${title}"?\n\nЭто действие нельзя отменить.`)) {
|
||||
const button = event.target.closest('button');
|
||||
const originalContent = button.innerHTML;
|
||||
button.innerHTML = '<i class="fas fa-spinner fa-spin text-sm"></i>';
|
||||
button.disabled = true;
|
||||
|
||||
fetch(`/admin/portfolio/${id}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
showNotification('Проект успешно удален', 'success');
|
||||
// Плавное удаление элемента
|
||||
const listItem = button.closest('li');
|
||||
listItem.style.opacity = '0.5';
|
||||
listItem.style.transform = 'scale(0.95)';
|
||||
setTimeout(() => {
|
||||
listItem.remove();
|
||||
updateProjectCount();
|
||||
}, 300);
|
||||
} else {
|
||||
showNotification(data.message || 'Ошибка при удалении проекта', 'error');
|
||||
button.innerHTML = originalContent;
|
||||
button.disabled = false;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
showNotification('Ошибка при удалении проекта', 'error');
|
||||
button.innerHTML = originalContent;
|
||||
button.disabled = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function togglePublish(id, currentStatus) {
|
||||
const button = event.target.closest('button');
|
||||
const originalContent = button.innerHTML;
|
||||
button.innerHTML = '<i class="fas fa-spinner fa-spin text-sm"></i>';
|
||||
button.disabled = true;
|
||||
|
||||
fetch(`/admin/portfolio/${id}/toggle-publish`, {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
showNotification(data.message, 'success');
|
||||
// Обновляем интерфейс
|
||||
const listItem = button.closest('li');
|
||||
const statusSpan = listItem.querySelector('.inline-flex');
|
||||
const newStatus = data.isPublished;
|
||||
|
||||
// Обновляем иконку кнопки
|
||||
button.innerHTML = `<i class="fas ${newStatus ? 'fa-eye-slash' : 'fa-eye'} text-sm"></i>`;
|
||||
button.title = newStatus ? 'Скрыть' : 'Опубликовать';
|
||||
|
||||
// Обновляем статус
|
||||
statusSpan.className = `inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${newStatus ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800'}`;
|
||||
statusSpan.innerHTML = `<i class="fas ${newStatus ? 'fa-check-circle' : 'fa-clock'} mr-1"></i>${newStatus ? 'Опубликовано' : 'Черновик'}`;
|
||||
|
||||
button.disabled = false;
|
||||
} else {
|
||||
showNotification(data.message || 'Ошибка при изменении статуса', 'error');
|
||||
button.innerHTML = originalContent;
|
||||
button.disabled = false;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
showNotification('Ошибка при изменении статуса', 'error');
|
||||
button.innerHTML = originalContent;
|
||||
button.disabled = false;
|
||||
});
|
||||
}
|
||||
|
||||
function duplicatePortfolio(id) {
|
||||
if (confirm('Создать копию этого проекта?')) {
|
||||
const button = event.target.closest('button');
|
||||
const originalContent = button.innerHTML;
|
||||
button.innerHTML = '<i class="fas fa-spinner fa-spin text-sm"></i>';
|
||||
button.disabled = true;
|
||||
|
||||
// Здесь можно добавить API для дублирования
|
||||
showNotification('Функция дублирования будет добавлена позже', 'info');
|
||||
button.innerHTML = originalContent;
|
||||
button.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
function searchProjects() {
|
||||
const searchTerm = document.getElementById('searchInput').value.toLowerCase();
|
||||
const items = document.querySelectorAll('.portfolio-item');
|
||||
|
||||
items.forEach(item => {
|
||||
const title = item.dataset.title;
|
||||
if (title.includes(searchTerm)) {
|
||||
item.style.display = 'block';
|
||||
} else {
|
||||
item.style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
updateProjectCount();
|
||||
}
|
||||
|
||||
function filterByCategory() {
|
||||
const selectedCategory = document.getElementById('categoryFilter').value;
|
||||
const items = document.querySelectorAll('.portfolio-item');
|
||||
|
||||
items.forEach(item => {
|
||||
const category = item.dataset.category;
|
||||
if (!selectedCategory || category === selectedCategory) {
|
||||
item.style.display = 'block';
|
||||
} else {
|
||||
item.style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
updateProjectCount();
|
||||
}
|
||||
|
||||
function updateProjectCount() {
|
||||
const visibleItems = document.querySelectorAll('.portfolio-item[style="display: block"], .portfolio-item:not([style*="display: none"])').length;
|
||||
const totalItems = document.querySelectorAll('.portfolio-item').length;
|
||||
const countElement = document.querySelector('h3 + p');
|
||||
if (countElement) {
|
||||
countElement.textContent = `Показано проектов: ${visibleItems} из ${totalItems}`;
|
||||
}
|
||||
}
|
||||
|
||||
function showNotification(message, type = 'info') {
|
||||
// Создаем уведомление
|
||||
const notification = document.createElement('div');
|
||||
notification.className = `fixed top-4 right-4 z-50 px-4 py-3 rounded-md shadow-lg text-white max-w-sm transform transition-all duration-300 translate-x-full`;
|
||||
|
||||
switch(type) {
|
||||
case 'success':
|
||||
notification.classList.add('bg-green-600');
|
||||
break;
|
||||
case 'error':
|
||||
notification.classList.add('bg-red-600');
|
||||
break;
|
||||
case 'info':
|
||||
notification.classList.add('bg-blue-600');
|
||||
break;
|
||||
default:
|
||||
notification.classList.add('bg-gray-600');
|
||||
}
|
||||
|
||||
notification.innerHTML = `
|
||||
<div class="flex items-center">
|
||||
<i class="fas ${type === 'success' ? 'fa-check-circle' : type === 'error' ? 'fa-exclamation-circle' : 'fa-info-circle'} mr-2"></i>
|
||||
<span>${message}</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(notification);
|
||||
|
||||
// Показываем уведомление
|
||||
setTimeout(() => {
|
||||
notification.classList.remove('translate-x-full');
|
||||
}, 100);
|
||||
|
||||
// Скрываем уведомление через 3 секунды
|
||||
setTimeout(() => {
|
||||
notification.classList.add('translate-x-full');
|
||||
setTimeout(() => {
|
||||
document.body.removeChild(notification);
|
||||
}, 300);
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
// Обработчик для поиска по Enter
|
||||
document.getElementById('searchInput').addEventListener('keyup', function(event) {
|
||||
if (event.key === 'Enter') {
|
||||
searchProjects();
|
||||
}
|
||||
});
|
||||
|
||||
// Инициализация
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
updateProjectCount();
|
||||
});
|
||||
</script>
|
||||
121
views/admin/services/list.ejs
Normal file
121
views/admin/services/list.ejs
Normal file
@@ -0,0 +1,121 @@
|
||||
<!-- Services List -->
|
||||
<div class="bg-white shadow rounded-lg">
|
||||
<div class="px-4 py-5 sm:px-6 border-b border-gray-200">
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="text-lg leading-6 font-medium text-gray-900">
|
||||
<i class="fas fa-cog mr-2"></i>
|
||||
Управление услугами
|
||||
</h3>
|
||||
<a href="/admin/services/add" class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md text-sm font-medium">
|
||||
<i class="fas fa-plus mr-1"></i>
|
||||
Добавить услугу
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white shadow overflow-hidden sm:rounded-md">
|
||||
<ul role="list" class="divide-y divide-gray-200">
|
||||
<% if (services && services.length > 0) { %>
|
||||
<% services.forEach(service => { %>
|
||||
<li>
|
||||
<div class="px-4 py-4 flex items-center justify-between">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0 h-10 w-10">
|
||||
<div class="h-10 w-10 rounded-full bg-blue-100 flex items-center justify-center">
|
||||
<i class="<%= service.icon || 'fas fa-cog' %> text-blue-600"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<div class="flex items-center">
|
||||
<div class="text-sm font-medium text-gray-900"><%= service.name %></div>
|
||||
<% if (service.featured) { %>
|
||||
<span class="ml-2 inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800">
|
||||
<i class="fas fa-star mr-1"></i>
|
||||
Рекомендуемая
|
||||
</span>
|
||||
<% } %>
|
||||
<% if (!service.isActive) { %>
|
||||
<span class="ml-2 inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800">
|
||||
Неактивна
|
||||
</span>
|
||||
<% } %>
|
||||
</div>
|
||||
<div class="text-sm text-gray-500">
|
||||
<%= service.category %> •
|
||||
<% if (service.pricing && service.pricing.basePrice) { %>
|
||||
от $<%= service.pricing.basePrice %>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<a href="/services#<%= service.id %>" target="_blank" class="text-blue-600 hover:text-blue-900">
|
||||
<i class="fas fa-external-link-alt"></i>
|
||||
</a>
|
||||
<a href="/admin/services/edit/<%= service.id %>" class="text-indigo-600 hover:text-indigo-900">
|
||||
<i class="fas fa-edit"></i>
|
||||
</a>
|
||||
<button onclick="deleteService('<%= service.id %>')" class="text-red-600 hover:text-red-900">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<% }) %>
|
||||
<% } else { %>
|
||||
<li>
|
||||
<div class="px-4 py-8 text-center">
|
||||
<i class="fas fa-cog text-4xl text-gray-300 mb-4"></i>
|
||||
<p class="text-gray-500">Услуги не найдены</p>
|
||||
<a href="/admin/services/add" class="mt-2 inline-block bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md text-sm font-medium">
|
||||
Добавить первую услугу
|
||||
</a>
|
||||
</div>
|
||||
</li>
|
||||
<% } %>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<% if (pagination && pagination.total > 1) { %>
|
||||
<div class="bg-white px-4 py-3 flex items-center justify-between border-t border-gray-200 sm:px-6">
|
||||
<div class="flex-1 flex justify-between sm:hidden">
|
||||
<% if (pagination.hasPrev) { %>
|
||||
<a href="?page=<%= pagination.current - 1 %>" class="relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50">
|
||||
Предыдущая
|
||||
</a>
|
||||
<% } %>
|
||||
<% if (pagination.hasNext) { %>
|
||||
<a href="?page=<%= pagination.current + 1 %>" class="ml-3 relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50">
|
||||
Следующая
|
||||
</a>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function deleteService(id) {
|
||||
if (confirm('Вы уверены, что хотите удалить эту услугу?')) {
|
||||
fetch(`/api/admin/services/${id}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
location.reload();
|
||||
} else {
|
||||
alert('Ошибка при удалении услуги');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
alert('Ошибка при удалении услуги');
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
350
views/admin/settings.ejs
Normal file
350
views/admin/settings.ejs
Normal file
@@ -0,0 +1,350 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Настройки сайта - SmartSolTech Admin</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.4.0/css/all.min.css">
|
||||
|
||||
<!-- Custom CSS -->
|
||||
<link rel="stylesheet" href="/css/main.css">
|
||||
<link rel="stylesheet" href="/css/fixes.css">
|
||||
</head>
|
||||
<body class="bg-gray-100">
|
||||
<!-- Admin Header -->
|
||||
<header class="bg-white shadow-sm border-b">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex justify-between items-center h-16">
|
||||
<div class="flex items-center">
|
||||
<h1 class="text-xl font-semibold text-gray-900">
|
||||
<i class="fas fa-cogs mr-2"></i>
|
||||
SmartSolTech Admin
|
||||
</h1>
|
||||
</div>
|
||||
<div class="flex items-center space-x-4">
|
||||
<span class="text-sm text-gray-600">
|
||||
Добро пожаловать, <%= user ? user.name : 'Admin' %>!
|
||||
</span>
|
||||
<a href="/" class="text-gray-500 hover:text-gray-700">
|
||||
<i class="fas fa-external-link-alt mr-1"></i>
|
||||
Посмотреть сайт
|
||||
</a>
|
||||
<form action="/admin/logout" method="post" class="inline">
|
||||
<button type="submit" class="text-red-600 hover:text-red-800">
|
||||
<i class="fas fa-sign-out-alt mr-1"></i>
|
||||
Выход
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="flex">
|
||||
<!-- Admin Sidebar -->
|
||||
<aside class="w-64 bg-white shadow-sm admin-sidebar min-h-screen">
|
||||
<nav class="mt-5 px-2">
|
||||
<div class="space-y-1">
|
||||
<a href="/admin/dashboard" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-600 hover:bg-gray-50 hover:text-gray-900">
|
||||
<i class="fas fa-tachometer-alt mr-3"></i>
|
||||
Панель управления
|
||||
</a>
|
||||
<a href="/admin/portfolio" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-600 hover:bg-gray-50 hover:text-gray-900">
|
||||
<i class="fas fa-briefcase mr-3"></i>
|
||||
Портфолио
|
||||
</a>
|
||||
<a href="/admin/services" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-600 hover:bg-gray-50 hover:text-gray-900">
|
||||
<i class="fas fa-cog mr-3"></i>
|
||||
Услуги
|
||||
</a>
|
||||
<a href="/admin/contacts" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-600 hover:bg-gray-50 hover:text-gray-900">
|
||||
<i class="fas fa-envelope mr-3"></i>
|
||||
Сообщения
|
||||
</a>
|
||||
<a href="/admin/media" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-600 hover:bg-gray-50 hover:text-gray-900">
|
||||
<i class="fas fa-images mr-3"></i>
|
||||
Медиа
|
||||
</a>
|
||||
<a href="/admin/settings" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md bg-blue-100 text-blue-700">
|
||||
<i class="fas fa-cogs mr-3"></i>
|
||||
Настройки
|
||||
</a>
|
||||
<a href="/admin/telegram" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-600 hover:bg-gray-50 hover:text-gray-900">
|
||||
<i class="fab fa-telegram mr-3"></i>
|
||||
Telegram Bot
|
||||
</a>
|
||||
<a href="/admin/banner-editor" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-600 hover:bg-gray-50 hover:text-gray-900">
|
||||
<i class="fas fa-paint-brush mr-3"></i>
|
||||
Редактор баннеров
|
||||
</a>
|
||||
</div>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="flex-1 p-8">
|
||||
<div class="space-y-6">
|
||||
<!-- Header -->
|
||||
<div class="bg-white shadow rounded-lg p-6">
|
||||
<h1 class="text-2xl font-bold text-gray-900 flex items-center">
|
||||
<i class="fas fa-cogs mr-3 text-blue-600"></i>
|
||||
Настройки сайта
|
||||
</h1>
|
||||
<p class="mt-2 text-gray-600">Управление основными параметрами сайта</p>
|
||||
</div>
|
||||
|
||||
<!-- Site Settings -->
|
||||
<div class="bg-white shadow rounded-lg">
|
||||
<div class="px-4 py-5 sm:px-6 border-b border-gray-200">
|
||||
<h3 class="text-lg leading-6 font-medium text-gray-900">
|
||||
<i class="fas fa-cogs mr-2"></i>
|
||||
Настройки сайта
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<form id="settingsForm" class="p-6">
|
||||
<div class="space-y-8">
|
||||
<!-- Basic Settings -->
|
||||
<div>
|
||||
<h4 class="text-lg font-medium text-gray-900 mb-4">Основные настройки</h4>
|
||||
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2">
|
||||
<div class="sm:col-span-2">
|
||||
<label for="siteName" class="block text-sm font-medium text-gray-700">Название сайта</label>
|
||||
<input type="text" name="siteName" id="siteName"
|
||||
value="<%= settings.siteName || 'SmartSolTech' %>"
|
||||
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500">
|
||||
</div>
|
||||
|
||||
<div class="sm:col-span-2">
|
||||
<label for="siteDescription" class="block text-sm font-medium text-gray-700">Описание сайта</label>
|
||||
<textarea name="siteDescription" id="siteDescription" rows="3"
|
||||
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500"><%= settings.siteDescription || '' %></textarea>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="logo" class="block text-sm font-medium text-gray-700">Логотип</label>
|
||||
<input type="file" name="logo" id="logo" accept="image/*"
|
||||
class="mt-1 block w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 file:text-sm file:font-semibold file:bg-blue-50 file:text-blue-700 hover:file:bg-blue-100">
|
||||
<% if (settings.logo) { %>
|
||||
<img src="<%= settings.logo %>" alt="Current logo" class="mt-2 h-16 w-auto">
|
||||
<% } %>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="favicon" class="block text-sm font-medium text-gray-700">Favicon</label>
|
||||
<input type="file" name="favicon" id="favicon" accept="image/*"
|
||||
class="mt-1 block w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 file:text-sm file:font-semibold file:bg-blue-50 file:text-blue-700 hover:file:bg-blue-100">
|
||||
<% if (settings.favicon) { %>
|
||||
<img src="<%= settings.favicon %>" alt="Current favicon" class="mt-2 h-8 w-8">
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Contact Information -->
|
||||
<div>
|
||||
<h4 class="text-lg font-medium text-gray-900 mb-4">Контактная информация</h4>
|
||||
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2">
|
||||
<div>
|
||||
<label for="contactEmail" class="block text-sm font-medium text-gray-700">Email</label>
|
||||
<input type="email" name="contact.email" id="contactEmail"
|
||||
value="<%= settings.contact?.email || '' %>"
|
||||
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="contactPhone" class="block text-sm font-medium text-gray-700">Телефон</label>
|
||||
<input type="tel" name="contact.phone" id="contactPhone"
|
||||
value="<%= settings.contact?.phone || '' %>"
|
||||
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500">
|
||||
</div>
|
||||
|
||||
<div class="sm:col-span-2">
|
||||
<label for="contactAddress" class="block text-sm font-medium text-gray-700">Адрес</label>
|
||||
<textarea name="contact.address" id="contactAddress" rows="2"
|
||||
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500"><%= settings.contact?.address || '' %></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Social Media -->
|
||||
<div>
|
||||
<h4 class="text-lg font-medium text-gray-900 mb-4">Социальные сети</h4>
|
||||
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2">
|
||||
<div>
|
||||
<label for="socialFacebook" class="block text-sm font-medium text-gray-700">Facebook</label>
|
||||
<input type="url" name="social.facebook" id="socialFacebook"
|
||||
value="<%= settings.social?.facebook || '' %>"
|
||||
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="socialTwitter" class="block text-sm font-medium text-gray-700">Twitter</label>
|
||||
<input type="url" name="social.twitter" id="socialTwitter"
|
||||
value="<%= settings.social?.twitter || '' %>"
|
||||
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="socialInstagram" class="block text-sm font-medium text-gray-700">Instagram</label>
|
||||
<input type="url" name="social.instagram" id="socialInstagram"
|
||||
value="<%= settings.social?.instagram || '' %>"
|
||||
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="socialLinkedin" class="block text-sm font-medium text-gray-700">LinkedIn</label>
|
||||
<input type="url" name="social.linkedin" id="socialLinkedin"
|
||||
value="<%= settings.social?.linkedin || '' %>"
|
||||
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Telegram Bot Settings -->
|
||||
<div>
|
||||
<h4 class="text-lg font-medium text-gray-900 mb-4">Telegram Bot</h4>
|
||||
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2">
|
||||
<div>
|
||||
<label for="telegramBotToken" class="block text-sm font-medium text-gray-700">Bot Token</label>
|
||||
<input type="text" name="telegram.botToken" id="telegramBotToken"
|
||||
value="<%= settings.telegram?.botToken || '' %>"
|
||||
placeholder="123456789:AABBccDDeeFFggHHiiJJkkLLmmNNooP"
|
||||
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500">
|
||||
<p class="mt-1 text-sm text-gray-500">Получите токен у @BotFather</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="telegramChatId" class="block text-sm font-medium text-gray-700">Chat ID</label>
|
||||
<input type="text" name="telegram.chatId" id="telegramChatId"
|
||||
value="<%= settings.telegram?.chatId || '' %>"
|
||||
placeholder="-123456789"
|
||||
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500">
|
||||
<p class="mt-1 text-sm text-gray-500">ID чата для уведомлений</p>
|
||||
</div>
|
||||
|
||||
<div class="sm:col-span-2">
|
||||
<button type="button" id="testTelegram" class="bg-green-500 hover:bg-green-600 text-white px-4 py-2 rounded-md text-sm font-medium">
|
||||
<i class="fab fa-telegram-plane mr-1"></i>
|
||||
Проверить соединение
|
||||
</button>
|
||||
<div id="telegramStatus" class="mt-2"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SEO Settings -->
|
||||
<div>
|
||||
<h4 class="text-lg font-medium text-gray-900 mb-4">SEO настройки</h4>
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label for="seoTitle" class="block text-sm font-medium text-gray-700">Meta Title</label>
|
||||
<input type="text" name="seo.title" id="seoTitle"
|
||||
value="<%= settings.seo?.title || '' %>"
|
||||
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="seoDescription" class="block text-sm font-medium text-gray-700">Meta Description</label>
|
||||
<textarea name="seo.description" id="seoDescription" rows="3"
|
||||
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500"><%= settings.seo?.description || '' %></textarea>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="seoKeywords" class="block text-sm font-medium text-gray-700">Keywords</label>
|
||||
<input type="text" name="seo.keywords" id="seoKeywords"
|
||||
value="<%= settings.seo?.keywords || '' %>"
|
||||
placeholder="веб-разработка, мобильные приложения, дизайн"
|
||||
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Submit Button -->
|
||||
<div class="mt-8 flex justify-end">
|
||||
<button type="submit" class="bg-blue-600 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
|
||||
<i class="fas fa-save mr-1"></i>
|
||||
Сохранить настройки
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.getElementById('settingsForm').addEventListener('submit', async function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const formData = new FormData(this);
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/admin/settings', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
alert('Настройки успешно сохранены');
|
||||
location.reload();
|
||||
} else {
|
||||
alert('Ошибка при сохранении настроек: ' + data.message);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
alert('Ошибка при сохранении настроек');
|
||||
}
|
||||
});
|
||||
|
||||
// Test Telegram connection
|
||||
document.getElementById('testTelegram').addEventListener('click', async function() {
|
||||
const botToken = document.getElementById('telegramBotToken').value;
|
||||
const chatId = document.getElementById('telegramChatId').value;
|
||||
const statusDiv = document.getElementById('telegramStatus');
|
||||
|
||||
if (!botToken || !chatId) {
|
||||
statusDiv.innerHTML = '<p class="text-red-600">Пожалуйста, заполните Bot Token и Chat ID</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
this.disabled = true;
|
||||
this.innerHTML = '<i class="fas fa-spinner fa-spin mr-1"></i> Проверка...';
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/admin/telegram/test', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ botToken, chatId })
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
statusDiv.innerHTML = '<p class="text-green-600"><i class="fas fa-check mr-1"></i> Соединение установлено успешно!</p>';
|
||||
} else {
|
||||
statusDiv.innerHTML = `<p class="text-red-600"><i class="fas fa-times mr-1"></i> Ошибка: ${data.message}</p>`;
|
||||
}
|
||||
} catch (error) {
|
||||
statusDiv.innerHTML = '<p class="text-red-600"><i class="fas fa-times mr-1"></i> Ошибка соединения</p>';
|
||||
}
|
||||
|
||||
this.disabled = false;
|
||||
this.innerHTML = '<i class="fab fa-telegram-plane mr-1"></i> Проверить соединение';
|
||||
});
|
||||
</script>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- JavaScript -->
|
||||
<script src="/js/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
885
views/admin/telegram.ejs
Normal file
885
views/admin/telegram.ejs
Normal file
@@ -0,0 +1,885 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Telegram Bot - SmartSolTech Admin</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.4.0/css/all.min.css">
|
||||
|
||||
<!-- Custom CSS -->
|
||||
<link rel="stylesheet" href="/css/main.css">
|
||||
<link rel="stylesheet" href="/css/fixes.css">
|
||||
</head>
|
||||
<body class="bg-gray-100">
|
||||
<!-- Admin Header -->
|
||||
<header class="bg-white shadow-sm border-b">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex justify-between items-center h-16">
|
||||
<div class="flex items-center">
|
||||
<h1 class="text-xl font-semibold text-gray-900">
|
||||
<i class="fas fa-cogs mr-2"></i>
|
||||
SmartSolTech Admin
|
||||
</h1>
|
||||
</div>
|
||||
<div class="flex items-center space-x-4">
|
||||
<span class="text-sm text-gray-600">
|
||||
Добро пожаловать, <%= user ? user.name : 'Admin' %>!
|
||||
</span>
|
||||
<a href="/" class="text-gray-500 hover:text-gray-700">
|
||||
<i class="fas fa-external-link-alt mr-1"></i>
|
||||
Посмотреть сайт
|
||||
</a>
|
||||
<form action="/admin/logout" method="post" class="inline">
|
||||
<button type="submit" class="text-red-600 hover:text-red-800">
|
||||
<i class="fas fa-sign-out-alt mr-1"></i>
|
||||
Выход
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="flex">
|
||||
<!-- Admin Sidebar -->
|
||||
<aside class="w-64 bg-white shadow-sm admin-sidebar min-h-screen">
|
||||
<nav class="mt-5 px-2">
|
||||
<div class="space-y-1">
|
||||
<a href="/admin/dashboard" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-600 hover:bg-gray-50 hover:text-gray-900">
|
||||
<i class="fas fa-tachometer-alt mr-3"></i>
|
||||
Панель управления
|
||||
</a>
|
||||
<a href="/admin/portfolio" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-600 hover:bg-gray-50 hover:text-gray-900">
|
||||
<i class="fas fa-briefcase mr-3"></i>
|
||||
Портфолио
|
||||
</a>
|
||||
<a href="/admin/services" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-600 hover:bg-gray-50 hover:text-gray-900">
|
||||
<i class="fas fa-cog mr-3"></i>
|
||||
Услуги
|
||||
</a>
|
||||
<a href="/admin/contacts" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-600 hover:bg-gray-50 hover:text-gray-900">
|
||||
<i class="fas fa-envelope mr-3"></i>
|
||||
Сообщения
|
||||
</a>
|
||||
<a href="/admin/media" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-600 hover:bg-gray-50 hover:text-gray-900">
|
||||
<i class="fas fa-images mr-3"></i>
|
||||
Медиа
|
||||
</a>
|
||||
<a href="/admin/settings" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-600 hover:bg-gray-50 hover:text-gray-900">
|
||||
<i class="fas fa-cogs mr-3"></i>
|
||||
Настройки
|
||||
</a>
|
||||
<a href="/admin/telegram" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md bg-blue-100 text-blue-700">
|
||||
<i class="fab fa-telegram mr-3"></i>
|
||||
Telegram Bot
|
||||
</a>
|
||||
<a href="/admin/banner-editor" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-600 hover:bg-gray-50 hover:text-gray-900">
|
||||
<i class="fas fa-paint-brush mr-3"></i>
|
||||
Редактор баннеров
|
||||
</a>
|
||||
</div>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="flex-1 p-8">
|
||||
<div class="space-y-6">
|
||||
<!-- Header -->
|
||||
<div class="bg-white shadow rounded-lg p-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-gray-900 flex items-center">
|
||||
<i class="fab fa-telegram mr-3 text-blue-500"></i>
|
||||
Telegram Bot
|
||||
</h1>
|
||||
<p class="mt-2 text-gray-600">Настройка и управление уведомлениями через Telegram</p>
|
||||
</div>
|
||||
<div class="flex items-center space-x-3">
|
||||
<div class="flex items-center">
|
||||
<div class="w-3 h-3 rounded-full <%= botConfigured ? 'bg-green-500' : 'bg-red-500' %> mr-2"></div>
|
||||
<span class="text-sm font-medium <%= botConfigured ? 'text-green-700' : 'text-red-700' %>">
|
||||
<%= botConfigured ? 'Подключен' : 'Не настроен' %>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bot Configuration -->
|
||||
<div class="bg-white shadow rounded-lg p-6">
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-6">
|
||||
<i class="fas fa-cog mr-2 text-blue-500"></i>
|
||||
Конфигурация бота
|
||||
</h3>
|
||||
|
||||
<form id="bot-config-form" class="space-y-6">
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<!-- Bot Token -->
|
||||
<div>
|
||||
<label for="botToken" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
Токен бота *
|
||||
</label>
|
||||
<div class="relative">
|
||||
<input type="password" id="botToken" name="botToken"
|
||||
value="<%= botToken %>"
|
||||
placeholder="1234567890:ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
class="block w-full pr-10 border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
||||
<button type="button" onclick="toggleTokenVisibility()"
|
||||
class="absolute inset-y-0 right-0 pr-3 flex items-center">
|
||||
<i id="token-eye" class="fas fa-eye text-gray-400"></i>
|
||||
</button>
|
||||
</div>
|
||||
<p class="mt-1 text-sm text-gray-500">
|
||||
Получите токен от <a href="https://t.me/BotFather" target="_blank" class="text-blue-600 underline">@BotFather</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Default Chat ID -->
|
||||
<div>
|
||||
<label for="chatId" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
ID чата по умолчанию
|
||||
</label>
|
||||
<input type="text" id="chatId" name="chatId"
|
||||
value="<%= chatId %>"
|
||||
placeholder="-1001234567890"
|
||||
class="block w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
||||
<p class="mt-1 text-sm text-gray-500">
|
||||
Оставьте пустым, если будете выбирать чат из списка
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex space-x-3">
|
||||
<button type="submit" class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg transition-colors">
|
||||
<i class="fas fa-save mr-2"></i>
|
||||
Сохранить настройки
|
||||
</button>
|
||||
<button type="button" onclick="getBotInfo()" class="bg-gray-600 hover:bg-gray-700 text-white px-4 py-2 rounded-lg transition-colors">
|
||||
<i class="fas fa-info-circle mr-2"></i>
|
||||
Получить информацию о боте
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div id="config-result" class="mt-4 hidden"></div>
|
||||
</div>
|
||||
|
||||
<% if (botConfigured) { %>
|
||||
<!-- Bot Information -->
|
||||
<div class="bg-white shadow rounded-lg p-6">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h3 class="text-lg font-semibold text-gray-900">
|
||||
<i class="fas fa-robot mr-2 text-green-500"></i>
|
||||
Информация о боте
|
||||
</h3>
|
||||
<button onclick="refreshBotInfo()" class="text-blue-600 hover:text-blue-800 text-sm">
|
||||
<i class="fas fa-refresh mr-1"></i>
|
||||
Обновить
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="bot-info-container">
|
||||
<% if (botInfo) { %>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<div class="bg-gray-50 p-4 rounded-lg">
|
||||
<div class="text-sm text-gray-600">Имя бота</div>
|
||||
<div class="text-lg font-semibold text-gray-900">@<%= botInfo.username %></div>
|
||||
</div>
|
||||
<div class="bg-gray-50 p-4 rounded-lg">
|
||||
<div class="text-sm text-gray-600">Отображаемое имя</div>
|
||||
<div class="text-lg font-semibold text-gray-900"><%= botInfo.first_name %></div>
|
||||
</div>
|
||||
<div class="bg-gray-50 p-4 rounded-lg">
|
||||
<div class="text-sm text-gray-600">ID бота</div>
|
||||
<div class="text-lg font-semibold text-gray-900"><%= botInfo.id %></div>
|
||||
</div>
|
||||
<div class="bg-gray-50 p-4 rounded-lg">
|
||||
<div class="text-sm text-gray-600">Может читать сообщения</div>
|
||||
<div class="text-lg font-semibold <%= botInfo.can_read_all_group_messages ? 'text-green-600' : 'text-red-600' %>">
|
||||
<%= botInfo.can_read_all_group_messages ? 'Да' : 'Нет' %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% } else { %>
|
||||
<div class="text-center py-4 text-gray-500">
|
||||
<i class="fas fa-robot text-4xl text-gray-300 mb-2"></i>
|
||||
<p>Настройте токен бота для получения информации</p>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Available Chats -->
|
||||
<div class="bg-white shadow rounded-lg p-6">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h3 class="text-lg font-semibold text-gray-900">
|
||||
<i class="fas fa-comments mr-2 text-blue-500"></i>
|
||||
Доступные чаты
|
||||
</h3>
|
||||
<button onclick="discoverChats()" class="bg-blue-600 hover:bg-blue-700 text-white px-3 py-1 rounded text-sm transition-colors">
|
||||
<i class="fas fa-search mr-1"></i>
|
||||
Найти чаты
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="available-chats-container">
|
||||
<% if (availableChats && availableChats.length > 0) { %>
|
||||
<div class="grid gap-3">
|
||||
<% availableChats.forEach(chat => { %>
|
||||
<div class="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
|
||||
<div class="flex items-center space-x-3">
|
||||
<div class="w-8 h-8 bg-blue-100 rounded-full flex items-center justify-center">
|
||||
<i class="fas <%= chat.type === 'group' || chat.type === 'supergroup' ? 'fa-users' : 'fa-user' %> text-blue-600 text-sm"></i>
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-medium text-gray-900"><%= chat.title %></div>
|
||||
<div class="text-sm text-gray-500">
|
||||
<%= chat.type %> • ID: <%= chat.id %>
|
||||
<% if (chat.username) { %>• @<%= chat.username %><% } %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button onclick="selectChat('<%= chat.id %>', '<%= chat.title %>')"
|
||||
class="text-blue-600 hover:text-blue-800 text-sm">
|
||||
Выбрать
|
||||
</button>
|
||||
</div>
|
||||
<% }); %>
|
||||
</div>
|
||||
<% } else { %>
|
||||
<div class="text-center py-8 text-gray-500">
|
||||
<i class="fas fa-comments text-4xl text-gray-300 mb-4"></i>
|
||||
<p class="mb-2">Чаты не найдены</p>
|
||||
<p class="text-sm">Отправьте боту сообщение или добавьте его в группу, затем нажмите "Найти чаты"</p>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
<% } %>
|
||||
|
||||
<!-- Message Sender -->
|
||||
<div class="bg-white shadow rounded-lg p-6">
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-6">
|
||||
<i class="fas fa-paper-plane mr-2 text-purple-500"></i>
|
||||
Отправить сообщение
|
||||
</h3>
|
||||
|
||||
<form id="send-message-form" class="space-y-6">
|
||||
<!-- Message Content -->
|
||||
<div>
|
||||
<label for="messageText" class="block text-sm font-medium text-gray-700 mb-2">
|
||||
Текст сообщения *
|
||||
</label>
|
||||
<textarea id="messageText" name="message" rows="4" required
|
||||
placeholder="Введите сообщение..."
|
||||
class="block w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:border-blue-500"></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Recipients -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">
|
||||
Получатели
|
||||
</label>
|
||||
<div id="recipients-container" class="space-y-2">
|
||||
<% if (availableChats && availableChats.length > 0) { %>
|
||||
<% availableChats.forEach(chat => { %>
|
||||
<label class="flex items-center">
|
||||
<input type="checkbox" name="chatIds" value="<%= chat.id %>"
|
||||
class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded">
|
||||
<span class="ml-2 text-sm text-gray-900">
|
||||
<%= chat.title %>
|
||||
<span class="text-gray-500">(<%= chat.type %>)</span>
|
||||
</span>
|
||||
</label>
|
||||
<% }); %>
|
||||
<% } else { %>
|
||||
<div class="text-sm text-gray-500">
|
||||
<i class="fas fa-info-circle mr-1"></i>
|
||||
Сообщение будет отправлено в чат по умолчанию
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Message Options -->
|
||||
<div class="bg-gray-50 p-4 rounded-lg">
|
||||
<h4 class="text-sm font-medium text-gray-900 mb-3">Настройки сообщения</h4>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<label class="flex items-center">
|
||||
<input type="checkbox" name="disableWebPagePreview"
|
||||
class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded">
|
||||
<span class="ml-2 text-sm text-gray-900">Отключить предпросмотр ссылок</span>
|
||||
</label>
|
||||
<label class="flex items-center">
|
||||
<input type="checkbox" name="disableNotification"
|
||||
class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded">
|
||||
<span class="ml-2 text-sm text-gray-900">Тихое уведомление</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<label for="parseMode" class="block text-sm font-medium text-gray-700 mb-1">
|
||||
Формат текста
|
||||
</label>
|
||||
<select name="parseMode" id="parseMode"
|
||||
class="block w-full border border-gray-300 rounded-md px-3 py-2 text-sm">
|
||||
<option value="HTML">HTML</option>
|
||||
<option value="Markdown">Markdown</option>
|
||||
<option value="">Обычный текст</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Submit Buttons -->
|
||||
<div class="flex justify-between">
|
||||
<button type="button" onclick="previewMessage()"
|
||||
class="inline-flex items-center px-4 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50">
|
||||
<i class="fas fa-eye mr-2"></i>
|
||||
Предпросмотр
|
||||
</button>
|
||||
<div class="space-x-3">
|
||||
<button type="button" onclick="testConnection()"
|
||||
class="inline-flex items-center px-4 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50">
|
||||
<i class="fas fa-vial mr-2"></i>
|
||||
Тест соединения
|
||||
</button>
|
||||
<button type="submit" id="sendMessageBtn"
|
||||
class="inline-flex items-center px-6 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700">
|
||||
<i class="fas fa-paper-plane mr-2"></i>
|
||||
Отправить сообщение
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div id="send-result" class="mt-4 hidden"></div>
|
||||
</div>
|
||||
<!-- Bot Status and Controls -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<!-- Connection Test -->
|
||||
<div class="bg-white shadow rounded-lg p-6">
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-4">
|
||||
<i class="fas fa-plug mr-2 text-green-500"></i>
|
||||
Проверка подключения
|
||||
</h3>
|
||||
<p class="text-gray-600 mb-4">
|
||||
Отправить тестовое сообщение для проверки работоспособности бота.
|
||||
</p>
|
||||
<button id="test-connection" class="w-full bg-green-600 hover:bg-green-700 text-white px-4 py-2 rounded-lg transition-colors">
|
||||
<i class="fas fa-vial mr-2"></i>
|
||||
Тестировать подключение
|
||||
</button>
|
||||
<div id="test-result" class="mt-4 hidden"></div>
|
||||
</div>
|
||||
|
||||
<!-- Send Custom Message -->
|
||||
<div class="bg-white shadow rounded-lg p-6">
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-4">
|
||||
<i class="fas fa-paper-plane mr-2 text-blue-500"></i>
|
||||
Отправить сообщение
|
||||
</h3>
|
||||
<form id="send-message-form">
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">
|
||||
Сообщение
|
||||
</label>
|
||||
<textarea
|
||||
id="custom-message"
|
||||
rows="4"
|
||||
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
placeholder="Введите текст сообщения..."></textarea>
|
||||
</div>
|
||||
<button type="submit" class="w-full bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg transition-colors">
|
||||
<i class="fas fa-send mr-2"></i>
|
||||
Отправить
|
||||
</button>
|
||||
</form>
|
||||
<div id="send-result" class="mt-4 hidden"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Notification Settings -->
|
||||
<div class="bg-white shadow rounded-lg p-6">
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-6">
|
||||
<i class="fas fa-bell mr-2 text-purple-500"></i>
|
||||
Настройки уведомлений
|
||||
</h3>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<!-- Notification Types -->
|
||||
<div>
|
||||
<h4 class="font-medium text-gray-900 mb-4">Типы уведомлений</h4>
|
||||
<div class="space-y-3">
|
||||
<div class="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
|
||||
<div class="flex items-center">
|
||||
<i class="fas fa-envelope mr-3 text-blue-500"></i>
|
||||
<span class="text-sm text-gray-900">Новые обращения</span>
|
||||
</div>
|
||||
<div class="w-3 h-3 bg-green-500 rounded-full"></div>
|
||||
</div>
|
||||
<div class="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
|
||||
<div class="flex items-center">
|
||||
<i class="fas fa-calculator mr-3 text-green-500"></i>
|
||||
<span class="text-sm text-gray-900">Расчеты стоимости</span>
|
||||
</div>
|
||||
<div class="w-3 h-3 bg-green-500 rounded-full"></div>
|
||||
</div>
|
||||
<div class="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
|
||||
<div class="flex items-center">
|
||||
<i class="fas fa-briefcase mr-3 text-purple-500"></i>
|
||||
<span class="text-sm text-gray-900">Новые проекты</span>
|
||||
</div>
|
||||
<div class="w-3 h-3 bg-green-500 rounded-full"></div>
|
||||
</div>
|
||||
<div class="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
|
||||
<div class="flex items-center">
|
||||
<i class="fas fa-cog mr-3 text-orange-500"></i>
|
||||
<span class="text-sm text-gray-900">Новые услуги</span>
|
||||
</div>
|
||||
<div class="w-3 h-3 bg-green-500 rounded-full"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bot Information -->
|
||||
<div>
|
||||
<h4 class="font-medium text-gray-900 mb-4">Информация о боте</h4>
|
||||
<div class="space-y-3">
|
||||
<div class="flex justify-between">
|
||||
<span class="text-sm text-gray-600">Статус:</span>
|
||||
<span class="text-sm font-medium text-green-600">Активен</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-sm text-gray-600">Токен:</span>
|
||||
<span class="text-sm font-mono text-gray-800">••••••••••</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-sm text-gray-600">Chat ID:</span>
|
||||
<span class="text-sm font-mono text-gray-800">••••••••••</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-sm text-gray-600">Последнее уведомление:</span>
|
||||
<span class="text-sm text-gray-600">Недавно</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Recent Notifications -->
|
||||
<div class="bg-white shadow rounded-lg p-6">
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-6">
|
||||
<i class="fas fa-history mr-2 text-gray-500"></i>
|
||||
Недавние уведомления
|
||||
</h3>
|
||||
|
||||
<div class="text-center py-8 text-gray-500">
|
||||
<i class="fas fa-inbox text-4xl text-gray-300 mb-4"></i>
|
||||
<p>Уведомления будут отображаться здесь после отправки</p>
|
||||
</div>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- JavaScript -->
|
||||
<script src="/js/main.js"></script>
|
||||
|
||||
<script>
|
||||
// Bot Configuration
|
||||
document.getElementById('bot-config-form').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const formData = new FormData(e.target);
|
||||
const data = Object.fromEntries(formData);
|
||||
|
||||
try {
|
||||
showLoading('Сохранение настроек...');
|
||||
|
||||
const response = await fetch('/admin/telegram/configure', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
showResult('config-result', result.message, 'success');
|
||||
// Update bot info if available
|
||||
if (result.botInfo) {
|
||||
updateBotInfo(result.botInfo);
|
||||
}
|
||||
if (result.availableChats) {
|
||||
updateAvailableChats(result.availableChats);
|
||||
}
|
||||
} else {
|
||||
showResult('config-result', result.message, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
showResult('config-result', 'Ошибка при сохранении настроек', 'error');
|
||||
} finally {
|
||||
hideLoading();
|
||||
}
|
||||
});
|
||||
|
||||
// Send message form
|
||||
document.getElementById('send-message-form').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const formData = new FormData(e.target);
|
||||
const message = formData.get('message');
|
||||
const chatIds = formData.getAll('chatIds');
|
||||
const parseMode = formData.get('parseMode');
|
||||
const disableWebPagePreview = formData.has('disableWebPagePreview');
|
||||
const disableNotification = formData.has('disableNotification');
|
||||
|
||||
if (!message.trim()) {
|
||||
showResult('send-result', 'Введите текст сообщения', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const data = {
|
||||
message: message.trim(),
|
||||
chatIds: chatIds.length > 0 ? chatIds : [],
|
||||
parseMode,
|
||||
disableWebPagePreview,
|
||||
disableNotification
|
||||
};
|
||||
|
||||
try {
|
||||
const btn = document.getElementById('sendMessageBtn');
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<i class="fas fa-spinner fa-spin mr-2"></i>Отправка...';
|
||||
|
||||
const response = await fetch('/admin/telegram/send', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
showResult('send-result', result.message, 'success');
|
||||
document.getElementById('messageText').value = '';
|
||||
} else {
|
||||
showResult('send-result', result.message, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
showResult('send-result', 'Ошибка при отправке сообщения', 'error');
|
||||
} finally {
|
||||
const btn = document.getElementById('sendMessageBtn');
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<i class="fas fa-paper-plane mr-2"></i>Отправить сообщение';
|
||||
}
|
||||
});
|
||||
|
||||
// Utility functions
|
||||
function toggleTokenVisibility() {
|
||||
const input = document.getElementById('botToken');
|
||||
const eye = document.getElementById('token-eye');
|
||||
|
||||
if (input.type === 'password') {
|
||||
input.type = 'text';
|
||||
eye.className = 'fas fa-eye-slash text-gray-400';
|
||||
} else {
|
||||
input.type = 'password';
|
||||
eye.className = 'fas fa-eye text-gray-400';
|
||||
}
|
||||
}
|
||||
|
||||
async function getBotInfo() {
|
||||
try {
|
||||
showLoading('Получение информации о боте...');
|
||||
|
||||
const response = await fetch('/admin/telegram/info');
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
updateBotInfo(result.botInfo);
|
||||
updateAvailableChats(result.availableChats);
|
||||
showResult('config-result', 'Информация о боте обновлена', 'success');
|
||||
} else {
|
||||
showResult('config-result', result.message, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
showResult('config-result', 'Ошибка при получении информации о боте', 'error');
|
||||
} finally {
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshBotInfo() {
|
||||
await getBotInfo();
|
||||
}
|
||||
|
||||
async function discoverChats() {
|
||||
try {
|
||||
showLoading('Поиск доступных чатов...');
|
||||
|
||||
const response = await fetch('/admin/telegram/info');
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success && result.availableChats) {
|
||||
updateAvailableChats(result.availableChats);
|
||||
showNotification('Найдено чатов: ' + result.availableChats.length, 'success');
|
||||
} else {
|
||||
showNotification('Чаты не найдены', 'warning');
|
||||
}
|
||||
} catch (error) {
|
||||
showNotification('Ошибка при поиске чатов', 'error');
|
||||
} finally {
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
function selectChat(chatId, chatTitle) {
|
||||
document.getElementById('chatId').value = chatId;
|
||||
showNotification(`Выбран чат: ${chatTitle}`, 'success');
|
||||
}
|
||||
|
||||
async function testConnection() {
|
||||
try {
|
||||
showLoading('Тестирование соединения...');
|
||||
|
||||
const response = await fetch('/admin/telegram/test', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
showResult('send-result', 'Соединение успешно! Тестовое сообщение отправлено.', 'success');
|
||||
} else {
|
||||
showResult('send-result', result.message, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
showResult('send-result', 'Ошибка при тестировании соединения', 'error');
|
||||
} finally {
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
function previewMessage() {
|
||||
const message = document.getElementById('messageText').value;
|
||||
const parseMode = document.getElementById('parseMode').value;
|
||||
|
||||
if (!message.trim()) {
|
||||
showNotification('Введите текст сообщения для предпросмотра', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
// Simple preview modal (you can enhance this)
|
||||
const preview = window.open('', 'preview', 'width=400,height=300');
|
||||
preview.document.write(`
|
||||
<html>
|
||||
<head><title>Предпросмотр сообщения</title></head>
|
||||
<body style="font-family: Arial, sans-serif; padding: 20px;">
|
||||
<h3>Предпросмотр сообщения (${parseMode || 'Обычный текст'})</h3>
|
||||
<div style="border: 1px solid #ccc; padding: 10px; background: #f9f9f9;">
|
||||
${parseMode === 'HTML' ? message : message.replace(/\n/g, '<br>')}
|
||||
</div>
|
||||
<button onclick="window.close()" style="margin-top: 10px;">Закрыть</button>
|
||||
</body>
|
||||
</html>
|
||||
`);
|
||||
}
|
||||
|
||||
// UI Helper functions
|
||||
function updateBotInfo(botInfo) {
|
||||
const container = document.getElementById('bot-info-container');
|
||||
if (!botInfo) return;
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<div class="bg-gray-50 p-4 rounded-lg">
|
||||
<div class="text-sm text-gray-600">Имя бота</div>
|
||||
<div class="text-lg font-semibold text-gray-900">@${botInfo.username}</div>
|
||||
</div>
|
||||
<div class="bg-gray-50 p-4 rounded-lg">
|
||||
<div class="text-sm text-gray-600">Отображаемое имя</div>
|
||||
<div class="text-lg font-semibold text-gray-900">${botInfo.first_name}</div>
|
||||
</div>
|
||||
<div class="bg-gray-50 p-4 rounded-lg">
|
||||
<div class="text-sm text-gray-600">ID бота</div>
|
||||
<div class="text-lg font-semibold text-gray-900">${botInfo.id}</div>
|
||||
</div>
|
||||
<div class="bg-gray-50 p-4 rounded-lg">
|
||||
<div class="text-sm text-gray-600">Может читать сообщения</div>
|
||||
<div class="text-lg font-semibold ${botInfo.can_read_all_group_messages ? 'text-green-600' : 'text-red-600'}">
|
||||
${botInfo.can_read_all_group_messages ? 'Да' : 'Нет'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function updateAvailableChats(chats) {
|
||||
const container = document.getElementById('available-chats-container');
|
||||
if (!chats || chats.length === 0) {
|
||||
container.innerHTML = `
|
||||
<div class="text-center py-8 text-gray-500">
|
||||
<i class="fas fa-comments text-4xl text-gray-300 mb-4"></i>
|
||||
<p class="mb-2">Чаты не найдены</p>
|
||||
<p class="text-sm">Отправьте боту сообщение или добавьте его в группу, затем нажмите "Найти чаты"</p>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
const chatsHtml = chats.map(chat => `
|
||||
<div class="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
|
||||
<div class="flex items-center space-x-3">
|
||||
<div class="w-8 h-8 bg-blue-100 rounded-full flex items-center justify-center">
|
||||
<i class="fas ${chat.type === 'group' || chat.type === 'supergroup' ? 'fa-users' : 'fa-user'} text-blue-600 text-sm"></i>
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-medium text-gray-900">${chat.title}</div>
|
||||
<div class="text-sm text-gray-500">
|
||||
${chat.type} • ID: ${chat.id}
|
||||
${chat.username ? '• @' + chat.username : ''}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button onclick="selectChat('${chat.id}', '${chat.title}')"
|
||||
class="text-blue-600 hover:text-blue-800 text-sm">
|
||||
Выбрать
|
||||
</button>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
container.innerHTML = `<div class="grid gap-3">${chatsHtml}</div>`;
|
||||
|
||||
// Also update recipients list
|
||||
updateRecipientsList(chats);
|
||||
}
|
||||
|
||||
function updateRecipientsList(chats) {
|
||||
const container = document.getElementById('recipients-container');
|
||||
if (!chats || chats.length === 0) {
|
||||
container.innerHTML = `
|
||||
<div class="text-sm text-gray-500">
|
||||
<i class="fas fa-info-circle mr-1"></i>
|
||||
Сообщение будет отправлено в чат по умолчанию
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
const recipientsHtml = chats.map(chat => `
|
||||
<label class="flex items-center">
|
||||
<input type="checkbox" name="chatIds" value="${chat.id}"
|
||||
class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded">
|
||||
<span class="ml-2 text-sm text-gray-900">
|
||||
${chat.title}
|
||||
<span class="text-gray-500">(${chat.type})</span>
|
||||
</span>
|
||||
</label>
|
||||
`).join('');
|
||||
|
||||
container.innerHTML = recipientsHtml;
|
||||
}
|
||||
|
||||
function showResult(elementId, message, type) {
|
||||
const element = document.getElementById(elementId);
|
||||
const className = type === 'success' ? 'bg-green-100 text-green-800' :
|
||||
type === 'warning' ? 'bg-yellow-100 text-yellow-800' :
|
||||
'bg-red-100 text-red-800';
|
||||
const icon = type === 'success' ? 'fa-check' :
|
||||
type === 'warning' ? 'fa-exclamation-triangle' :
|
||||
'fa-times';
|
||||
|
||||
element.className = `mt-4 p-3 rounded-lg ${className}`;
|
||||
element.innerHTML = `<i class="fas ${icon} mr-2"></i>${message}`;
|
||||
element.classList.remove('hidden');
|
||||
|
||||
setTimeout(() => {
|
||||
element.classList.add('hidden');
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
function showNotification(message, type = 'info') {
|
||||
const notification = document.createElement('div');
|
||||
notification.className = `fixed top-4 right-4 z-50 px-4 py-3 rounded-lg shadow-lg text-white max-w-sm transform transition-all duration-300 translate-x-full`;
|
||||
|
||||
switch(type) {
|
||||
case 'success':
|
||||
notification.classList.add('bg-green-600');
|
||||
break;
|
||||
case 'error':
|
||||
notification.classList.add('bg-red-600');
|
||||
break;
|
||||
case 'warning':
|
||||
notification.classList.add('bg-yellow-600');
|
||||
break;
|
||||
default:
|
||||
notification.classList.add('bg-blue-600');
|
||||
}
|
||||
|
||||
notification.innerHTML = `
|
||||
<div class="flex items-center">
|
||||
<i class="fas ${type === 'success' ? 'fa-check-circle' : type === 'error' ? 'fa-exclamation-circle' : type === 'warning' ? 'fa-exclamation-triangle' : 'fa-info-circle'} mr-2"></i>
|
||||
<span>${message}</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(notification);
|
||||
|
||||
setTimeout(() => {
|
||||
notification.classList.remove('translate-x-full');
|
||||
}, 100);
|
||||
|
||||
setTimeout(() => {
|
||||
notification.classList.add('translate-x-full');
|
||||
setTimeout(() => {
|
||||
if (document.body.contains(notification)) {
|
||||
document.body.removeChild(notification);
|
||||
}
|
||||
}, 300);
|
||||
}, 4000);
|
||||
}
|
||||
|
||||
function showLoading(message) {
|
||||
// Create or show loading overlay
|
||||
let overlay = document.getElementById('loading-overlay');
|
||||
if (!overlay) {
|
||||
overlay = document.createElement('div');
|
||||
overlay.id = 'loading-overlay';
|
||||
overlay.className = 'fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50';
|
||||
overlay.innerHTML = `
|
||||
<div class="bg-white p-6 rounded-lg shadow-lg">
|
||||
<div class="flex items-center">
|
||||
<i class="fas fa-spinner fa-spin text-blue-600 text-xl mr-3"></i>
|
||||
<span id="loading-message">${message}</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(overlay);
|
||||
} else {
|
||||
document.getElementById('loading-message').textContent = message;
|
||||
overlay.style.display = 'flex';
|
||||
}
|
||||
}
|
||||
|
||||
function hideLoading() {
|
||||
const overlay = document.getElementById('loading-overlay');
|
||||
if (overlay) {
|
||||
overlay.style.display = 'none';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -19,7 +19,7 @@
|
||||
<%- include('partials/navigation') %>
|
||||
|
||||
<!-- Calculator Header -->
|
||||
<section class="relative bg-gradient-to-r from-blue-600 to-purple-600 dark:from-blue-800 dark:to-purple-800 py-20 mt-16 hero-section">
|
||||
<section class="relative bg-gradient-to-r from-blue-600 to-purple-600 dark:from-blue-800 dark:to-purple-800 mt-16 hero-section-compact">
|
||||
<div class="absolute inset-0 bg-black opacity-20"></div>
|
||||
<div class="relative max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
|
||||
<h1 class="text-4xl md:text-5xl font-bold text-white mb-4">
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
<%- include('partials/navigation') %>
|
||||
|
||||
<!-- Hero Section -->
|
||||
<section class="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 py-20 hero-section">
|
||||
<section class="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 hero-section-compact">
|
||||
<div class="absolute inset-0 bg-black opacity-50 dark:opacity-70"></div>
|
||||
<div class="relative max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
|
||||
<h1 class="text-5xl md:text-6xl font-bold text-white mb-6">
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<html lang="<%= locale || 'ko' %>" class="<%= theme === 'dark' ? 'dark' : '' %>">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>오류 - SmartSolTech</title>
|
||||
<title><%= title || __('errors.title') %></title>
|
||||
|
||||
<!-- PWA -->
|
||||
<meta name="theme-color" content="#3B82F6">
|
||||
@@ -14,9 +14,11 @@
|
||||
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
|
||||
<link href="/css/main.css" rel="stylesheet">
|
||||
<link href="/css/fixes.css" rel="stylesheet">
|
||||
<link href="/css/dark-theme.css" rel="stylesheet">
|
||||
</head>
|
||||
<body class="bg-gray-50">
|
||||
<%- include('partials/navigation') %>
|
||||
<body class="font-sans dark:bg-gray-900 dark:text-gray-100">
|
||||
<%- include('partials/navigation', { settings: settings || {}, currentPage: 'error' }) %>
|
||||
|
||||
<!-- Error Section -->
|
||||
<section class="min-h-screen flex items-center justify-center py-20">
|
||||
@@ -30,12 +32,12 @@
|
||||
|
||||
<!-- Error Title -->
|
||||
<h1 class="text-4xl md:text-5xl font-bold text-gray-900 mb-6">
|
||||
<%= title || '오류가 발생했습니다' %>
|
||||
<%= title || __('errors.default_title') %>
|
||||
</h1>
|
||||
|
||||
<!-- Error Message -->
|
||||
<p class="text-xl text-gray-600 mb-8 leading-relaxed">
|
||||
<%= message || '요청을 처리하는 중 문제가 발생했습니다.' %>
|
||||
<%= message || __('errors.default_message') %>
|
||||
</p>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
@@ -43,37 +45,53 @@
|
||||
<a href="/"
|
||||
class="bg-blue-600 text-white px-8 py-3 rounded-full hover:bg-blue-700 transition-colors font-semibold">
|
||||
<i class="fas fa-home mr-2"></i>
|
||||
홈으로 돌아가기
|
||||
<%= __('errors.back_home') %>
|
||||
</a>
|
||||
<button onclick="history.back()"
|
||||
class="border-2 border-blue-600 text-blue-600 px-8 py-3 rounded-full hover:bg-blue-600 hover:text-white transition-colors font-semibold">
|
||||
<i class="fas fa-arrow-left mr-2"></i>
|
||||
이전 페이지로
|
||||
<%= __('errors.go_back') %>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Help Section -->
|
||||
<div class="mt-12 pt-8 border-t border-gray-200">
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-4">도움이 필요하신가요?</h3>
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-4"><%= __('errors.need_help') %></h3>
|
||||
<p class="text-gray-600 mb-6">
|
||||
문제가 지속되면 언제든지 저희에게 연락해 주세요.
|
||||
<%= __('errors.help_message') %>
|
||||
</p>
|
||||
<div class="flex flex-col sm:flex-row gap-4 justify-center">
|
||||
<a href="/contact"
|
||||
class="text-blue-600 hover:text-blue-700 font-semibold">
|
||||
<i class="fas fa-envelope mr-2"></i>
|
||||
문의하기
|
||||
<%= __('errors.contact_support') %>
|
||||
</a>
|
||||
<% if (settings && settings.contact && settings.contact.email) { %>
|
||||
<a href="mailto:<%= settings.contact.email %>"
|
||||
class="text-blue-600 hover:text-blue-700 font-semibold">
|
||||
<i class="fas fa-at mr-2"></i>
|
||||
<%= settings.contact.email %>
|
||||
</a>
|
||||
<% } else { %>
|
||||
<a href="mailto:info@smartsoltech.kr"
|
||||
class="text-blue-600 hover:text-blue-700 font-semibold">
|
||||
<i class="fas fa-at mr-2"></i>
|
||||
info@smartsoltech.kr
|
||||
</a>
|
||||
<% } %>
|
||||
<% if (settings && settings.contact && settings.contact.phone) { %>
|
||||
<a href="tel:<%= settings.contact.phone %>"
|
||||
class="text-blue-600 hover:text-blue-700 font-semibold">
|
||||
<i class="fas fa-phone mr-2"></i>
|
||||
<%= settings.contact.phone %>
|
||||
</a>
|
||||
<% } else { %>
|
||||
<a href="tel:+82-10-1234-5678"
|
||||
class="text-blue-600 hover:text-blue-700 font-semibold">
|
||||
<i class="fas fa-phone mr-2"></i>
|
||||
+82-10-1234-5678
|
||||
</a>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
118
views/index.ejs
118
views/index.ejs
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title><%- __(title || 'meta.title') %></title>
|
||||
<title><%- title || 'SmartSolTech - Innovative Technology Solutions' %></title>
|
||||
|
||||
<!-- SEO Meta Tags -->
|
||||
<meta name="description" content="<%- __('meta.description') %>">
|
||||
@@ -17,15 +17,39 @@
|
||||
<!-- PWA -->
|
||||
<meta name="theme-color" content="#3B82F6">
|
||||
<link rel="manifest" href="/manifest.json">
|
||||
<link rel="apple-touch-icon" href="/images/icons/icon-192x192.png">
|
||||
<link rel="apple-touch-icon" href="/images/icon-192x192.png">
|
||||
|
||||
<!-- Styles -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
|
||||
<link href="https://unpkg.com/aos@2.3.1/dist/aos.css" rel="stylesheet">
|
||||
<!-- Custom CSS (load first) -->
|
||||
<link href="/css/base.css" rel="stylesheet">
|
||||
<link href="/css/main.css" rel="stylesheet">
|
||||
<link href="/css/fixes.css" rel="stylesheet">
|
||||
<link href="/css/dark-theme.css" rel="stylesheet">
|
||||
|
||||
<!-- Tailwind CSS via CDN -->
|
||||
<script src="https://cdn.tailwindcss.com?plugins=forms,typography,aspect-ratio"></script>
|
||||
|
||||
<!-- Font Awesome -->
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" rel="stylesheet" integrity="sha512-DTOQO9RWCH3ppGqcWaEA1BIZOC6xxalwEsw9c2QQeAIftl+Vegovlnee1c9QX4TctnWMn13TZye+giMm8e2LwA==" crossorigin="anonymous">
|
||||
|
||||
<!-- AOS Animation Library -->
|
||||
<link href="https://unpkg.com/aos@2.3.1/dist/aos.css" rel="stylesheet">
|
||||
|
||||
<!-- Tailwind Configuration -->
|
||||
<script>
|
||||
tailwind.config = {
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
'primary': '#3B82F6',
|
||||
'secondary': '#8B5CF6',
|
||||
'accent': '#10B981',
|
||||
},
|
||||
fontFamily: {
|
||||
'sans': ['Inter', 'system-ui', 'sans-serif'],
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body class="font-sans dark:bg-gray-900 dark:text-gray-100">
|
||||
<%- include('partials/navigation') %>
|
||||
@@ -337,64 +361,34 @@
|
||||
<!-- Scripts -->
|
||||
<script src="https://unpkg.com/aos@2.3.1/dist/aos.js"></script>
|
||||
<script src="/js/main.js"></script>
|
||||
|
||||
<!-- Initialize AOS -->
|
||||
<script>
|
||||
// Initialize AOS
|
||||
AOS.init({
|
||||
duration: 800,
|
||||
once: true,
|
||||
offset: 100
|
||||
});
|
||||
|
||||
// Blob animation
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
@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;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
|
||||
// Contact form handler
|
||||
document.getElementById('quick-contact-form').addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
const formData = new FormData(this);
|
||||
|
||||
fetch('/contact', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
alert('<%- __("contact.form.success") %>');
|
||||
this.reset();
|
||||
} else {
|
||||
alert('<%- __("contact.form.error") %>');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
alert('<%- __("contact.form.error") %>');
|
||||
});
|
||||
});
|
||||
|
||||
// Theme initialization
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const theme = localStorage.getItem('theme') || 'light';
|
||||
document.documentElement.className = theme === 'dark' ? 'dark' : '';
|
||||
if (typeof AOS !== 'undefined') {
|
||||
AOS.init({
|
||||
duration: 800,
|
||||
easing: 'ease-in-out',
|
||||
once: true
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- PWA Service Worker -->
|
||||
<script>
|
||||
if ('serviceWorker' in navigator) {
|
||||
window.addEventListener('load', () => {
|
||||
navigator.serviceWorker.register('/sw.js')
|
||||
.then(registration => {
|
||||
console.log('SW registered: ', registration);
|
||||
})
|
||||
.catch(registrationError => {
|
||||
console.log('SW registration failed: ', registrationError);
|
||||
});
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
@@ -1,5 +1,5 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<html lang="<%= locale || 'ko' %>" class="<%= theme === 'dark' ? 'dark' : '' %>">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
@@ -35,14 +35,39 @@
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
||||
|
||||
<!-- CSS -->
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/tailwindcss/2.2.19/tailwind.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="/css/main.css">
|
||||
<!-- Tailwind CSS (newer version with CDN Play) -->
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
|
||||
<!-- Animation Library -->
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/aos/2.3.4/aos.css" rel="stylesheet">
|
||||
<!-- Font Awesome -->
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" rel="stylesheet">
|
||||
|
||||
<!-- AOS Animation Library -->
|
||||
<link href="https://unpkg.com/aos@2.3.1/dist/aos.css" rel="stylesheet">
|
||||
|
||||
<!-- Custom CSS -->
|
||||
<link href="/css/base.css" rel="stylesheet">
|
||||
<link href="/css/main.css" rel="stylesheet">
|
||||
<link href="/css/fixes.css" rel="stylesheet">
|
||||
|
||||
<!-- Tailwind Configuration -->
|
||||
<script>
|
||||
tailwind.config = {
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
'primary': '#3B82F6',
|
||||
'secondary': '#8B5CF6',
|
||||
'accent': '#10B981',
|
||||
},
|
||||
fontFamily: {
|
||||
'sans': ['Inter', 'system-ui', 'sans-serif'],
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body class="bg-gray-50 font-inter">
|
||||
<body class="font-sans dark:bg-gray-900 dark:text-gray-100">
|
||||
<!-- Navigation -->
|
||||
<%- include('partials/navigation') %>
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<span class="ml-2 text-xl font-bold">SmartSolTech</span>
|
||||
</div>
|
||||
<p class="text-gray-300 dark:text-gray-400 mb-4">
|
||||
<%- __('footer.company.description') %>
|
||||
<%= __('footer.company.description') %>
|
||||
</p>
|
||||
|
||||
<!-- Social Links -->
|
||||
@@ -70,34 +70,34 @@
|
||||
|
||||
<!-- Quick Links -->
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold mb-4"><%- __('footer.links.title') %></h3>
|
||||
<h3 class="text-lg font-semibold mb-4"><%= __('footer.links.title') %></h3>
|
||||
<ul class="space-y-2">
|
||||
<li><a href="/" class="text-gray-300 dark:text-gray-400 hover:text-white dark:hover:text-gray-200 transition-colors"><%- __('nav.home') %></a></li>
|
||||
<li><a href="/about" class="text-gray-300 dark:text-gray-400 hover:text-white dark:hover:text-gray-200 transition-colors"><%- __('nav.about') %></a></li>
|
||||
<li><a href="/services" class="text-gray-300 dark:text-gray-400 hover:text-white dark:hover:text-gray-200 transition-colors"><%- __('nav.services') %></a></li>
|
||||
<li><a href="/portfolio" class="text-gray-300 dark:text-gray-400 hover:text-white dark:hover:text-gray-200 transition-colors"><%- __('nav.portfolio') %></a></li>
|
||||
<li><a href="/calculator" class="text-gray-300 dark:text-gray-400 hover:text-white dark:hover:text-gray-200 transition-colors"><%- __('nav.calculator') %></a></li>
|
||||
<li><a href="/" class="text-gray-300 dark:text-gray-400 hover:text-white dark:hover:text-gray-200 transition-colors"><%= __('nav.home') %></a></li>
|
||||
<li><a href="/about" class="text-gray-300 dark:text-gray-400 hover:text-white dark:hover:text-gray-200 transition-colors"><%= __('nav.about') %></a></li>
|
||||
<li><a href="/services" class="text-gray-300 dark:text-gray-400 hover:text-white dark:hover:text-gray-200 transition-colors"><%= __('nav.services') %></a></li>
|
||||
<li><a href="/portfolio" class="text-gray-300 dark:text-gray-400 hover:text-white dark:hover:text-gray-200 transition-colors"><%= __('nav.portfolio') %></a></li>
|
||||
<li><a href="/calculator" class="text-gray-300 dark:text-gray-400 hover:text-white dark:hover:text-gray-200 transition-colors"><%= __('nav.calculator') %></a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Contact Info -->
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold mb-4"><%- __('footer.contact.title') %></h3>
|
||||
<h3 class="text-lg font-semibold mb-4"><%= __('footer.contact.title') %></h3>
|
||||
<ul class="space-y-2 text-gray-300 dark:text-gray-400">
|
||||
<li class="flex items-center">
|
||||
<svg class="h-5 w-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 4.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"></path>
|
||||
</svg>
|
||||
<a href="mailto:<%= settings && settings.contact ? settings.contact.email : __('footer.contact.email') %>" class="hover:text-white dark:hover:text-gray-200 transition-colors">
|
||||
<%= settings && settings.contact ? settings.contact.email : __('footer.contact.email') %>
|
||||
<a href="mailto:<%= settings && settings.contact && settings.contact.email ? settings.contact.email : __('footer.contact.email') %>" class="hover:text-white dark:hover:text-gray-200 transition-colors">
|
||||
<%= settings && settings.contact && settings.contact.email ? settings.contact.email : __('footer.contact.email') %>
|
||||
</a>
|
||||
</li>
|
||||
<li class="flex items-center">
|
||||
<svg class="h-5 w-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z"></path>
|
||||
</svg>
|
||||
<a href="tel:<%= settings && settings.contact ? settings.contact.phone : __('footer.contact.phone') %>" class="hover:text-white dark:hover:text-gray-200 transition-colors">
|
||||
<%= settings && settings.contact ? settings.contact.phone : __('footer.contact.phone') %>
|
||||
<a href="tel:<%= settings && settings.contact && settings.contact.phone ? settings.contact.phone : __('footer.contact.phone') %>" class="hover:text-white dark:hover:text-gray-200 transition-colors">
|
||||
<%= settings && settings.contact && settings.contact.phone ? settings.contact.phone : __('footer.contact.phone') %>
|
||||
</a>
|
||||
</li>
|
||||
<li class="flex items-start">
|
||||
@@ -105,7 +105,7 @@
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"></path>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"></path>
|
||||
</svg>
|
||||
<span><%= settings && settings.contact ? settings.contact.address : __('footer.contact.address') %></span>
|
||||
<span><%= settings && settings.contact && settings.contact.address ? settings.contact.address : __('footer.contact.address') %></span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -114,11 +114,11 @@
|
||||
<!-- Bottom Section -->
|
||||
<div class="border-t border-gray-800 dark:border-gray-700 mt-8 pt-8 flex flex-col md:flex-row justify-between items-center">
|
||||
<p class="text-gray-300 dark:text-gray-400 text-sm">
|
||||
<%- __('footer.copyright', { year: new Date().getFullYear() }) %>
|
||||
<%= __('footer.copyright').replace('{{year}}', new Date().getFullYear()) %>
|
||||
</p>
|
||||
<div class="flex space-x-6 mt-4 md:mt-0">
|
||||
<a href="/privacy" class="text-gray-300 dark:text-gray-400 hover:text-white dark:hover:text-gray-200 text-sm transition-colors"><%- __('footer.privacy') %></a>
|
||||
<a href="/terms" class="text-gray-300 dark:text-gray-400 hover:text-white dark:hover:text-gray-200 text-sm transition-colors"><%- __('footer.terms') %></a>
|
||||
<a href="/privacy" class="text-gray-300 dark:text-gray-400 hover:text-white dark:hover:text-gray-200 text-sm transition-colors"><%= __('footer.privacy') %></a>
|
||||
<a href="/terms" class="text-gray-300 dark:text-gray-400 hover:text-white dark:hover:text-gray-200 text-sm transition-colors"><%= __('footer.terms') %></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -13,88 +13,69 @@
|
||||
<div class="hidden md:flex items-center space-x-6">
|
||||
<!-- Navigation Links -->
|
||||
<a href="/" class="<%= currentPage === 'home' ? 'text-blue-600 border-b-2 border-blue-600' : 'text-gray-700 dark:text-gray-300 hover:text-blue-600' %> px-3 py-2 text-sm font-medium transition-colors nav-link">
|
||||
<%- __('navigation.home') %>
|
||||
<%= __('navigation.home') %>
|
||||
</a>
|
||||
<a href="/about" class="<%= currentPage === 'about' ? 'text-blue-600 border-b-2 border-blue-600' : 'text-gray-700 dark:text-gray-300 hover:text-blue-600' %> px-3 py-2 text-sm font-medium transition-colors nav-link">
|
||||
<%- __('navigation.about') %>
|
||||
<%= __('navigation.about') %>
|
||||
</a>
|
||||
<a href="/services" class="<%= currentPage === 'services' ? 'text-blue-600 border-b-2 border-blue-600' : 'text-gray-700 dark:text-gray-300 hover:text-blue-600' %> px-3 py-2 text-sm font-medium transition-colors nav-link">
|
||||
<%- __('navigation.services') %>
|
||||
<%= __('navigation.services') %>
|
||||
</a>
|
||||
<a href="/portfolio" class="<%= currentPage === 'portfolio' ? 'text-blue-600 border-b-2 border-blue-600' : 'text-gray-700 dark:text-gray-300 hover:text-blue-600' %> px-3 py-2 text-sm font-medium transition-colors nav-link">
|
||||
<%- __('navigation.portfolio') %>
|
||||
<%= __('navigation.portfolio') %>
|
||||
</a>
|
||||
<a href="/calculator" class="<%= currentPage === 'calculator' ? 'text-blue-600 border-b-2 border-blue-600' : 'text-gray-700 dark:text-gray-300 hover:text-blue-600' %> px-3 py-2 text-sm font-medium transition-colors nav-link">
|
||||
<%- __('navigation.calculator') %>
|
||||
<%= __('navigation.calculator') %>
|
||||
</a>
|
||||
<a href="/contact" class="<%= currentPage === 'contact' ? 'text-blue-600 border-b-2 border-blue-600' : 'text-gray-700 dark:text-gray-300 hover:text-blue-600' %> px-3 py-2 text-sm font-medium transition-colors nav-link">
|
||||
<%- __('navigation.contact') %>
|
||||
<%= __('navigation.contact') %>
|
||||
</a>
|
||||
|
||||
<!-- Language Dropdown -->
|
||||
<div class="relative group">
|
||||
<button class="flex items-center text-gray-700 dark:text-gray-300 hover:text-blue-600 px-3 py-2 text-sm font-medium transition-colors" id="language-dropdown">
|
||||
<i class="fas fa-globe mr-2"></i>
|
||||
<%- __('language.' + currentLanguage) %>
|
||||
<%
|
||||
let currentFlag = '🇰🇷';
|
||||
if (currentLanguage === 'en') currentFlag = '🇺🇸';
|
||||
else if (currentLanguage === 'ru') currentFlag = '🇷🇺';
|
||||
else if (currentLanguage === 'kk') currentFlag = '🇰🇿';
|
||||
%>
|
||||
<span class="mr-2"><%= currentFlag %></span>
|
||||
<%= __('language.' + currentLanguage) %>
|
||||
<i class="fas fa-chevron-down ml-1 text-xs"></i>
|
||||
</button>
|
||||
<div class="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="/lang/ko" class="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">
|
||||
🇰🇷 <%- __('language.korean') %>
|
||||
🇰🇷 <%= __('language.korean') %>
|
||||
</a>
|
||||
<a href="/lang/en" class="block px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700">
|
||||
🇺🇸 <%- __('language.english') %>
|
||||
🇺🇸 <%= __('language.english') %>
|
||||
</a>
|
||||
<a href="/lang/ru" class="block px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700">
|
||||
🇷🇺 <%- __('language.russian') %>
|
||||
🇷🇺 <%= __('language.russian') %>
|
||||
</a>
|
||||
<a href="/lang/kk" class="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">
|
||||
🇰🇿 <%- __('language.kazakh') %>
|
||||
🇰🇿 <%= __('language.kazakh') %>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Theme Toggle -->
|
||||
<button id="theme-toggle" class="p-2 text-gray-700 dark:text-gray-300 hover:text-blue-600 transition-colors" title="<%- __('theme.toggle') %>">
|
||||
<i class="fas fa-sun dark:hidden"></i>
|
||||
<i class="fas fa-moon hidden dark:block"></i>
|
||||
</button>
|
||||
|
||||
<!-- CTA Button -->
|
||||
<a href="/contact" class="bg-blue-600 text-white px-4 py-2 rounded-lg text-sm font-medium hover:bg-blue-700 transition-colors btn-primary">
|
||||
<%- __('hero.cta_primary') %>
|
||||
</a>
|
||||
<!-- iOS Style Theme Toggle -->
|
||||
<div class="relative inline-block" title="<%= __('theme.toggle') %>">
|
||||
<input type="checkbox" id="theme-toggle" class="sr-only">
|
||||
<label for="theme-toggle" class="flex items-center cursor-pointer">
|
||||
<div class="relative w-12 h-6 bg-gray-300 dark:bg-gray-600 rounded-full transition-colors duration-200">
|
||||
<div class="absolute top-0.5 left-0.5 w-5 h-5 bg-white rounded-full shadow-md transform transition-transform duration-200 flex items-center justify-center theme-toggle-slider">
|
||||
<i class="fas fa-sun text-yellow-500 text-xs theme-sun-icon"></i>
|
||||
<i class="fas fa-moon text-blue-500 text-xs hidden theme-moon-icon"></i>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mobile menu button -->
|
||||
<div class="md:hidden flex items-center space-x-2">
|
||||
<!-- Mobile Theme Toggle -->
|
||||
<button id="mobile-theme-toggle" class="p-2 text-gray-700 dark:text-gray-300 hover:text-blue-600 transition-colors">
|
||||
<i class="fas fa-sun dark:hidden"></i>
|
||||
<i class="fas fa-moon hidden dark:block"></i>
|
||||
</button>
|
||||
|
||||
<!-- Mobile Language Toggle -->
|
||||
<div class="relative">
|
||||
<button id="mobile-language-toggle" class="p-2 text-gray-700 dark:text-gray-300 hover:text-blue-600 transition-colors">
|
||||
<i class="fas fa-globe"></i>
|
||||
</button>
|
||||
<div id="mobile-language-menu" class="absolute right-0 mt-2 w-40 bg-white dark:bg-gray-800 rounded-lg shadow-lg border dark:border-gray-700 hidden">
|
||||
<a href="/lang/ko" class="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">
|
||||
🇰🇷 <%- __('language.korean') %>
|
||||
</a>
|
||||
<a href="/lang/en" class="block px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700">
|
||||
🇺🇸 <%- __('language.english') %>
|
||||
</a>
|
||||
<a href="/lang/ru" class="block px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700">
|
||||
🇷🇺 <%- __('language.russian') %>
|
||||
</a>
|
||||
<a href="/lang/kk" class="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">
|
||||
🇰🇿 <%- __('language.kazakh') %>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Hamburger Menu -->
|
||||
<button id="mobile-menu-toggle" class="p-2 text-gray-700 dark:text-gray-300 hover:text-blue-600 transition-colors">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
@@ -109,30 +90,23 @@
|
||||
<div id="mobile-menu" class="md:hidden hidden bg-white dark:bg-gray-900 border-t dark:border-gray-700">
|
||||
<div class="px-2 pt-2 pb-3 space-y-1">
|
||||
<a href="/" class="<%= currentPage === 'home' ? 'bg-blue-50 dark:bg-blue-900 text-blue-600' : '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">
|
||||
<%- __('navigation.home') %>
|
||||
<%= __('navigation.home') %>
|
||||
</a>
|
||||
<a href="/about" class="<%= currentPage === 'about' ? 'bg-blue-50 dark:bg-blue-900 text-blue-600' : '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">
|
||||
<%- __('navigation.about') %>
|
||||
<%= __('navigation.about') %>
|
||||
</a>
|
||||
<a href="/services" class="<%= currentPage === 'services' ? 'bg-blue-50 dark:bg-blue-900 text-blue-600' : '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">
|
||||
<%- __('navigation.services') %>
|
||||
<%= __('navigation.services') %>
|
||||
</a>
|
||||
<a href="/portfolio" class="<%= currentPage === 'portfolio' ? 'bg-blue-50 dark:bg-blue-900 text-blue-600' : '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">
|
||||
<%- __('navigation.portfolio') %>
|
||||
<%= __('navigation.portfolio') %>
|
||||
</a>
|
||||
<a href="/calculator" class="<%= currentPage === 'calculator' ? 'bg-blue-50 dark:bg-blue-900 text-blue-600' : '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">
|
||||
<%- __('navigation.calculator') %>
|
||||
<%= __('navigation.calculator') %>
|
||||
</a>
|
||||
<a href="/contact" class="<%= currentPage === 'contact' ? 'bg-blue-50 dark:bg-blue-900 text-blue-600' : '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">
|
||||
<%- __('navigation.contact') %>
|
||||
<%= __('navigation.contact') %>
|
||||
</a>
|
||||
|
||||
<!-- Mobile CTA -->
|
||||
<div class="pt-4 pb-2">
|
||||
<a href="/contact" class="block w-full bg-blue-600 text-white text-center px-4 py-2 rounded-lg text-sm font-medium hover:bg-blue-700 transition-colors">
|
||||
<%- __('hero.cta_primary') %>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
@@ -141,40 +115,53 @@
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Theme Management
|
||||
const themeToggle = document.getElementById('theme-toggle');
|
||||
const mobileThemeToggle = document.getElementById('mobile-theme-toggle');
|
||||
const html = document.documentElement;
|
||||
const slider = document.querySelector('.theme-toggle-slider');
|
||||
const sunIcon = document.querySelector('.theme-sun-icon');
|
||||
const moonIcon = document.querySelector('.theme-moon-icon');
|
||||
|
||||
// Get current theme
|
||||
const currentTheme = localStorage.getItem('theme') ||
|
||||
(window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
|
||||
|
||||
// Apply theme
|
||||
if (currentTheme === 'dark') {
|
||||
html.classList.add('dark');
|
||||
} else {
|
||||
html.classList.remove('dark');
|
||||
}
|
||||
|
||||
// Theme toggle handlers
|
||||
function toggleTheme() {
|
||||
const isDark = html.classList.contains('dark');
|
||||
// Apply theme and update toggle
|
||||
function applyTheme(isDark) {
|
||||
if (isDark) {
|
||||
html.classList.remove('dark');
|
||||
localStorage.setItem('theme', 'light');
|
||||
fetch('/theme/light');
|
||||
} else {
|
||||
html.classList.add('dark');
|
||||
localStorage.setItem('theme', 'dark');
|
||||
fetch('/theme/dark');
|
||||
themeToggle.checked = true;
|
||||
if (slider) {
|
||||
slider.style.transform = 'translateX(24px)';
|
||||
slider.style.backgroundColor = '#374151';
|
||||
}
|
||||
if (sunIcon) sunIcon.classList.add('hidden');
|
||||
if (moonIcon) moonIcon.classList.remove('hidden');
|
||||
} else {
|
||||
html.classList.remove('dark');
|
||||
themeToggle.checked = false;
|
||||
if (slider) {
|
||||
slider.style.transform = 'translateX(0)';
|
||||
slider.style.backgroundColor = '#ffffff';
|
||||
}
|
||||
if (sunIcon) sunIcon.classList.remove('hidden');
|
||||
if (moonIcon) moonIcon.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
if (themeToggle) {
|
||||
themeToggle.addEventListener('click', toggleTheme);
|
||||
// Initial theme application
|
||||
applyTheme(currentTheme === 'dark');
|
||||
|
||||
// Theme toggle handler
|
||||
function toggleTheme() {
|
||||
const isDark = html.classList.contains('dark');
|
||||
const newTheme = isDark ? 'light' : 'dark';
|
||||
|
||||
applyTheme(!isDark);
|
||||
localStorage.setItem('theme', newTheme);
|
||||
fetch(`/theme/${newTheme}`);
|
||||
}
|
||||
|
||||
if (mobileThemeToggle) {
|
||||
mobileThemeToggle.addEventListener('click', toggleTheme);
|
||||
if (themeToggle) {
|
||||
themeToggle.addEventListener('change', toggleTheme);
|
||||
}
|
||||
|
||||
// Mobile Menu Management
|
||||
@@ -187,19 +174,22 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
});
|
||||
}
|
||||
|
||||
// Mobile Language Menu
|
||||
const mobileLanguageToggle = document.getElementById('mobile-language-toggle');
|
||||
const mobileLanguageMenu = document.getElementById('mobile-language-menu');
|
||||
// Desktop Language Dropdown Management
|
||||
const languageDropdown = document.getElementById('language-dropdown');
|
||||
const languageMenu = languageDropdown ? languageDropdown.nextElementSibling : null;
|
||||
|
||||
if (mobileLanguageToggle && mobileLanguageMenu) {
|
||||
mobileLanguageToggle.addEventListener('click', function() {
|
||||
mobileLanguageMenu.classList.toggle('hidden');
|
||||
if (languageDropdown && languageMenu) {
|
||||
languageDropdown.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
languageMenu.classList.toggle('opacity-0');
|
||||
languageMenu.classList.toggle('invisible');
|
||||
});
|
||||
|
||||
// Close language menu when clicking outside
|
||||
document.addEventListener('click', function(event) {
|
||||
if (!mobileLanguageToggle.contains(event.target) && !mobileLanguageMenu.contains(event.target)) {
|
||||
mobileLanguageMenu.classList.add('hidden');
|
||||
if (!languageDropdown.contains(event.target) && !languageMenu.contains(event.target)) {
|
||||
languageMenu.classList.add('opacity-0');
|
||||
languageMenu.classList.add('invisible');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<html lang="<%= locale || 'ko' %>" class="<%= theme === 'dark' ? 'dark' : '' %>">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
@@ -29,8 +29,9 @@
|
||||
<link href="https://unpkg.com/swiper@8/swiper-bundle.min.css" rel="stylesheet">
|
||||
<link href="/css/main.css" rel="stylesheet">
|
||||
<link href="/css/fixes.css" rel="stylesheet">
|
||||
<link href="/css/dark-theme.css" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<body class="font-sans dark:bg-gray-900 dark:text-gray-100">
|
||||
<%- include('partials/navigation') %>
|
||||
|
||||
<!-- Portfolio Header -->
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<html lang="<%= locale || 'ko' %>" class="<%= theme === 'dark' ? 'dark' : '' %>">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>포트폴리오 - SmartSolTech</title>
|
||||
<title><%- __('portfolio.meta.title') %> - SmartSolTech</title>
|
||||
|
||||
<!-- SEO Meta Tags -->
|
||||
<meta name="description" content="SmartSolTech의 다양한 프로젝트와 성공 사례를 확인해보세요. 웹 개발, 모바일 앱, UI/UX 디자인 포트폴리오.">
|
||||
<meta name="keywords" content="포트폴리오, 웹 개발, 모바일 앱, UI/UX 디자인, 프로젝트, SmartSolTech">
|
||||
<meta name="description" content="<%- __('portfolio.meta.description') %>">
|
||||
<meta name="keywords" content="<%- __('portfolio.meta.keywords') %>">
|
||||
|
||||
<!-- Open Graph -->
|
||||
<meta property="og:title" content="포트폴리오 - SmartSolTech">
|
||||
<meta property="og:description" content="SmartSolTech의 다양한 프로젝트와 성공 사례">
|
||||
<meta property="og:title" content="<%- __('portfolio.meta.og_title') %>">
|
||||
<meta property="og:description" content="<%- __('portfolio.meta.og_description') %>">
|
||||
<meta property="og:type" content="website">
|
||||
|
||||
<!-- PWA -->
|
||||
@@ -24,31 +24,33 @@
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
|
||||
<link href="https://unpkg.com/aos@2.3.1/dist/aos.css" rel="stylesheet">
|
||||
<link href="/css/main.css" rel="stylesheet">
|
||||
<link href="/css/fixes.css" rel="stylesheet">
|
||||
<link href="/css/dark-theme.css" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<body class="font-sans dark:bg-gray-900 dark:text-gray-100">
|
||||
<%- include('partials/navigation') %>
|
||||
|
||||
<!-- Portfolio Hero Section -->
|
||||
<section class="hero-section bg-gradient-to-br from-blue-600 via-purple-600 to-blue-800 pt-20">
|
||||
<section class="hero-section-compact bg-gradient-to-br from-blue-600 via-purple-600 to-blue-800">
|
||||
<div class="container mx-auto px-4 py-20 text-center text-white">
|
||||
<h1 class="text-5xl md:text-6xl font-bold mb-6" data-aos="fade-up">
|
||||
우리의 <span class="text-yellow-300">포트폴리오</span>
|
||||
<%- __('portfolio_page.title') %>
|
||||
</h1>
|
||||
<p class="text-xl md:text-2xl mb-8 opacity-90" data-aos="fade-up" data-aos-delay="200">
|
||||
혁신적인 프로젝트와 창의적인 솔루션들을 만나보세요
|
||||
<%- __('portfolio_page.subtitle') %>
|
||||
</p>
|
||||
<div class="flex flex-wrap justify-center gap-4" data-aos="fade-up" data-aos-delay="400">
|
||||
<button class="filter-btn bg-white text-blue-600 px-6 py-3 rounded-full font-semibold hover:bg-blue-50 transition-colors active" data-filter="all">
|
||||
전체
|
||||
<%- __('portfolio_page.categories.all') %>
|
||||
</button>
|
||||
<button class="filter-btn bg-blue-700 text-white px-6 py-3 rounded-full font-semibold hover:bg-blue-800 transition-colors" data-filter="web-development">
|
||||
웹 개발
|
||||
<%- __('portfolio_page.categories.web-development') %>
|
||||
</button>
|
||||
<button class="filter-btn bg-blue-700 text-white px-6 py-3 rounded-full font-semibold hover:bg-blue-800 transition-colors" data-filter="mobile-app">
|
||||
모바일 앱
|
||||
<%- __('portfolio_page.categories.mobile-app') %>
|
||||
</button>
|
||||
<button class="filter-btn bg-blue-700 text-white px-6 py-3 rounded-full font-semibold hover:bg-blue-800 transition-colors" data-filter="ui-ux-design">
|
||||
UI/UX 디자인
|
||||
<%- __('portfolio_page.categories.ui-ux-design') %>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -88,7 +90,7 @@
|
||||
<% if (item.featured) { %>
|
||||
<div class="absolute top-4 right-4">
|
||||
<span class="bg-yellow-500 text-white px-2 py-1 rounded-full text-xs font-bold">
|
||||
<i class="fas fa-star"></i> FEATURED
|
||||
<i class="fas fa-star"></i> <%- __('portfolio_page.labels.featured') %>
|
||||
</span>
|
||||
</div>
|
||||
<% } %>
|
||||
@@ -96,7 +98,7 @@
|
||||
<!-- Overlay -->
|
||||
<div class="absolute inset-0 bg-gradient-to-t from-black/50 to-transparent opacity-0 hover:opacity-100 transition-opacity duration-300 flex items-center justify-center">
|
||||
<a href="/portfolio/<%= item._id %>" class="bg-white text-gray-900 px-6 py-3 rounded-full font-semibold hover:bg-gray-100 transition-colors">
|
||||
자세히 보기
|
||||
<%- __('portfolio_page.buttons.details') %>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -149,7 +151,7 @@
|
||||
<!-- Action Button -->
|
||||
<div class="mt-4 pt-4 border-t border-gray-100">
|
||||
<a href="/portfolio/<%= item._id %>" class="block w-full bg-blue-600 text-white text-center py-2 rounded-lg hover:bg-blue-700 transition-colors font-semibold">
|
||||
프로젝트 상세보기
|
||||
<%- __('portfolio_page.buttons.projectDetails') %>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -158,8 +160,8 @@
|
||||
<% } else { %>
|
||||
<div class="col-span-full text-center py-12">
|
||||
<i class="fas fa-folder-open text-6xl text-gray-300 mb-4"></i>
|
||||
<h3 class="text-2xl font-bold text-gray-500 mb-2">아직 포트폴리오가 없습니다</h3>
|
||||
<p class="text-gray-400">곧 멋진 프로젝트들을 공개할 예정입니다!</p>
|
||||
<h3 class="text-2xl font-bold text-gray-500 mb-2"><%- __('portfolio_page.empty.title') %></h3>
|
||||
<p class="text-gray-400"><%- __('portfolio_page.empty.subtitle') %></p>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
@@ -168,7 +170,7 @@
|
||||
<% if (portfolioItems && portfolioItems.length >= 9) { %>
|
||||
<div class="text-center mt-12" data-aos="fade-up">
|
||||
<button id="load-more-btn" class="bg-blue-600 text-white px-8 py-3 rounded-full hover:bg-blue-700 transition-colors font-semibold">
|
||||
더 많은 프로젝트 보기
|
||||
<%- __('portfolio_page.buttons.loadMore') %>
|
||||
</button>
|
||||
</div>
|
||||
<% } %>
|
||||
@@ -179,17 +181,17 @@
|
||||
<section class="section-padding bg-gradient-to-r from-blue-600 to-purple-600 text-white">
|
||||
<div class="container mx-auto px-4 text-center">
|
||||
<h2 class="text-3xl md:text-4xl font-bold mb-6" data-aos="fade-up">
|
||||
다음 프로젝트의 주인공이 되어보세요
|
||||
<%- __('portfolio_page.cta.title') %>
|
||||
</h2>
|
||||
<p class="text-xl mb-8 opacity-90" data-aos="fade-up" data-aos-delay="200">
|
||||
우리와 함께 혁신적인 디지털 솔루션을 만들어보세요
|
||||
<%- __('portfolio_page.cta.subtitle') %>
|
||||
</p>
|
||||
<div class="flex flex-col sm:flex-row gap-4 justify-center" data-aos="fade-up" data-aos-delay="400">
|
||||
<a href="/contact" class="bg-white text-blue-600 px-8 py-3 rounded-full hover:bg-gray-100 transition-colors font-semibold">
|
||||
프로젝트 문의하기
|
||||
<%- __('portfolio_page.buttons.contact') %>
|
||||
</a>
|
||||
<a href="/calculator" class="border-2 border-white text-white px-8 py-3 rounded-full hover:bg-white hover:text-blue-600 transition-colors font-semibold">
|
||||
비용 계산하기
|
||||
<%- __('portfolio_page.buttons.calculate') %>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -266,30 +268,30 @@
|
||||
}
|
||||
});
|
||||
|
||||
// Category name mapping
|
||||
// Category name mapping - uses server-side localization
|
||||
function getCategoryName(category) {
|
||||
const categoryNames = {
|
||||
'web-development': '웹 개발',
|
||||
'mobile-app': '모바일 앱',
|
||||
'ui-ux-design': 'UI/UX 디자인',
|
||||
'branding': '브랜딩',
|
||||
'marketing': '디지털 마케팅'
|
||||
'web-development': '<%- __("portfolio_page.categories.web-development") %>',
|
||||
'mobile-app': '<%- __("portfolio_page.categories.mobile-app") %>',
|
||||
'ui-ux-design': '<%- __("portfolio_page.categories.ui-ux-design") %>',
|
||||
'branding': '<%- __("portfolio_page.categories.branding") %>',
|
||||
'marketing': '<%- __("portfolio_page.categories.marketing") %>'
|
||||
};
|
||||
return categoryNames[category] || category;
|
||||
}
|
||||
</script>
|
||||
|
||||
<%
|
||||
// Helper function for category names
|
||||
// Helper function for category names - uses i18n
|
||||
function getCategoryName(category) {
|
||||
const categoryNames = {
|
||||
'web-development': '웹 개발',
|
||||
'mobile-app': '모바일 앱',
|
||||
'ui-ux-design': 'UI/UX 디자인',
|
||||
'branding': '브랜딩',
|
||||
'marketing': '디지털 마케팅'
|
||||
const categoryMap = {
|
||||
'web-development': 'portfolio_page.categories.web-development',
|
||||
'mobile-app': 'portfolio_page.categories.mobile-app',
|
||||
'ui-ux-design': 'portfolio_page.categories.ui-ux-design',
|
||||
'branding': 'portfolio_page.categories.branding',
|
||||
'marketing': 'portfolio_page.categories.marketing'
|
||||
};
|
||||
return categoryNames[category] || category;
|
||||
return categoryMap[category] ? __(categoryMap[category]) : category;
|
||||
}
|
||||
%>
|
||||
</body>
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<html lang="<%= locale || 'ko' %>" class="<%= theme === 'dark' ? 'dark' : '' %>">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>서비스 - SmartSolTech</title>
|
||||
<title><%- __('services.meta.title') %> - SmartSolTech</title>
|
||||
|
||||
<!-- SEO Meta Tags -->
|
||||
<meta name="description" content="SmartSolTech의 전문 서비스를 확인하세요. 웹 개발, 모바일 앱, UI/UX 디자인, 디지털 마케팅 등 다양한 기술 솔루션을 제공합니다.">
|
||||
<meta name="keywords" content="웹 개발, 모바일 앱, UI/UX 디자인, 디지털 마케팅, 기술 솔루션, SmartSolTech">
|
||||
<meta name="description" content="<%- __('services.meta.description') %>">
|
||||
<meta name="keywords" content="<%- __('services.meta.keywords') %>">
|
||||
|
||||
<!-- PWA -->
|
||||
<meta name="theme-color" content="#3B82F6">
|
||||
@@ -19,18 +19,20 @@
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
|
||||
<link href="https://unpkg.com/aos@2.3.1/dist/aos.css" rel="stylesheet">
|
||||
<link href="/css/main.css" rel="stylesheet">
|
||||
<link href="/css/fixes.css" rel="stylesheet">
|
||||
<link href="/css/dark-theme.css" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<body class="font-sans dark:bg-gray-900 dark:text-gray-100">
|
||||
<%- include('partials/navigation') %>
|
||||
|
||||
<!-- Services Hero Section -->
|
||||
<section class="hero-section bg-gradient-to-br from-blue-600 via-purple-600 to-blue-800 pt-20">
|
||||
<div class="container mx-auto px-4 py-20 text-center text-white">
|
||||
<h1 class="text-5xl md:text-6xl font-bold mb-6" data-aos="fade-up">
|
||||
우리의 <span class="text-yellow-300">서비스</span>
|
||||
<!-- Services Hero Section - Компактный -->
|
||||
<section class="hero-section-compact bg-gradient-to-br from-blue-600 via-purple-600 to-blue-800">
|
||||
<div class="container mx-auto px-4 py-16 text-center text-white">
|
||||
<h1 class="text-4xl md:text-5xl font-bold mb-4" data-aos="fade-up">
|
||||
<%- __('services.hero.title') %> <span class="text-yellow-300"><%- __('services.hero.title_highlight') %></span>
|
||||
</h1>
|
||||
<p class="text-xl md:text-2xl mb-8 opacity-90" data-aos="fade-up" data-aos-delay="200">
|
||||
혁신적인 기술로 비즈니스의 성장을 지원합니다
|
||||
<%- __('services.hero.subtitle') %>
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
@@ -62,9 +64,9 @@
|
||||
<!-- Pricing -->
|
||||
<% if (service.pricing) { %>
|
||||
<div class="mb-6">
|
||||
<div class="text-sm text-gray-500 mb-1">시작가격</div>
|
||||
<div class="text-sm text-gray-500 mb-1"><%- __('services.cards.starting_price') %></div>
|
||||
<div class="text-2xl font-bold text-blue-600">
|
||||
<%= service.pricing.basePrice ? service.pricing.basePrice.toLocaleString() : '상담' %>
|
||||
<%= service.pricing.basePrice ? service.pricing.basePrice.toLocaleString() : __('services.cards.consultation') %>
|
||||
<% if (service.pricing.basePrice) { %>
|
||||
<span class="text-sm text-gray-500">원~</span>
|
||||
<% } %>
|
||||
@@ -82,11 +84,11 @@
|
||||
<div class="flex flex-col gap-3">
|
||||
<a href="/contact?service=<%= service._id %>"
|
||||
class="block w-full bg-gradient-to-r from-blue-600 to-purple-600 text-white text-center py-3 rounded-lg font-semibold hover:from-blue-700 hover:to-purple-700 transition-all duration-300">
|
||||
문의하기
|
||||
<%- __('services.cards.contact') %>
|
||||
</a>
|
||||
<a href="/calculator?service=<%= service._id %>"
|
||||
class="block w-full border-2 border-blue-600 text-blue-600 text-center py-3 rounded-lg font-semibold hover:bg-blue-600 hover:text-white transition-all duration-300">
|
||||
비용 계산하기
|
||||
<%- __('services.cards.calculate_cost') %>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -94,7 +96,7 @@
|
||||
<% if (service.featured) { %>
|
||||
<div class="absolute top-4 right-4">
|
||||
<span class="bg-yellow-500 text-white px-2 py-1 rounded-full text-xs font-bold">
|
||||
<i class="fas fa-star"></i> 인기
|
||||
<i class="fas fa-star"></i> <%- __('services.cards.popular') %>
|
||||
</span>
|
||||
</div>
|
||||
<% } %>
|
||||
@@ -103,8 +105,8 @@
|
||||
<% } else { %>
|
||||
<div class="col-span-full text-center py-12">
|
||||
<i class="fas fa-cogs text-6xl text-gray-300 mb-4"></i>
|
||||
<h3 class="text-2xl font-bold text-gray-500 mb-2">서비스 준비 중</h3>
|
||||
<p class="text-gray-400">곧 다양한 서비스를 제공할 예정입니다!</p>
|
||||
<h3 class="text-2xl font-bold text-gray-500 mb-2"><%- __('services.cards.coming_soon') %></h3>
|
||||
<p class="text-gray-400"><%- __('services.cards.coming_soon_desc') %></p>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
@@ -116,10 +118,10 @@
|
||||
<div class="container mx-auto px-4">
|
||||
<div class="text-center mb-16" data-aos="fade-up">
|
||||
<h2 class="text-3xl md:text-4xl font-bold text-gray-900 mb-4">
|
||||
프로젝트 진행 과정
|
||||
<%- __('services.process.title') %>
|
||||
</h2>
|
||||
<p class="text-xl text-gray-600">
|
||||
체계적이고 전문적인 프로세스로 프로젝트를 진행합니다
|
||||
<p class="text-lg text-gray-600 mb-12 max-w-3xl mx-auto">
|
||||
<%- __('services.process.subtitle') %>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user