AdminLTE3
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -1,227 +1,360 @@
|
||||
|
||||
<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>
|
||||
<!-- Dashboard Content -->
|
||||
<div class="row">
|
||||
<!-- Stats Cards -->
|
||||
<div class="col-lg-3 col-6">
|
||||
<div class="info-box">
|
||||
<span class="info-box-icon bg-info elevation-1">
|
||||
<i class="fas fa-briefcase"></i>
|
||||
</span>
|
||||
<div class="info-box-content">
|
||||
<span class="info-box-text">포트폴리오 프로젝트</span>
|
||||
<span class="info-box-number"><%= stats.portfolioCount || 0 %></span>
|
||||
<div class="progress">
|
||||
<div class="progress-bar bg-info" style="width: 70%"></div>
|
||||
</div>
|
||||
<span class="progress-description">
|
||||
70% 완료된 프로젝트
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 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 class="col-lg-3 col-6">
|
||||
<div class="info-box">
|
||||
<span class="info-box-icon bg-success elevation-1">
|
||||
<i class="fas fa-cogs"></i>
|
||||
</span>
|
||||
<div class="info-box-content">
|
||||
<span class="info-box-text">제공 서비스</span>
|
||||
<span class="info-box-number"><%= stats.servicesCount || 0 %></span>
|
||||
<div class="progress">
|
||||
<div class="progress-bar bg-success" style="width: 100%"></div>
|
||||
</div>
|
||||
<span class="progress-description">
|
||||
모든 서비스 활성화
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 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 class="col-lg-3 col-6">
|
||||
<div class="info-box">
|
||||
<span class="info-box-icon bg-warning elevation-1">
|
||||
<i class="fas fa-envelope"></i>
|
||||
</span>
|
||||
<div class="info-box-content">
|
||||
<span class="info-box-text">문의 메시지</span>
|
||||
<span class="info-box-number"><%= stats.contactsCount || 0 %></span>
|
||||
<div class="progress">
|
||||
<div class="progress-bar bg-warning" style="width: 60%"></div>
|
||||
</div>
|
||||
<span class="progress-description">
|
||||
60% 응답 완료
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-3 col-6">
|
||||
<div class="info-box">
|
||||
<span class="info-box-icon bg-danger elevation-1">
|
||||
<i class="fas fa-users"></i>
|
||||
</span>
|
||||
<div class="info-box-content">
|
||||
<span class="info-box-text">관리자 계정</span>
|
||||
<span class="info-box-number"><%= stats.usersCount || 0 %></span>
|
||||
<div class="progress">
|
||||
<div class="progress-bar bg-danger" style="width: 100%"></div>
|
||||
</div>
|
||||
<span class="progress-description">
|
||||
모든 계정 활성화
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main Content Row -->
|
||||
<div class="row">
|
||||
<!-- Recent Portfolio Projects -->
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">
|
||||
<i class="fas fa-briefcase mr-1"></i>
|
||||
최근 포트폴리오 프로젝트
|
||||
</h3>
|
||||
<div class="card-tools">
|
||||
<button type="button" class="btn btn-tool" data-card-widget="collapse">
|
||||
<i class="fas fa-minus"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<% if (recentPortfolio && recentPortfolio.length > 0) { %>
|
||||
<ul class="list-unstyled">
|
||||
<% recentPortfolio.forEach(function(project, index) { %>
|
||||
<li class="d-flex align-items-center <%= index < recentPortfolio.length - 1 ? 'border-bottom pb-3 mb-3' : '' %>">
|
||||
<div class="flex-shrink-0">
|
||||
<span class="badge badge-info badge-pill">
|
||||
<i class="fas fa-code"></i>
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex-grow-1 ml-3">
|
||||
<h6 class="mb-1 font-weight-bold"><%= project.title %></h6>
|
||||
<p class="text-muted mb-1"><%= project.category %></p>
|
||||
<small class="text-muted">
|
||||
<i class="fas fa-calendar mr-1"></i>
|
||||
<%= project.createdAt ? project.createdAt.toLocaleDateString('ko-KR') : '날짜 없음' %>
|
||||
</small>
|
||||
</div>
|
||||
<div class="flex-shrink-0">
|
||||
<span class="badge badge-<%= project.status === 'completed' ? 'success' : project.status === 'in-progress' ? 'warning' : 'secondary' %>">
|
||||
<%= project.status === 'completed' ? '완료' : project.status === 'in-progress' ? '진행중' : '계획' %>
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
<% }); %>
|
||||
</ul>
|
||||
<div class="text-center mt-3">
|
||||
<a href="/admin/portfolio" class="btn btn-primary btn-sm">
|
||||
<i class="fas fa-eye mr-1"></i>
|
||||
모든 프로젝트 보기
|
||||
</a>
|
||||
</div>
|
||||
<% } else { %>
|
||||
<div class="text-center py-4">
|
||||
<i class="fas fa-briefcase fa-3x text-muted mb-3"></i>
|
||||
<p class="text-muted">아직 포트폴리오 프로젝트가 없습니다.</p>
|
||||
<a href="/admin/portfolio/add" class="btn btn-primary btn-sm">
|
||||
<i class="fas fa-plus mr-1"></i>
|
||||
첫 번째 프로젝트 추가
|
||||
</a>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Recent Contact Messages -->
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">
|
||||
<i class="fas fa-envelope mr-1"></i>
|
||||
최근 문의 메시지
|
||||
</h3>
|
||||
<div class="card-tools">
|
||||
<button type="button" class="btn btn-tool" data-card-widget="collapse">
|
||||
<i class="fas fa-minus"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<% if (recentContacts && recentContacts.length > 0) { %>
|
||||
<ul class="list-unstyled">
|
||||
<% recentContacts.forEach(function(contact, index) { %>
|
||||
<li class="d-flex align-items-center <%= index < recentContacts.length - 1 ? 'border-bottom pb-3 mb-3' : '' %>">
|
||||
<div class="flex-shrink-0">
|
||||
<div class="bg-primary text-white rounded-circle d-flex align-items-center justify-content-center" style="width: 40px; height: 40px;">
|
||||
<%= contact.name ? contact.name.charAt(0).toUpperCase() : 'U' %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-grow-1 ml-3">
|
||||
<h6 class="mb-1 font-weight-bold"><%= contact.name %></h6>
|
||||
<p class="text-muted mb-1"><%= contact.email %></p>
|
||||
<small class="text-muted">
|
||||
<i class="fas fa-clock mr-1"></i>
|
||||
<%= contact.createdAt ? contact.createdAt.toLocaleDateString('ko-KR') : '날짜 없음' %>
|
||||
</small>
|
||||
</div>
|
||||
<div class="flex-shrink-0">
|
||||
<span class="badge badge-<%= contact.status === 'replied' ? 'success' : contact.status === 'pending' ? 'warning' : 'secondary' %>">
|
||||
<%= contact.status === 'replied' ? '답변완료' : contact.status === 'pending' ? '대기중' : '신규' %>
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
<% }); %>
|
||||
</ul>
|
||||
<div class="text-center mt-3">
|
||||
<a href="/admin/contacts" class="btn btn-warning btn-sm">
|
||||
<i class="fas fa-envelope-open mr-1"></i>
|
||||
모든 문의 보기
|
||||
</a>
|
||||
</div>
|
||||
<% } else { %>
|
||||
<div class="text-center py-4">
|
||||
<i class="fas fa-envelope fa-3x text-muted mb-3"></i>
|
||||
<p class="text-muted">새로운 문의가 없습니다.</p>
|
||||
<small class="text-muted">고객 문의가 들어오면 여기에 표시됩니다.</small>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick Actions & Tools -->
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">
|
||||
<i class="fas fa-bolt mr-1"></i>
|
||||
빠른 작업
|
||||
</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-lg-3 col-md-6">
|
||||
<div class="info-box bg-gradient-info">
|
||||
<span class="info-box-icon">
|
||||
<i class="fas fa-plus"></i>
|
||||
</span>
|
||||
<div class="info-box-content">
|
||||
<span class="info-box-text">새 프로젝트</span>
|
||||
<span class="info-box-number">추가하기</span>
|
||||
<div class="progress">
|
||||
<div class="progress-bar" style="width: 100%"></div>
|
||||
</div>
|
||||
<a href="/admin/portfolio/add" class="progress-description text-white">
|
||||
포트폴리오에 새 프로젝트 추가 →
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-3 col-md-6">
|
||||
<div class="info-box bg-gradient-success">
|
||||
<span class="info-box-icon">
|
||||
<i class="fas fa-cog"></i>
|
||||
</span>
|
||||
<div class="info-box-content">
|
||||
<span class="info-box-text">서비스 관리</span>
|
||||
<span class="info-box-number">설정</span>
|
||||
<div class="progress">
|
||||
<div class="progress-bar" style="width: 100%"></div>
|
||||
</div>
|
||||
<a href="/admin/services" class="progress-description text-white">
|
||||
서비스 가격 및 내용 수정 →
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-3 col-md-6">
|
||||
<div class="info-box bg-gradient-warning">
|
||||
<span class="info-box-icon">
|
||||
<i class="fas fa-images"></i>
|
||||
</span>
|
||||
<div class="info-box-content">
|
||||
<span class="info-box-text">미디어</span>
|
||||
<span class="info-box-number">업로드</span>
|
||||
<div class="progress">
|
||||
<div class="progress-bar" style="width: 100%"></div>
|
||||
</div>
|
||||
<a href="/admin/media" class="progress-description text-white">
|
||||
이미지 및 파일 관리 →
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-3 col-md-6">
|
||||
<div class="info-box bg-gradient-danger">
|
||||
<span class="info-box-icon">
|
||||
<i class="fas fa-wrench"></i>
|
||||
</span>
|
||||
<div class="info-box-content">
|
||||
<span class="info-box-text">사이트 설정</span>
|
||||
<span class="info-box-number">관리</span>
|
||||
<div class="progress">
|
||||
<div class="progress-bar" style="width: 100%"></div>
|
||||
</div>
|
||||
<a href="/admin/settings" class="progress-description text-white">
|
||||
전체 사이트 설정 변경 →
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- System Status -->
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="card card-primary">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">
|
||||
<i class="fas fa-server mr-1"></i>
|
||||
시스템 상태
|
||||
</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
<div class="description-block border-right">
|
||||
<span class="description-percentage text-success">
|
||||
<i class="fas fa-caret-up"></i> 99.2%
|
||||
</span>
|
||||
<h5 class="description-header">서버 업타임</h5>
|
||||
<span class="description-text">지난 30일</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="description-block">
|
||||
<span class="description-percentage text-info">
|
||||
<i class="fas fa-caret-up"></i> 2.3초
|
||||
</span>
|
||||
<h5 class="description-header">평균 응답시간</h5>
|
||||
<span class="description-text">페이지 로딩</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<div class="card card-success">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">
|
||||
<i class="fab fa-telegram mr-1"></i>
|
||||
텔레그램 봇 상태
|
||||
</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
<div class="description-block border-right">
|
||||
<span class="description-percentage text-success">
|
||||
<i class="fas fa-check-circle"></i> 연결됨
|
||||
</span>
|
||||
<h5 class="description-header">봇 상태</h5>
|
||||
<span class="description-text">정상 작동</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="description-block">
|
||||
<span class="description-percentage text-info">
|
||||
<i class="fas fa-paper-plane"></i> 24개
|
||||
</span>
|
||||
<h5 class="description-header">전송된 알림</h5>
|
||||
<span class="description-text">오늘</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-center mt-2">
|
||||
<a href="/admin/telegram" class="btn btn-success btn-sm">
|
||||
<i class="fab fa-telegram mr-1"></i>
|
||||
텔레그램 설정
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -5,100 +5,349 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title><%= title %> - SmartSolTech Admin</title>
|
||||
|
||||
<!-- Tailwind CSS -->
|
||||
<link rel="stylesheet" href="/css/tailwind.css">
|
||||
<!-- Bootstrap CSS (Local) -->
|
||||
<link rel="stylesheet" href="/vendor/bootstrap/bootstrap.min.css">
|
||||
|
||||
<!-- Font Awesome -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
|
||||
<!-- AdminLTE CSS (Local) -->
|
||||
<link rel="stylesheet" href="/vendor/adminlte/adminlte.min.css">
|
||||
|
||||
<!-- Custom CSS -->
|
||||
<link rel="stylesheet" href="/css/main.css">
|
||||
<link rel="stylesheet" href="/css/fixes.css">
|
||||
<!-- Font Awesome (Local) -->
|
||||
<link rel="stylesheet" href="/vendor/adminlte/fontawesome.min.css">
|
||||
|
||||
<!-- Google Fonts -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@300;400;500;700&display=swap" rel="stylesheet">
|
||||
|
||||
<!-- Custom Korean Admin Styles -->
|
||||
<style>
|
||||
.admin-sidebar {
|
||||
min-height: calc(100vh - 64px);
|
||||
body, .content-wrapper, .main-sidebar {
|
||||
font-family: 'Noto Sans KR', 'Malgun Gothic', 'Apple SD Gothic Neo', sans-serif;
|
||||
}
|
||||
|
||||
.brand-text {
|
||||
font-weight: 700;
|
||||
color: #007bff !important;
|
||||
}
|
||||
|
||||
/* Sidebar navigation styles */
|
||||
.main-sidebar .nav-sidebar .nav-link {
|
||||
border-radius: 8px;
|
||||
margin: 2px 8px;
|
||||
transition: all 0.3s ease;
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
.main-sidebar .nav-sidebar .nav-link:hover {
|
||||
background-color: rgba(0, 123, 255, 0.1);
|
||||
color: #007bff;
|
||||
transform: translateX(5px);
|
||||
}
|
||||
|
||||
.main-sidebar .nav-sidebar .nav-link.active {
|
||||
background: linear-gradient(135deg, #007bff 0%, #0056b3 100%);
|
||||
color: white !important;
|
||||
box-shadow: 0 4px 8px rgba(0, 123, 255, 0.3);
|
||||
}
|
||||
|
||||
.main-sidebar .nav-sidebar .nav-link.active i {
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.main-sidebar .nav-sidebar .nav-link.active .badge {
|
||||
background-color: rgba(255, 255, 255, 0.2) !important;
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
/* Fix for navigation icons */
|
||||
.nav-icon {
|
||||
margin-right: 8px;
|
||||
width: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.content-header h1 {
|
||||
font-weight: 600;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
.card {
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||
border: none;
|
||||
}
|
||||
|
||||
.btn {
|
||||
border-radius: 8px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.info-box {
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.info-box-icon {
|
||||
border-radius: 12px 0 0 12px;
|
||||
}
|
||||
|
||||
.navbar-light {
|
||||
background: white !important;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.main-sidebar {
|
||||
background: linear-gradient(180deg, #f8f9fa 0%, #e9ecef 100%);
|
||||
}
|
||||
|
||||
/* Breadcrumb styling */
|
||||
.breadcrumb {
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.breadcrumb-item a {
|
||||
color: #007bff;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.breadcrumb-item.active {
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
/* Content wrapper padding */
|
||||
.content-wrapper {
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
||||
/* Badges in navigation */
|
||||
.nav-sidebar .badge {
|
||||
font-size: 0.75rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="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">
|
||||
<body class="hold-transition sidebar-mini layout-fixed">
|
||||
<div class="wrapper">
|
||||
<!-- Navbar -->
|
||||
<nav class="main-header navbar navbar-expand navbar-light">
|
||||
<!-- Left navbar links -->
|
||||
<ul class="navbar-nav">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-widget="pushmenu" href="#" role="button">
|
||||
<i class="fas fa-bars"></i>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item d-none d-sm-inline-block">
|
||||
<a href="/" class="nav-link" target="_blank">
|
||||
<i class="fas fa-external-link-alt mr-1"></i>
|
||||
Посмотреть сайт
|
||||
사이트 보기
|
||||
</a>
|
||||
<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>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<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 <%= currentPage === 'dashboard' ? 'bg-blue-100 text-blue-700' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900' %>">
|
||||
<i class="fas fa-tachometer-alt mr-3"></i>
|
||||
Панель управления
|
||||
<!-- Right navbar links -->
|
||||
<ul class="navbar-nav ml-auto">
|
||||
<!-- User Menu -->
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link" data-toggle="dropdown" href="#">
|
||||
<i class="far fa-user mr-1"></i>
|
||||
<%= user ? user.name : '관리자' %>
|
||||
</a>
|
||||
<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>
|
||||
<div class="dropdown-menu dropdown-menu-lg dropdown-menu-right">
|
||||
<div class="dropdown-divider"></div>
|
||||
<a href="/admin/settings" class="dropdown-item">
|
||||
<i class="fas fa-cog mr-2"></i> 설정
|
||||
</a>
|
||||
<div class="dropdown-divider"></div>
|
||||
<form action="/admin/logout" method="post" class="dropdown-item p-0">
|
||||
<button type="submit" class="btn btn-link text-left w-100 text-danger">
|
||||
<i class="fas fa-sign-out-alt mr-2"></i> 로그아웃
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<!-- Main Sidebar Container -->
|
||||
<aside class="main-sidebar sidebar-light-primary elevation-4">
|
||||
<!-- Brand Logo -->
|
||||
<a href="/admin/dashboard" class="brand-link">
|
||||
<img src="/images/icons/icon-192x192.png" alt="SmartSolTech" class="brand-image img-circle elevation-3" style="opacity: .8">
|
||||
<span class="brand-text font-weight-bold">SmartSolTech</span>
|
||||
</a>
|
||||
|
||||
<!-- Sidebar -->
|
||||
<div class="sidebar">
|
||||
<!-- Sidebar Menu -->
|
||||
<nav class="mt-3">
|
||||
<ul class="nav nav-pills nav-sidebar flex-column" data-widget="treeview" role="menu" data-accordion="false">
|
||||
<li class="nav-item">
|
||||
<a href="/admin/dashboard" class="nav-link <%= currentPage === 'dashboard' ? 'active' : '' %>">
|
||||
<i class="nav-icon fas fa-tachometer-alt"></i>
|
||||
<p>대시보드</p>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a href="/admin/portfolio" class="nav-link <%= currentPage === 'portfolio' ? 'active' : '' %>">
|
||||
<i class="nav-icon fas fa-briefcase"></i>
|
||||
<p>
|
||||
포트폴리오
|
||||
<% if (typeof stats !== 'undefined' && stats.portfolioCount) { %>
|
||||
<span class="badge badge-info right"><%= stats.portfolioCount %></span>
|
||||
<% } %>
|
||||
</p>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a href="/admin/services" class="nav-link <%= currentPage === 'services' ? 'active' : '' %>">
|
||||
<i class="nav-icon fas fa-cogs"></i>
|
||||
<p>
|
||||
서비스 관리
|
||||
<% if (typeof stats !== 'undefined' && stats.servicesCount) { %>
|
||||
<span class="badge badge-success right"><%= stats.servicesCount %></span>
|
||||
<% } %>
|
||||
</p>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a href="/admin/contacts" class="nav-link <%= currentPage === 'contacts' ? 'active' : '' %>">
|
||||
<i class="nav-icon fas fa-envelope"></i>
|
||||
<p>
|
||||
문의 관리
|
||||
<% if (typeof stats !== 'undefined' && stats.contactsCount) { %>
|
||||
<span class="badge badge-warning right"><%= stats.contactsCount %></span>
|
||||
<% } %>
|
||||
</p>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a href="/admin/media" class="nav-link <%= currentPage === 'media' ? 'active' : '' %>">
|
||||
<i class="nav-icon fas fa-images"></i>
|
||||
<p>미디어 관리</p>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-header">시스템 설정</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a href="/admin/settings" class="nav-link <%= currentPage === 'settings' ? 'active' : '' %>">
|
||||
<i class="nav-icon fas fa-cog"></i>
|
||||
<p>사이트 설정</p>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a href="/admin/telegram" class="nav-link <%= currentPage === 'telegram' ? 'active' : '' %>">
|
||||
<i class="nav-icon fab fa-telegram"></i>
|
||||
<p>텔레그램 봇</p>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a href="/admin/banner-editor" class="nav-link <%= currentPage === 'banner-editor' ? 'active' : '' %>">
|
||||
<i class="nav-icon fas fa-paint-brush"></i>
|
||||
<p>배너 편집기</p>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="flex-1 p-8">
|
||||
<%- body %>
|
||||
</main>
|
||||
<!-- Content Wrapper -->
|
||||
<div class="content-wrapper">
|
||||
<!-- Content Header -->
|
||||
<div class="content-header">
|
||||
<div class="container-fluid">
|
||||
<div class="row mb-2">
|
||||
<div class="col-sm-6">
|
||||
<h1 class="m-0"><%= title %></h1>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<ol class="breadcrumb float-sm-right">
|
||||
<li class="breadcrumb-item"><a href="/admin/dashboard">홈</a></li>
|
||||
<% if (currentPage !== 'dashboard') { %>
|
||||
<li class="breadcrumb-item active"><%= title %></li>
|
||||
<% } %>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main content -->
|
||||
<section class="content">
|
||||
<div class="container-fluid">
|
||||
<%- body %>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="main-footer">
|
||||
<strong>© 2024 <a href="/">SmartSolTech</a></strong>
|
||||
모든 권리 보유.
|
||||
<div class="float-right d-none d-sm-inline-block">
|
||||
<b>Version</b> 2.0.0
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<!-- JavaScript -->
|
||||
<!-- jQuery (Local) -->
|
||||
<script src="/vendor/jquery/jquery-3.6.0.min.js"></script>
|
||||
<!-- Bootstrap 4 (Local) -->
|
||||
<script src="/vendor/bootstrap/bootstrap.bundle.min.js"></script>
|
||||
<!-- AdminLTE App (Local) -->
|
||||
<script src="/vendor/adminlte/adminlte.min.js"></script>
|
||||
|
||||
<!-- Custom JavaScript -->
|
||||
<script src="/js/main.js"></script>
|
||||
|
||||
<script>
|
||||
// Korean localization for AdminLTE
|
||||
$(document).ready(function() {
|
||||
// Update any English text to Korean
|
||||
$('.brand-link .brand-text').text('스마트솔테크');
|
||||
|
||||
// Get current page from URL
|
||||
const currentPath = window.location.pathname;
|
||||
const currentPage = currentPath.split('/').pop() || 'dashboard';
|
||||
|
||||
// Remove active class from all nav links
|
||||
$('.nav-sidebar .nav-link').removeClass('active');
|
||||
|
||||
// Add active class to current page nav link
|
||||
$('.nav-sidebar .nav-link').each(function() {
|
||||
const href = $(this).attr('href');
|
||||
if (href) {
|
||||
const pageName = href.split('/').pop();
|
||||
if (currentPath.includes(pageName) ||
|
||||
(currentPath === '/admin' && pageName === 'dashboard') ||
|
||||
(currentPath === '/admin/' && pageName === 'dashboard')) {
|
||||
$(this).addClass('active');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Add smooth transitions
|
||||
$('.nav-link').on('click', function() {
|
||||
if (!$(this).hasClass('active')) {
|
||||
$('.nav-link.active').removeClass('active');
|
||||
$(this).addClass('active');
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize AdminLTE components
|
||||
if (typeof AdminLTE !== 'undefined') {
|
||||
AdminLTE.init();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,241 +1,709 @@
|
||||
<!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">
|
||||
|
||||
<!-- 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">
|
||||
Выбрать файлы
|
||||
<!-- Content Header (Page header) -->
|
||||
<section class="content-header">
|
||||
<div class="container-fluid">
|
||||
<div class="row mb-2">
|
||||
<div class="col-sm-6">
|
||||
<h1><i class="fas fa-images mr-2"></i>Медиа Галерея</h1>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<div class="float-sm-right">
|
||||
<button id="refresh-btn" class="btn btn-secondary">
|
||||
<i class="fas fa-sync-alt mr-1"></i>Обновить
|
||||
</button>
|
||||
<button id="cancel-upload" class="bg-gray-500 hover:bg-gray-600 text-white px-6 py-3 rounded-lg ml-3">
|
||||
Отмена
|
||||
<button id="upload-btn" class="btn btn-primary">
|
||||
<i class="fas fa-upload mr-1"></i>Загрузить файлы
|
||||
</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>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 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>
|
||||
<!-- Main content -->
|
||||
<section class="content">
|
||||
<div class="container-fluid">
|
||||
<!-- Upload Zone -->
|
||||
<div id="upload-zone" class="card" style="display: none;">
|
||||
<div class="card-body text-center">
|
||||
<div class="mb-4">
|
||||
<i class="fas fa-cloud-upload-alt fa-6x text-muted mb-4"></i>
|
||||
<p class="h5 text-muted mb-2">Перетащите файлы сюда или нажмите для выбора</p>
|
||||
<p class="text-muted">Поддерживаются: JPG, PNG, GIF, SVG (максимум 10MB каждый)</p>
|
||||
</div>
|
||||
<input type="file" id="file-input" multiple accept="image/*" class="d-none">
|
||||
<button type="button" onclick="document.getElementById('file-input').click()" class="btn btn-primary">
|
||||
Выбрать файлы
|
||||
</button>
|
||||
<button id="cancel-upload" class="btn btn-secondary ml-3">
|
||||
Отмена
|
||||
</button>
|
||||
</div>
|
||||
<div 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>
|
||||
|
||||
<!-- Upload Progress -->
|
||||
<div id="upload-progress" class="card" style="display: none;">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Загрузка файлов</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="progress-list">
|
||||
<!-- Progress items will be added here -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filter and Search -->
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-3">
|
||||
<div class="form-group">
|
||||
<label>Тип файла</label>
|
||||
<select id="file-type-filter" class="form-control">
|
||||
<option value="">Все типы</option>
|
||||
<option value="image/jpeg">JPEG</option>
|
||||
<option value="image/png">PNG</option>
|
||||
<option value="image/gif">GIF</option>
|
||||
<option value="image/svg+xml">SVG</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="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 class="col-md-3">
|
||||
<div class="form-group">
|
||||
<label>Размер</label>
|
||||
<select id="size-filter" class="form-control">
|
||||
<option value="">Любой размер</option>
|
||||
<option value="small">Маленький (< 1MB)</option>
|
||||
<option value="medium">Средний (1-5MB)</option>
|
||||
<option value="large">Большой (> 5MB)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label>Поиск</label>
|
||||
<div class="input-group">
|
||||
<input type="text" id="search-input" placeholder="Поиск по имени файла..." class="form-control">
|
||||
<div class="input-group-append">
|
||||
<span class="input-group-text"><i class="fas fa-search"></i></span>
|
||||
</div>
|
||||
</div>
|
||||
<div 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>
|
||||
<!-- Media Grid -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Файлы</h3>
|
||||
<div class="card-tools">
|
||||
<span id="file-count" class="badge badge-secondary">Загрузка...</span>
|
||||
<div class="btn-group ml-2">
|
||||
<button id="grid-view" class="btn btn-sm btn-default">
|
||||
<i class="fas fa-th-large"></i>
|
||||
</button>
|
||||
<button id="list-view" class="btn btn-sm btn-default">
|
||||
<i class="fas fa-list"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<!-- Loading State -->
|
||||
<div id="loading" class="text-center py-5">
|
||||
<div class="spinner-border text-primary" role="status">
|
||||
<span class="sr-only">Загрузка...</span>
|
||||
</div>
|
||||
<p class="mt-2 text-muted">Загрузка медиа файлов...</p>
|
||||
</div>
|
||||
|
||||
<!-- Empty State -->
|
||||
<div id="empty-state" class="text-center py-5" style="display: none;">
|
||||
<i class="fas fa-images fa-6x text-muted mb-4"></i>
|
||||
<h4 class="text-muted mb-2">Нет загруженных файлов</h4>
|
||||
<p class="text-muted mb-4">Начните с загрузки ваших первых изображений</p>
|
||||
<button onclick="document.getElementById('upload-btn').click()" class="btn btn-primary">
|
||||
<i class="fas fa-upload mr-2"></i>Загрузить файлы
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Media Grid -->
|
||||
<div id="media-grid" class="row">
|
||||
<!-- Media items will be loaded here -->
|
||||
</div>
|
||||
|
||||
<!-- Media List -->
|
||||
<div id="media-list" style="display: none;">
|
||||
<!-- List items will be loaded here -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<div class="card-footer" id="pagination" style="display: none;">
|
||||
<nav aria-label="Page navigation">
|
||||
<ul class="pagination justify-content-center m-0">
|
||||
<li class="page-item">
|
||||
<button id="prev-page" class="page-link">
|
||||
<i class="fas fa-chevron-left"></i>
|
||||
</button>
|
||||
</li>
|
||||
<div id="page-numbers" class="d-flex">
|
||||
<!-- Page numbers will be added here -->
|
||||
</div>
|
||||
<li class="page-item">
|
||||
<button id="next-page" class="page-link">
|
||||
<i class="fas fa-chevron-right"></i>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Media Preview Modal -->
|
||||
<div class="modal fade" id="preview-modal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog modal-xl" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 id="modal-title" class="modal-title">Предпросмотр файла</h4>
|
||||
<button type="button" class="close" data-dismiss="modal">
|
||||
<span>×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<img id="modal-image" src="" alt="" class="img-fluid rounded">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="form-group">
|
||||
<label>Имя файла</label>
|
||||
<input id="modal-filename" type="text" class="form-control" readonly>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>URL</label>
|
||||
<div class="input-group">
|
||||
<input id="modal-url" type="text" class="form-control" readonly>
|
||||
<div class="input-group-append">
|
||||
<button onclick="copyToClipboard()" class="btn btn-outline-secondary" type="button">
|
||||
<i class="fas fa-copy"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
<div class="form-group">
|
||||
<label>Размер</label>
|
||||
<p id="modal-size" class="form-control-plaintext">-</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="form-group">
|
||||
<label>Тип</label>
|
||||
<p id="modal-type" class="form-control-plaintext">-</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
<div class="form-group">
|
||||
<label>Ширина</label>
|
||||
<p id="modal-width" class="form-control-plaintext">-</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="form-group">
|
||||
<label>Высота</label>
|
||||
<p id="modal-height" class="form-control-plaintext">-</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Загружено</label>
|
||||
<p id="modal-date" class="form-control-plaintext">-</p>
|
||||
</div>
|
||||
<div class="btn-group d-flex">
|
||||
<button onclick="downloadFile()" class="btn btn-primary">
|
||||
<i class="fas fa-download mr-1"></i>Скачать
|
||||
</button>
|
||||
<button onclick="deleteFile()" class="btn btn-danger">
|
||||
<i class="fas fa-trash mr-1"></i>Удалить
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Scripts -->
|
||||
<script>
|
||||
let currentFiles = [];
|
||||
let filteredFiles = [];
|
||||
let currentPage = 1;
|
||||
let filesPerPage = 24;
|
||||
let isGridView = true;
|
||||
let currentFile = null;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
loadFiles();
|
||||
setupEventListeners();
|
||||
});
|
||||
|
||||
function setupEventListeners() {
|
||||
// Upload button
|
||||
document.getElementById('upload-btn').addEventListener('click', function() {
|
||||
document.getElementById('upload-zone').style.display = 'block';
|
||||
});
|
||||
|
||||
// Cancel upload
|
||||
document.getElementById('cancel-upload').addEventListener('click', function() {
|
||||
document.getElementById('upload-zone').style.display = 'none';
|
||||
});
|
||||
|
||||
// File input
|
||||
document.getElementById('file-input').addEventListener('change', handleFileSelect);
|
||||
|
||||
// Refresh
|
||||
document.getElementById('refresh-btn').addEventListener('click', loadFiles);
|
||||
|
||||
// View toggle
|
||||
document.getElementById('grid-view').addEventListener('click', function() {
|
||||
isGridView = true;
|
||||
updateViewButtons();
|
||||
renderFiles();
|
||||
});
|
||||
|
||||
document.getElementById('list-view').addEventListener('click', function() {
|
||||
isGridView = false;
|
||||
updateViewButtons();
|
||||
renderFiles();
|
||||
});
|
||||
|
||||
// Filters
|
||||
document.getElementById('file-type-filter').addEventListener('change', applyFilters);
|
||||
document.getElementById('size-filter').addEventListener('change', applyFilters);
|
||||
document.getElementById('search-input').addEventListener('input', applyFilters);
|
||||
|
||||
// Drag and drop
|
||||
const uploadZone = document.getElementById('upload-zone');
|
||||
uploadZone.addEventListener('dragover', function(e) {
|
||||
e.preventDefault();
|
||||
uploadZone.classList.add('border-blue-500');
|
||||
});
|
||||
|
||||
uploadZone.addEventListener('dragleave', function(e) {
|
||||
e.preventDefault();
|
||||
uploadZone.classList.remove('border-blue-500');
|
||||
});
|
||||
|
||||
uploadZone.addEventListener('drop', function(e) {
|
||||
e.preventDefault();
|
||||
uploadZone.classList.remove('border-blue-500');
|
||||
handleFileSelect({ target: { files: e.dataTransfer.files } });
|
||||
});
|
||||
}
|
||||
|
||||
function updateViewButtons() {
|
||||
const gridBtn = document.getElementById('grid-view');
|
||||
const listBtn = document.getElementById('list-view');
|
||||
|
||||
if (isGridView) {
|
||||
gridBtn.classList.add('bg-blue-500', 'text-white');
|
||||
gridBtn.classList.remove('btn-default');
|
||||
listBtn.classList.remove('bg-blue-500', 'text-white');
|
||||
listBtn.classList.add('btn-default');
|
||||
} else {
|
||||
listBtn.classList.add('bg-blue-500', 'text-white');
|
||||
listBtn.classList.remove('btn-default');
|
||||
gridBtn.classList.remove('bg-blue-500', 'text-white');
|
||||
gridBtn.classList.add('btn-default');
|
||||
}
|
||||
}
|
||||
|
||||
async function loadFiles() {
|
||||
try {
|
||||
document.getElementById('loading').style.display = 'block';
|
||||
document.getElementById('empty-state').style.display = 'none';
|
||||
document.getElementById('media-grid').style.display = 'none';
|
||||
document.getElementById('media-list').style.display = 'none';
|
||||
|
||||
const response = await fetch('/api/admin/media');
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
currentFiles = data.files;
|
||||
filteredFiles = [...currentFiles];
|
||||
updateFileCount();
|
||||
renderFiles();
|
||||
|
||||
if (currentFiles.length === 0) {
|
||||
document.getElementById('empty-state').style.display = 'block';
|
||||
}
|
||||
} else {
|
||||
throw new Error(data.message);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading files:', error);
|
||||
alert('Ошибка загрузки файлов: ' + error.message);
|
||||
} finally {
|
||||
document.getElementById('loading').style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
function applyFilters() {
|
||||
const typeFilter = document.getElementById('file-type-filter').value;
|
||||
const sizeFilter = document.getElementById('size-filter').value;
|
||||
const searchQuery = document.getElementById('search-input').value.toLowerCase();
|
||||
|
||||
filteredFiles = currentFiles.filter(file => {
|
||||
let matches = true;
|
||||
|
||||
// Type filter
|
||||
if (typeFilter && file.mimetype !== typeFilter) {
|
||||
matches = false;
|
||||
}
|
||||
|
||||
// Size filter
|
||||
if (sizeFilter) {
|
||||
const sizeMB = file.size / (1024 * 1024);
|
||||
if (sizeFilter === 'small' && sizeMB >= 1) matches = false;
|
||||
if (sizeFilter === 'medium' && (sizeMB < 1 || sizeMB > 5)) matches = false;
|
||||
if (sizeFilter === 'large' && sizeMB <= 5) matches = false;
|
||||
}
|
||||
|
||||
// Search filter
|
||||
if (searchQuery && !file.filename.toLowerCase().includes(searchQuery)) {
|
||||
matches = false;
|
||||
}
|
||||
|
||||
return matches;
|
||||
});
|
||||
|
||||
currentPage = 1;
|
||||
updateFileCount();
|
||||
renderFiles();
|
||||
}
|
||||
|
||||
function updateFileCount() {
|
||||
const countElement = document.getElementById('file-count');
|
||||
countElement.textContent = `${filteredFiles.length} файлов`;
|
||||
}
|
||||
|
||||
function renderFiles() {
|
||||
const gridContainer = document.getElementById('media-grid');
|
||||
const listContainer = document.getElementById('media-list');
|
||||
|
||||
if (isGridView) {
|
||||
gridContainer.style.display = 'block';
|
||||
listContainer.style.display = 'none';
|
||||
renderGridView();
|
||||
} else {
|
||||
gridContainer.style.display = 'none';
|
||||
listContainer.style.display = 'block';
|
||||
renderListView();
|
||||
}
|
||||
|
||||
renderPagination();
|
||||
}
|
||||
|
||||
function renderGridView() {
|
||||
const container = document.getElementById('media-grid');
|
||||
const startIndex = (currentPage - 1) * filesPerPage;
|
||||
const endIndex = startIndex + filesPerPage;
|
||||
const pageFiles = filteredFiles.slice(startIndex, endIndex);
|
||||
|
||||
container.innerHTML = pageFiles.map(file => `
|
||||
<div class="col-md-2 col-sm-3 col-6 mb-3">
|
||||
<div class="card h-100">
|
||||
<div class="card-img-top" style="height: 150px; background-image: url('${file.url}'); background-size: cover; background-position: center; cursor: pointer;" onclick="openPreview('${file._id}')"></div>
|
||||
<div class="card-body p-2">
|
||||
<p class="card-text small text-truncate" title="${file.filename}">${file.filename}</p>
|
||||
<small class="text-muted">${formatFileSize(file.size)}</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
function renderListView() {
|
||||
const container = document.getElementById('media-list');
|
||||
const startIndex = (currentPage - 1) * filesPerPage;
|
||||
const endIndex = startIndex + filesPerPage;
|
||||
const pageFiles = filteredFiles.slice(startIndex, endIndex);
|
||||
|
||||
container.innerHTML = pageFiles.map(file => `
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-md-1">
|
||||
<img src="${file.url}" alt="${file.filename}" class="img-thumbnail" style="width: 60px; height: 60px; object-fit: cover;">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<h6 class="mb-1">${file.filename}</h6>
|
||||
<small class="text-muted">${file.mimetype}</small>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<span class="badge badge-secondary">${formatFileSize(file.size)}</span>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<small class="text-muted">${new Date(file.uploadedAt).toLocaleDateString('ru-RU')}</small>
|
||||
</div>
|
||||
<div class="col-md-2 text-right">
|
||||
<div class="btn-group">
|
||||
<button onclick="openPreview('${file._id}')" class="btn btn-sm btn-outline-primary">
|
||||
<i class="fas fa-eye"></i>
|
||||
</button>
|
||||
<button onclick="downloadFile('${file._id}')" class="btn btn-sm btn-outline-secondary">
|
||||
<i class="fas fa-download"></i>
|
||||
</button>
|
||||
<button onclick="deleteFile('${file._id}')" class="btn btn-sm btn-outline-danger">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
function renderPagination() {
|
||||
const totalPages = Math.ceil(filteredFiles.length / filesPerPage);
|
||||
const paginationContainer = document.getElementById('pagination');
|
||||
|
||||
if (totalPages <= 1) {
|
||||
paginationContainer.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
paginationContainer.style.display = 'block';
|
||||
|
||||
// Update prev/next buttons
|
||||
const prevBtn = document.getElementById('prev-page');
|
||||
const nextBtn = document.getElementById('next-page');
|
||||
|
||||
prevBtn.disabled = currentPage === 1;
|
||||
nextBtn.disabled = currentPage === totalPages;
|
||||
|
||||
prevBtn.onclick = () => {
|
||||
if (currentPage > 1) {
|
||||
currentPage--;
|
||||
renderFiles();
|
||||
}
|
||||
};
|
||||
|
||||
nextBtn.onclick = () => {
|
||||
if (currentPage < totalPages) {
|
||||
currentPage++;
|
||||
renderFiles();
|
||||
}
|
||||
};
|
||||
|
||||
// Update page numbers
|
||||
const pageNumbersContainer = document.getElementById('page-numbers');
|
||||
let pageNumbers = '';
|
||||
|
||||
for (let i = 1; i <= totalPages; i++) {
|
||||
if (i === currentPage) {
|
||||
pageNumbers += `<li class="page-item active"><span class="page-link">${i}</span></li>`;
|
||||
} else {
|
||||
pageNumbers += `<li class="page-item"><button class="page-link" onclick="goToPage(${i})">${i}</button></li>`;
|
||||
}
|
||||
}
|
||||
|
||||
pageNumbersContainer.innerHTML = pageNumbers;
|
||||
}
|
||||
|
||||
function goToPage(page) {
|
||||
currentPage = page;
|
||||
renderFiles();
|
||||
}
|
||||
|
||||
function 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];
|
||||
}
|
||||
|
||||
function openPreview(fileId) {
|
||||
const file = currentFiles.find(f => f._id === fileId);
|
||||
if (!file) return;
|
||||
|
||||
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 = file.url;
|
||||
document.getElementById('modal-size').textContent = formatFileSize(file.size);
|
||||
document.getElementById('modal-type').textContent = file.mimetype;
|
||||
document.getElementById('modal-width').textContent = file.width || '-';
|
||||
document.getElementById('modal-height').textContent = file.height || '-';
|
||||
document.getElementById('modal-date').textContent = new Date(file.uploadedAt).toLocaleDateString('ru-RU');
|
||||
|
||||
$('#preview-modal').modal('show');
|
||||
}
|
||||
|
||||
function copyToClipboard() {
|
||||
const urlInput = document.getElementById('modal-url');
|
||||
urlInput.select();
|
||||
document.execCommand('copy');
|
||||
|
||||
// Show feedback
|
||||
const btn = event.target.closest('button');
|
||||
const originalHtml = btn.innerHTML;
|
||||
btn.innerHTML = '<i class="fas fa-check"></i>';
|
||||
setTimeout(() => {
|
||||
btn.innerHTML = originalHtml;
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
function downloadFile() {
|
||||
if (currentFile) {
|
||||
const link = document.createElement('a');
|
||||
link.href = currentFile.url;
|
||||
link.download = currentFile.filename;
|
||||
link.click();
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteFile() {
|
||||
if (!currentFile) return;
|
||||
|
||||
if (!confirm(`Вы уверены, что хотите удалить файл "${currentFile.filename}"?`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/admin/media/${currentFile._id}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
$('#preview-modal').modal('hide');
|
||||
loadFiles();
|
||||
alert('Файл успешно удален');
|
||||
} else {
|
||||
throw new Error(data.message);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error deleting file:', error);
|
||||
alert('Ошибка удаления файла: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleFileSelect(event) {
|
||||
const files = Array.from(event.target.files);
|
||||
if (files.length === 0) return;
|
||||
|
||||
document.getElementById('upload-zone').style.display = 'none';
|
||||
document.getElementById('upload-progress').style.display = 'block';
|
||||
|
||||
const progressList = document.getElementById('progress-list');
|
||||
progressList.innerHTML = '';
|
||||
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const file = files[i];
|
||||
const progressItem = createProgressItem(file.name, i);
|
||||
progressList.appendChild(progressItem);
|
||||
|
||||
try {
|
||||
await uploadFile(file, i);
|
||||
} catch (error) {
|
||||
updateProgress(i, 100, 'error', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
document.getElementById('upload-progress').style.display = 'none';
|
||||
loadFiles();
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
function createProgressItem(filename, index) {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'progress-item';
|
||||
div.innerHTML = `
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<span class="filename">${filename}</span>
|
||||
<span class="status" id="status-${index}">Загрузка...</span>
|
||||
</div>
|
||||
<div class="progress">
|
||||
<div class="progress-bar" id="progress-${index}" role="progressbar" style="width: 0%"></div>
|
||||
</div>
|
||||
`;
|
||||
return div;
|
||||
}
|
||||
|
||||
function updateProgress(index, percent, status, message) {
|
||||
const progressBar = document.getElementById(`progress-${index}`);
|
||||
const statusSpan = document.getElementById(`status-${index}`);
|
||||
|
||||
if (progressBar) {
|
||||
progressBar.style.width = percent + '%';
|
||||
|
||||
if (status === 'success') {
|
||||
progressBar.classList.add('bg-success');
|
||||
statusSpan.textContent = 'Готово';
|
||||
statusSpan.classList.add('text-success');
|
||||
} else if (status === 'error') {
|
||||
progressBar.classList.add('bg-danger');
|
||||
statusSpan.textContent = 'Ошибка: ' + message;
|
||||
statusSpan.classList.add('text-danger');
|
||||
} else {
|
||||
statusSpan.textContent = Math.round(percent) + '%';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function uploadFile(file, index) {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const xhr = new XMLHttpRequest();
|
||||
|
||||
xhr.upload.addEventListener('progress', function(e) {
|
||||
if (e.lengthComputable) {
|
||||
const percent = (e.loaded / e.total) * 100;
|
||||
updateProgress(index, percent, 'uploading');
|
||||
}
|
||||
});
|
||||
|
||||
xhr.addEventListener('load', function() {
|
||||
if (xhr.status === 200) {
|
||||
const response = JSON.parse(xhr.responseText);
|
||||
if (response.success) {
|
||||
updateProgress(index, 100, 'success');
|
||||
resolve(response);
|
||||
} else {
|
||||
reject(new Error(response.message));
|
||||
}
|
||||
} else {
|
||||
reject(new Error('HTTP ' + xhr.status));
|
||||
}
|
||||
});
|
||||
|
||||
xhr.addEventListener('error', function() {
|
||||
reject(new Error('Network error'));
|
||||
});
|
||||
|
||||
xhr.open('POST', '/api/admin/media/upload');
|
||||
xhr.send(formData);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<script>
|
||||
class MediaGallery {
|
||||
|
||||
@@ -1,358 +1,362 @@
|
||||
<!-- 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>
|
||||
<!-- Content Header (Page header) -->
|
||||
<div class="content-header">
|
||||
<div class="container-fluid">
|
||||
<div class="row mb-2">
|
||||
<div class="col-sm-6">
|
||||
<h1><i class="fas fa-briefcase mr-2"></i>Управление портфолио</h1>
|
||||
</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 class="col-sm-6">
|
||||
<ol class="breadcrumb float-sm-right">
|
||||
<li class="breadcrumb-item"><a href="/admin/dashboard">Админ</a></li>
|
||||
<li class="breadcrumb-item active">Портфолио</li>
|
||||
</ol>
|
||||
</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>
|
||||
|
||||
<!-- Main content -->
|
||||
<section class="content">
|
||||
<div class="container-fluid">
|
||||
<!-- Search and Filter Bar -->
|
||||
<div class="row mb-3">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<div class="input-group">
|
||||
<input type="text" id="searchInput" class="form-control" placeholder="Поиск проектов...">
|
||||
<div class="input-group-append">
|
||||
<button type="button" class="btn btn-outline-secondary" onclick="searchProjects()">
|
||||
<i class="fas fa-search"></i>
|
||||
</button>
|
||||
</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 class="col-md-3">
|
||||
<select id="categoryFilter" class="form-control" onchange="filterByCategory()">
|
||||
<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>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<select id="statusFilter" class="form-control" onchange="filterByStatus()">
|
||||
<option value="">Все статусы</option>
|
||||
<option value="published">Опубликовано</option>
|
||||
<option value="draft">Черновик</option>
|
||||
<option value="featured">Рекомендуемое</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3 text-right">
|
||||
<a href="/admin/portfolio/add" class="btn btn-primary">
|
||||
<i class="fas fa-plus mr-1"></i> Добавить проект
|
||||
</a>
|
||||
</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>
|
||||
<% } %>
|
||||
</div>
|
||||
|
||||
<!-- Portfolio Table -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Список проектов</h3>
|
||||
<div class="card-tools">
|
||||
<span class="badge badge-secondary" id="projectCount">
|
||||
Всего: <%= portfolio ? portfolio.length : 0 %>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body table-responsive p-0">
|
||||
<table class="table table-hover text-nowrap">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 60px;">Фото</th>
|
||||
<th>Название</th>
|
||||
<th>Категория</th>
|
||||
<th>Статус</th>
|
||||
<th>Технологии</th>
|
||||
<th>Создано</th>
|
||||
<th style="width: 150px;">Действия</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% if (portfolio && portfolio.length > 0) { %>
|
||||
<% portfolio.forEach(item => { %>
|
||||
<tr class="portfolio-item" data-category="<%= item.category %>" data-title="<%= item.title.toLowerCase() %>" data-status="<%= item.isPublished ? 'published' : 'draft' %><%= item.featured ? ' featured' : '' %>">
|
||||
<td>
|
||||
<% if (item.images && item.images.length > 0) { %>
|
||||
<img src="<%= item.images[0].url %>" alt="<%= item.title %>" class="img-circle img-size-32">
|
||||
<% } else { %>
|
||||
<div class="img-circle bg-secondary d-flex align-items-center justify-content-center" style="width: 32px; height: 32px;">
|
||||
<i class="fas fa-image text-white"></i>
|
||||
</div>
|
||||
<% } %>
|
||||
</td>
|
||||
<td>
|
||||
<div>
|
||||
<strong><%= item.title %></strong>
|
||||
<% if (item.featured) { %>
|
||||
<span class="badge badge-warning ml-1">
|
||||
<i class="fas fa-star"></i>
|
||||
</span>
|
||||
<% } %>
|
||||
</div>
|
||||
<small class="text-muted"><%= item.shortDescription || 'Описание не указано' %></small>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge badge-info">
|
||||
<%= item.category.replace('-', ' ') %>
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<% if (item.isPublished) { %>
|
||||
<span class="badge badge-success">
|
||||
<i class="fas fa-check-circle mr-1"></i>Опубликовано
|
||||
</span>
|
||||
<% } else { %>
|
||||
<span class="badge badge-secondary">
|
||||
<i class="fas fa-clock mr-1"></i>Черновик
|
||||
</span>
|
||||
<% } %>
|
||||
</td>
|
||||
<td>
|
||||
<% if (item.technologies && item.technologies.length > 0) { %>
|
||||
<% item.technologies.slice(0, 2).forEach(tech => { %>
|
||||
<span class="badge badge-light mr-1"><%= tech %></span>
|
||||
<% }) %>
|
||||
<% if (item.technologies.length > 2) { %>
|
||||
<span class="text-muted">+<%= item.technologies.length - 2 %></span>
|
||||
<% } %>
|
||||
<% } else { %>
|
||||
<span class="text-muted">—</span>
|
||||
<% } %>
|
||||
</td>
|
||||
<td>
|
||||
<small class="text-muted">
|
||||
<%= new Date(item.createdAt).toLocaleDateString('ru-RU') %>
|
||||
</small>
|
||||
</td>
|
||||
<td>
|
||||
<div class="btn-group btn-group-sm" role="group">
|
||||
<% if (item.isPublished) { %>
|
||||
<a href="/portfolio/<%= item.id %>" target="_blank" class="btn btn-info btn-sm" title="Просмотр">
|
||||
<i class="fas fa-external-link-alt"></i>
|
||||
</a>
|
||||
<% } %>
|
||||
<button type="button" class="btn btn-<%= item.isPublished ? 'warning' : 'success' %> btn-sm"
|
||||
onclick="togglePublish('<%= item.id %>', '<%= item.isPublished %>')"
|
||||
title="<%= item.isPublished ? 'Скрыть' : 'Опубликовать' %>">
|
||||
<i class="fas <%= item.isPublished ? 'fa-eye-slash' : 'fa-eye' %>"></i>
|
||||
</button>
|
||||
<a href="/admin/portfolio/edit/<%= item.id %>" class="btn btn-primary btn-sm" title="Редактировать">
|
||||
<i class="fas fa-edit"></i>
|
||||
</a>
|
||||
<button type="button" class="btn btn-danger btn-sm"
|
||||
onclick="deletePortfolio('<%= item.id %>', '<%= item.title %>')"
|
||||
title="Удалить">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<% }) %>
|
||||
<% } else { %>
|
||||
<tr>
|
||||
<td colspan="7" class="text-center py-4">
|
||||
<div class="text-muted">
|
||||
<i class="fas fa-briefcase fa-3x mb-3"></i>
|
||||
<p>Проекты не найдены</p>
|
||||
<a href="/admin/portfolio/add" class="btn btn-primary">
|
||||
Добавить первый проект
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<% } %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!-- Pagination -->
|
||||
<% if (pagination && pagination.total > 1) { %>
|
||||
<div class="card-footer clearfix">
|
||||
<ul class="pagination pagination-sm m-0 float-right">
|
||||
<% if (pagination.hasPrev) { %>
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page=<%= pagination.current - 1 %>">«</a>
|
||||
</li>
|
||||
<% } %>
|
||||
|
||||
<% for (let i = 1; i <= pagination.total; i++) { %>
|
||||
<li class="page-item <%= pagination.current === i ? 'active' : '' %>">
|
||||
<a class="page-link" href="?page=<%= i %>"><%= i %></a>
|
||||
</li>
|
||||
<% } %>
|
||||
|
||||
<% if (pagination.hasNext) { %>
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page=<%= pagination.current + 1 %>">»</a>
|
||||
</li>
|
||||
<% } %>
|
||||
</ul>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<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;
|
||||
});
|
||||
}
|
||||
Swal.fire({
|
||||
title: 'Удалить проект?',
|
||||
text: `Вы уверены, что хотите удалить проект "${title}"?`,
|
||||
icon: 'warning',
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: '#d33',
|
||||
cancelButtonColor: '#3085d6',
|
||||
confirmButtonText: 'Да, удалить!',
|
||||
cancelButtonText: 'Отмена'
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
fetch(`/api/admin/portfolio/${id}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
Swal.fire('Удалено!', 'Проект был удален.', 'success').then(() => {
|
||||
location.reload();
|
||||
});
|
||||
} else {
|
||||
Swal.fire('Ошибка!', data.message || 'Ошибка при удалении проекта', 'error');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
Swal.fire('Ошибка!', 'Произошла ошибка при удалении проекта', 'error');
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
const isPublished = currentStatus === 'true';
|
||||
|
||||
fetch(`/admin/portfolio/${id}/toggle-publish`, {
|
||||
fetch(`/api/admin/portfolio/${id}`, {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
}
|
||||
},
|
||||
body: JSON.stringify({
|
||||
isPublished: !isPublished
|
||||
})
|
||||
})
|
||||
.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;
|
||||
toastr.success(`Проект ${!isPublished ? 'опубликован' : 'скрыт'}`);
|
||||
setTimeout(() => {
|
||||
location.reload();
|
||||
}, 1000);
|
||||
} else {
|
||||
showNotification(data.message || 'Ошибка при изменении статуса', 'error');
|
||||
button.innerHTML = originalContent;
|
||||
button.disabled = false;
|
||||
toastr.error(data.message || 'Ошибка при изменении статуса');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
showNotification('Ошибка при изменении статуса', 'error');
|
||||
button.innerHTML = originalContent;
|
||||
button.disabled = false;
|
||||
toastr.error('Произошла ошибка при изменении статуса');
|
||||
});
|
||||
}
|
||||
|
||||
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');
|
||||
let visibleCount = 0;
|
||||
|
||||
items.forEach(item => {
|
||||
const title = item.dataset.title;
|
||||
if (title.includes(searchTerm)) {
|
||||
item.style.display = 'block';
|
||||
item.style.display = 'table-row';
|
||||
visibleCount++;
|
||||
} else {
|
||||
item.style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
updateProjectCount();
|
||||
updateProjectCount(visibleCount, items.length);
|
||||
}
|
||||
|
||||
function filterByCategory() {
|
||||
const selectedCategory = document.getElementById('categoryFilter').value;
|
||||
const selectedStatus = document.getElementById('statusFilter').value;
|
||||
filterPortfolio(selectedCategory, selectedStatus);
|
||||
}
|
||||
|
||||
function filterByStatus() {
|
||||
const selectedCategory = document.getElementById('categoryFilter').value;
|
||||
const selectedStatus = document.getElementById('statusFilter').value;
|
||||
filterPortfolio(selectedCategory, selectedStatus);
|
||||
}
|
||||
|
||||
function filterPortfolio(category, status) {
|
||||
const items = document.querySelectorAll('.portfolio-item');
|
||||
let visibleCount = 0;
|
||||
|
||||
items.forEach(item => {
|
||||
const category = item.dataset.category;
|
||||
if (!selectedCategory || category === selectedCategory) {
|
||||
item.style.display = 'block';
|
||||
const itemCategory = item.dataset.category;
|
||||
const itemStatus = item.dataset.status;
|
||||
|
||||
let showItem = true;
|
||||
|
||||
if (category && itemCategory !== category) {
|
||||
showItem = false;
|
||||
}
|
||||
|
||||
if (status && !itemStatus.includes(status)) {
|
||||
showItem = false;
|
||||
}
|
||||
|
||||
if (showItem) {
|
||||
item.style.display = 'table-row';
|
||||
visibleCount++;
|
||||
} else {
|
||||
item.style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
updateProjectCount();
|
||||
updateProjectCount(visibleCount, items.length);
|
||||
}
|
||||
|
||||
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');
|
||||
function updateProjectCount(visible, total) {
|
||||
const countElement = document.getElementById('projectCount');
|
||||
if (countElement) {
|
||||
countElement.textContent = `Показано проектов: ${visibleItems} из ${totalItems}`;
|
||||
if (visible !== undefined) {
|
||||
countElement.textContent = `Показано: ${visible} из ${total}`;
|
||||
} else {
|
||||
countElement.textContent = `Всего: ${total}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
// Search on Enter key
|
||||
document.getElementById('searchInput').addEventListener('keyup', function(event) {
|
||||
if (event.key === 'Enter') {
|
||||
searchProjects();
|
||||
}
|
||||
});
|
||||
|
||||
// Инициализация
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
updateProjectCount();
|
||||
// Auto search on input
|
||||
document.getElementById('searchInput').addEventListener('input', function() {
|
||||
searchProjects();
|
||||
});
|
||||
</script>
|
||||
344
views/admin/services/add.ejs
Normal file
344
views/admin/services/add.ejs
Normal file
@@ -0,0 +1,344 @@
|
||||
<!-- Content Header (Page header) -->
|
||||
<div class="content-header">
|
||||
<div class="container-fluid">
|
||||
<div class="row mb-2">
|
||||
<div class="col-sm-6">
|
||||
<h1><i class="fas fa-plus mr-2"></i>Добавить услугу</h1>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<ol class="breadcrumb float-sm-right">
|
||||
<li class="breadcrumb-item"><a href="/admin/dashboard">Админ</a></li>
|
||||
<li class="breadcrumb-item"><a href="/admin/services">Услуги</a></li>
|
||||
<li class="breadcrumb-item active">Добавить</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main content -->
|
||||
<section class="content">
|
||||
<div class="container-fluid">
|
||||
<form id="serviceForm" action="/api/admin/services" method="POST">
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<!-- Basic Information -->
|
||||
<div class="card card-primary">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Основная информация</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="form-group">
|
||||
<label for="name">Название услуги *</label>
|
||||
<input type="text" class="form-control" id="name" name="name" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="shortDescription">Краткое описание</label>
|
||||
<textarea class="form-control" id="shortDescription" name="shortDescription" rows="2" placeholder="Краткое описание для списков и карточек"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="description">Полное описание</label>
|
||||
<textarea class="form-control" id="description" name="description" rows="6" placeholder="Детальное описание услуги"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="category">Категория *</label>
|
||||
<select class="form-control" id="category" name="category" required>
|
||||
<option value="">Выберите категорию</option>
|
||||
<option value="web-development">Веб-разработка</option>
|
||||
<option value="mobile-development">Мобильная разработка</option>
|
||||
<option value="ui-ux-design">UI/UX Дизайн</option>
|
||||
<option value="consulting">Консалтинг</option>
|
||||
<option value="support">Поддержка</option>
|
||||
<option value="other">Другое</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="icon">Иконка</label>
|
||||
<input type="text" class="form-control" id="icon" name="icon" placeholder="fas fa-code" value="fas fa-cog">
|
||||
<small class="form-text text-muted">FontAwesome класс иконки</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="estimatedTime">Время выполнения</label>
|
||||
<input type="text" class="form-control" id="estimatedTime" name="estimatedTime" placeholder="2-4 недели">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pricing -->
|
||||
<div class="card card-info">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Ценообразование</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="basePrice">Базовая цена ($)</label>
|
||||
<input type="number" class="form-control" id="basePrice" name="pricing[basePrice]" min="0" step="0.01">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="currency">Валюта</label>
|
||||
<select class="form-control" id="currency" name="pricing[currency]">
|
||||
<option value="USD">USD ($)</option>
|
||||
<option value="EUR">EUR (€)</option>
|
||||
<option value="KRW">KRW (₩)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="pricingType">Тип ценообразования</label>
|
||||
<select class="form-control" id="pricingType" name="pricing[type]">
|
||||
<option value="fixed">Фиксированная цена</option>
|
||||
<option value="hourly">Почасовая оплата</option>
|
||||
<option value="project">За проект</option>
|
||||
<option value="subscription">Подписка</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Features -->
|
||||
<div class="card card-success">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Функции и возможности</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="featuresContainer">
|
||||
<div class="feature-item mb-3">
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<input type="text" class="form-control" name="features[0][name]" placeholder="Название функции">
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<select class="form-control" name="features[0][included]">
|
||||
<option value="true">Включено</option>
|
||||
<option value="false">Не включено</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-1">
|
||||
<button type="button" class="btn btn-danger btn-sm remove-feature">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="btn btn-sm btn-outline-primary" id="addFeature">
|
||||
<i class="fas fa-plus mr-1"></i> Добавить функцию
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<!-- Status & Settings -->
|
||||
<div class="card card-warning">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Настройки</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="form-group">
|
||||
<div class="custom-control custom-switch">
|
||||
<input type="checkbox" class="custom-control-input" id="isActive" name="isActive" checked>
|
||||
<label class="custom-control-label" for="isActive">Активная услуга</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="custom-control custom-switch">
|
||||
<input type="checkbox" class="custom-control-input" id="featured" name="featured">
|
||||
<label class="custom-control-label" for="featured">Рекомендуемая услуга</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="order">Порядок отображения</label>
|
||||
<input type="number" class="form-control" id="order" name="order" value="0" min="0">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tags -->
|
||||
<div class="card card-secondary">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Теги</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="form-group">
|
||||
<label for="tags">Теги (через запятую)</label>
|
||||
<input type="text" class="form-control" id="tags" name="tags" placeholder="веб-дизайн, frontend, react">
|
||||
<small class="form-text text-muted">Разделите теги запятыми</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SEO -->
|
||||
<div class="card card-dark">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">SEO настройки</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="form-group">
|
||||
<label for="seoTitle">SEO заголовок</label>
|
||||
<input type="text" class="form-control" id="seoTitle" name="seo[title]">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="seoDescription">SEO описание</label>
|
||||
<textarea class="form-control" id="seoDescription" name="seo[description]" rows="3"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="seoKeywords">Ключевые слова</label>
|
||||
<input type="text" class="form-control" id="seoKeywords" name="seo[keywords]" placeholder="через, запятую">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Submit Buttons -->
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<button type="submit" class="btn btn-success">
|
||||
<i class="fas fa-save mr-1"></i> Сохранить услугу
|
||||
</button>
|
||||
<a href="/admin/services" class="btn btn-secondary ml-2">
|
||||
<i class="fas fa-times mr-1"></i> Отмена
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
let featureIndex = 1;
|
||||
|
||||
// Add feature
|
||||
$('#addFeature').click(function() {
|
||||
const featureHtml = `
|
||||
<div class="feature-item mb-3">
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<input type="text" class="form-control" name="features[${featureIndex}][name]" placeholder="Название функции">
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<select class="form-control" name="features[${featureIndex}][included]">
|
||||
<option value="true">Включено</option>
|
||||
<option value="false">Не включено</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-1">
|
||||
<button type="button" class="btn btn-danger btn-sm remove-feature">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
$('#featuresContainer').append(featureHtml);
|
||||
featureIndex++;
|
||||
});
|
||||
|
||||
// Remove feature
|
||||
$(document).on('click', '.remove-feature', function() {
|
||||
$(this).closest('.feature-item').remove();
|
||||
});
|
||||
|
||||
// Form submission
|
||||
$('#serviceForm').submit(function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const formData = new FormData(this);
|
||||
const data = {};
|
||||
|
||||
// Convert FormData to object
|
||||
for (let [key, value] of formData.entries()) {
|
||||
if (key.includes('[') && key.includes(']')) {
|
||||
// Handle nested objects (pricing, seo, features)
|
||||
const matches = key.match(/(\w+)\[(\w+|\d+)\](?:\[(\w+)\])?/);
|
||||
if (matches) {
|
||||
const [, parent, child, grandchild] = matches;
|
||||
if (!data[parent]) data[parent] = {};
|
||||
|
||||
if (grandchild) {
|
||||
// features array handling
|
||||
if (!data[parent][child]) data[parent][child] = {};
|
||||
data[parent][child][grandchild] = value;
|
||||
} else {
|
||||
data[parent][child] = value;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
data[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
// Convert features object to array
|
||||
if (data.features) {
|
||||
const featuresArray = [];
|
||||
Object.keys(data.features).forEach(index => {
|
||||
if (data.features[index].name) {
|
||||
featuresArray.push({
|
||||
name: data.features[index].name,
|
||||
included: data.features[index].included === 'true'
|
||||
});
|
||||
}
|
||||
});
|
||||
data.features = featuresArray;
|
||||
}
|
||||
|
||||
// Convert checkboxes
|
||||
data.isActive = $('#isActive').is(':checked');
|
||||
data.featured = $('#featured').is(':checked');
|
||||
|
||||
// Convert tags to array
|
||||
if (data.tags) {
|
||||
data.tags = data.tags.split(',').map(tag => tag.trim()).filter(tag => tag);
|
||||
}
|
||||
|
||||
fetch('/api/admin/services', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(result => {
|
||||
if (result.success) {
|
||||
toastr.success('Услуга успешно создана!');
|
||||
setTimeout(() => {
|
||||
window.location.href = '/admin/services';
|
||||
}, 1500);
|
||||
} else {
|
||||
toastr.error(result.message || 'Ошибка при создании услуги');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
toastr.error('Произошла ошибка при создании услуги');
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
411
views/admin/services/edit.ejs
Normal file
411
views/admin/services/edit.ejs
Normal file
@@ -0,0 +1,411 @@
|
||||
<!-- Content Header (Page header) -->
|
||||
<div class="content-header">
|
||||
<div class="container-fluid">
|
||||
<div class="row mb-2">
|
||||
<div class="col-sm-6">
|
||||
<h1><i class="fas fa-edit mr-2"></i>Редактировать услугу</h1>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<ol class="breadcrumb float-sm-right">
|
||||
<li class="breadcrumb-item"><a href="/admin/dashboard">Админ</a></li>
|
||||
<li class="breadcrumb-item"><a href="/admin/services">Услуги</a></li>
|
||||
<li class="breadcrumb-item active">Редактировать</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main content -->
|
||||
<section class="content">
|
||||
<div class="container-fluid">
|
||||
<form id="serviceForm">
|
||||
<input type="hidden" id="serviceId" value="<%= service.id %>">
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<!-- Basic Information -->
|
||||
<div class="card card-primary">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Основная информация</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="form-group">
|
||||
<label for="name">Название услуги *</label>
|
||||
<input type="text" class="form-control" id="name" name="name" value="<%= service.name %>" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="shortDescription">Краткое описание</label>
|
||||
<textarea class="form-control" id="shortDescription" name="shortDescription" rows="2"><%= service.shortDescription || '' %></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="description">Полное описание</label>
|
||||
<textarea class="form-control" id="description" name="description" rows="6"><%= service.description || '' %></textarea>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="category">Категория *</label>
|
||||
<select class="form-control" id="category" name="category" required>
|
||||
<option value="">Выберите категорию</option>
|
||||
<option value="web-development" <%= service.category === 'web-development' ? 'selected' : '' %>>Веб-разработка</option>
|
||||
<option value="mobile-development" <%= service.category === 'mobile-development' ? 'selected' : '' %>>Мобильная разработка</option>
|
||||
<option value="ui-ux-design" <%= service.category === 'ui-ux-design' ? 'selected' : '' %>>UI/UX Дизайн</option>
|
||||
<option value="consulting" <%= service.category === 'consulting' ? 'selected' : '' %>>Консалтинг</option>
|
||||
<option value="support" <%= service.category === 'support' ? 'selected' : '' %>>Поддержка</option>
|
||||
<option value="other" <%= service.category === 'other' ? 'selected' : '' %>>Другое</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="icon">Иконка</label>
|
||||
<input type="text" class="form-control" id="icon" name="icon" value="<%= service.icon || 'fas fa-cog' %>">
|
||||
<small class="form-text text-muted">FontAwesome класс иконки</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="estimatedTime">Время выполнения</label>
|
||||
<input type="text" class="form-control" id="estimatedTime" name="estimatedTime" value="<%= service.estimatedTime || '' %>">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pricing -->
|
||||
<div class="card card-info">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Ценообразование</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="basePrice">Базовая цена ($)</label>
|
||||
<input type="number" class="form-control" id="basePrice" name="pricing[basePrice]" value="<%= service.pricing?.basePrice || '' %>" min="0" step="0.01">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="currency">Валюта</label>
|
||||
<select class="form-control" id="currency" name="pricing[currency]">
|
||||
<option value="USD" <%= service.pricing?.currency === 'USD' ? 'selected' : '' %>>USD ($)</option>
|
||||
<option value="EUR" <%= service.pricing?.currency === 'EUR' ? 'selected' : '' %>>EUR (€)</option>
|
||||
<option value="KRW" <%= service.pricing?.currency === 'KRW' ? 'selected' : '' %>>KRW (₩)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="pricingType">Тип ценообразования</label>
|
||||
<select class="form-control" id="pricingType" name="pricing[type]">
|
||||
<option value="fixed" <%= service.pricing?.type === 'fixed' ? 'selected' : '' %>>Фиксированная цена</option>
|
||||
<option value="hourly" <%= service.pricing?.type === 'hourly' ? 'selected' : '' %>>Почасовая оплата</option>
|
||||
<option value="project" <%= service.pricing?.type === 'project' ? 'selected' : '' %>>За проект</option>
|
||||
<option value="subscription" <%= service.pricing?.type === 'subscription' ? 'selected' : '' %>>Подписка</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Features -->
|
||||
<div class="card card-success">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Функции и возможности</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="featuresContainer">
|
||||
<% if (service.features && service.features.length > 0) { %>
|
||||
<% service.features.forEach((feature, index) => { %>
|
||||
<div class="feature-item mb-3">
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<input type="text" class="form-control" name="features[<%= index %>][name]" value="<%= feature.name %>">
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<select class="form-control" name="features[<%= index %>][included]">
|
||||
<option value="true" <%= feature.included ? 'selected' : '' %>>Включено</option>
|
||||
<option value="false" <%= !feature.included ? 'selected' : '' %>>Не включено</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-1">
|
||||
<button type="button" class="btn btn-danger btn-sm remove-feature">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% }) %>
|
||||
<% } else { %>
|
||||
<div class="feature-item mb-3">
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<input type="text" class="form-control" name="features[0][name]" placeholder="Название функции">
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<select class="form-control" name="features[0][included]">
|
||||
<option value="true">Включено</option>
|
||||
<option value="false">Не включено</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-1">
|
||||
<button type="button" class="btn btn-danger btn-sm remove-feature">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
<button type="button" class="btn btn-sm btn-outline-primary" id="addFeature">
|
||||
<i class="fas fa-plus mr-1"></i> Добавить функцию
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<!-- Status & Settings -->
|
||||
<div class="card card-warning">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Настройки</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="form-group">
|
||||
<div class="custom-control custom-switch">
|
||||
<input type="checkbox" class="custom-control-input" id="isActive" name="isActive" <%= service.isActive ? 'checked' : '' %>>
|
||||
<label class="custom-control-label" for="isActive">Активная услуга</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="custom-control custom-switch">
|
||||
<input type="checkbox" class="custom-control-input" id="featured" name="featured" <%= service.featured ? 'checked' : '' %>>
|
||||
<label class="custom-control-label" for="featured">Рекомендуемая услуга</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="order">Порядок отображения</label>
|
||||
<input type="number" class="form-control" id="order" name="order" value="<%= service.order || 0 %>" min="0">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tags -->
|
||||
<div class="card card-secondary">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Теги</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="form-group">
|
||||
<label for="tags">Теги (через запятую)</label>
|
||||
<input type="text" class="form-control" id="tags" name="tags" value="<%= service.tags ? service.tags.join(', ') : '' %>">
|
||||
<small class="form-text text-muted">Разделите теги запятыми</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SEO -->
|
||||
<div class="card card-dark">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">SEO настройки</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="form-group">
|
||||
<label for="seoTitle">SEO заголовок</label>
|
||||
<input type="text" class="form-control" id="seoTitle" name="seo[title]" value="<%= service.seo?.title || '' %>">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="seoDescription">SEO описание</label>
|
||||
<textarea class="form-control" id="seoDescription" name="seo[description]" rows="3"><%= service.seo?.description || '' %></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="seoKeywords">Ключевые слова</label>
|
||||
<input type="text" class="form-control" id="seoKeywords" name="seo[keywords]" value="<%= service.seo?.keywords || '' %>">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Submit Buttons -->
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<button type="submit" class="btn btn-success">
|
||||
<i class="fas fa-save mr-1"></i> Сохранить изменения
|
||||
</button>
|
||||
<a href="/admin/services" class="btn btn-secondary ml-2">
|
||||
<i class="fas fa-times mr-1"></i> Отмена
|
||||
</a>
|
||||
<button type="button" class="btn btn-danger ml-2" onclick="deleteService()">
|
||||
<i class="fas fa-trash mr-1"></i> Удалить услугу
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
const service = <%- JSON.stringify(service) %>;
|
||||
let featureIndex = service.features ? service.features.length : 1;
|
||||
|
||||
// Add feature
|
||||
$('#addFeature').click(function() {
|
||||
const featureHtml = `
|
||||
<div class="feature-item mb-3">
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<input type="text" class="form-control" name="features[${featureIndex}][name]" placeholder="Название функции">
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<select class="form-control" name="features[${featureIndex}][included]">
|
||||
<option value="true">Включено</option>
|
||||
<option value="false">Не включено</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-1">
|
||||
<button type="button" class="btn btn-danger btn-sm remove-feature">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
$('#featuresContainer').append(featureHtml);
|
||||
featureIndex++;
|
||||
});
|
||||
|
||||
// Remove feature
|
||||
$(document).on('click', '.remove-feature', function() {
|
||||
$(this).closest('.feature-item').remove();
|
||||
});
|
||||
|
||||
// Form submission
|
||||
$('#serviceForm').submit(function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const serviceId = $('#serviceId').val();
|
||||
const formData = new FormData(this);
|
||||
const data = {};
|
||||
|
||||
// Convert FormData to object
|
||||
for (let [key, value] of formData.entries()) {
|
||||
if (key.includes('[') && key.includes(']')) {
|
||||
// Handle nested objects (pricing, seo, features)
|
||||
const matches = key.match(/(\w+)\[(\w+|\d+)\](?:\[(\w+)\])?/);
|
||||
if (matches) {
|
||||
const [, parent, child, grandchild] = matches;
|
||||
if (!data[parent]) data[parent] = {};
|
||||
|
||||
if (grandchild) {
|
||||
// features array handling
|
||||
if (!data[parent][child]) data[parent][child] = {};
|
||||
data[parent][child][grandchild] = value;
|
||||
} else {
|
||||
data[parent][child] = value;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
data[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
// Convert features object to array
|
||||
if (data.features) {
|
||||
const featuresArray = [];
|
||||
Object.keys(data.features).forEach(index => {
|
||||
if (data.features[index].name) {
|
||||
featuresArray.push({
|
||||
name: data.features[index].name,
|
||||
included: data.features[index].included === 'true'
|
||||
});
|
||||
}
|
||||
});
|
||||
data.features = featuresArray;
|
||||
}
|
||||
|
||||
// Convert checkboxes
|
||||
data.isActive = $('#isActive').is(':checked');
|
||||
data.featured = $('#featured').is(':checked');
|
||||
|
||||
// Convert tags to array
|
||||
if (data.tags) {
|
||||
data.tags = data.tags.split(',').map(tag => tag.trim()).filter(tag => tag);
|
||||
}
|
||||
|
||||
fetch(`/api/admin/services/${serviceId}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(result => {
|
||||
if (result.success) {
|
||||
toastr.success('Услуга успешно обновлена!');
|
||||
setTimeout(() => {
|
||||
window.location.href = '/admin/services';
|
||||
}, 1500);
|
||||
} else {
|
||||
toastr.error(result.message || 'Ошибка при обновлении услуги');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
toastr.error('Произошла ошибка при обновлении услуги');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function deleteService() {
|
||||
const serviceId = $('#serviceId').val();
|
||||
|
||||
Swal.fire({
|
||||
title: 'Удалить услугу?',
|
||||
text: 'Это действие невозможно отменить!',
|
||||
icon: 'warning',
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: '#d33',
|
||||
cancelButtonColor: '#3085d6',
|
||||
confirmButtonText: 'Да, удалить!',
|
||||
cancelButtonText: 'Отмена'
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
fetch(`/api/admin/services/${serviceId}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
Swal.fire('Удалено!', 'Услуга была удалена.', 'success').then(() => {
|
||||
window.location.href = '/admin/services';
|
||||
});
|
||||
} else {
|
||||
Swal.fire('Ошибка!', data.message || 'Ошибка при удалении услуги', 'error');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
Swal.fire('Ошибка!', 'Произошла ошибка при удалении услуги', 'error');
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
@@ -1,121 +1,206 @@
|
||||
<!-- 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">
|
||||
Предыдущая
|
||||
<!-- Content Header (Page header) -->
|
||||
<div class="content-header">
|
||||
<div class="container-fluid">
|
||||
<div class="row mb-2">
|
||||
<div class="col-sm-6">
|
||||
<h1><i class="fas fa-cogs mr-2"></i>Управление услугами</h1>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<div class="float-sm-right">
|
||||
<a href="/admin/services/add" class="btn btn-primary">
|
||||
<i class="fas fa-plus mr-1"></i>
|
||||
Добавить услугу
|
||||
</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>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main content -->
|
||||
<section class="content">
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Список услуг</h3>
|
||||
<div class="card-tools">
|
||||
<div class="input-group input-group-sm" style="width: 150px;">
|
||||
<input type="text" name="table_search" class="form-control float-right" placeholder="Поиск">
|
||||
<div class="input-group-append">
|
||||
<button type="submit" class="btn btn-default">
|
||||
<i class="fas fa-search"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body table-responsive p-0">
|
||||
<% if (services && services.length > 0) { %>
|
||||
<table class="table table-hover text-nowrap">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Название</th>
|
||||
<th>Категория</th>
|
||||
<th>Статус</th>
|
||||
<th>Цена от</th>
|
||||
<th>Время</th>
|
||||
<th>Рекомендуемая</th>
|
||||
<th>Создано</th>
|
||||
<th>Действия</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% services.forEach(service => { %>
|
||||
<tr>
|
||||
<td>
|
||||
<small class="text-muted">#<%= service.id.slice(-8) %></small>
|
||||
</td>
|
||||
<td>
|
||||
<div class="d-flex align-items-center">
|
||||
<i class="<%= service.icon || 'fas fa-cog' %> mr-2 text-primary"></i>
|
||||
<strong><%= service.name %></strong>
|
||||
</div>
|
||||
<% if (service.shortDescription) { %>
|
||||
<small class="text-muted d-block">
|
||||
<%= service.shortDescription.substring(0, 80) %><%= service.shortDescription.length > 80 ? '...' : '' %>
|
||||
</small>
|
||||
<% } %>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge badge-info"><%= service.category %></span>
|
||||
</td>
|
||||
<td>
|
||||
<% if (service.isActive) { %>
|
||||
<span class="badge badge-success">Активна</span>
|
||||
<% } else { %>
|
||||
<span class="badge badge-secondary">Неактивна</span>
|
||||
<% } %>
|
||||
</td>
|
||||
<td>
|
||||
<% if (service.pricing && service.pricing.basePrice) { %>
|
||||
<span class="text-success font-weight-bold">$<%= service.pricing.basePrice %></span>
|
||||
<% } else { %>
|
||||
<span class="text-muted">Не указана</span>
|
||||
<% } %>
|
||||
</td>
|
||||
<td>
|
||||
<% if (service.estimatedTime) { %>
|
||||
<i class="fas fa-clock mr-1 text-info"></i>
|
||||
<%= service.estimatedTime %>
|
||||
<% } else { %>
|
||||
<span class="text-muted">-</span>
|
||||
<% } %>
|
||||
</td>
|
||||
<td>
|
||||
<% if (service.featured) { %>
|
||||
<i class="fas fa-star text-warning" title="Рекомендуемая услуга"></i>
|
||||
<% } else { %>
|
||||
<i class="far fa-star text-muted"></i>
|
||||
<% } %>
|
||||
</td>
|
||||
<td>
|
||||
<small class="text-muted">
|
||||
<%= new Date(service.createdAt).toLocaleDateString('ru-RU') %>
|
||||
</small>
|
||||
</td>
|
||||
<td>
|
||||
<div class="btn-group btn-group-sm">
|
||||
<a href="/services#<%= service.id %>" target="_blank" class="btn btn-info btn-sm" title="Посмотреть на сайте">
|
||||
<i class="fas fa-eye"></i>
|
||||
</a>
|
||||
<a href="/admin/services/edit/<%= service.id %>" class="btn btn-warning btn-sm" title="Редактировать">
|
||||
<i class="fas fa-edit"></i>
|
||||
</a>
|
||||
<button onclick="deleteService('<%= service.id %>')" class="btn btn-danger btn-sm" title="Удалить">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<% }) %>
|
||||
</tbody>
|
||||
</table>
|
||||
<% } else { %>
|
||||
<div class="p-4 text-center">
|
||||
<i class="fas fa-cogs text-muted" style="font-size: 4rem;"></i>
|
||||
<h4 class="mt-3 text-muted">Услуги не найдены</h4>
|
||||
<p class="text-muted">Добавьте первую услугу для начала работы</p>
|
||||
<a href="/admin/services/add" class="btn btn-primary">
|
||||
<i class="fas fa-plus mr-1"></i>
|
||||
Добавить услугу
|
||||
</a>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<% if (pagination && pagination.total > 1) { %>
|
||||
<div class="card-footer clearfix">
|
||||
<ul class="pagination pagination-sm m-0 float-right">
|
||||
<% if (pagination.hasPrev) { %>
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page=<%= pagination.current - 1 %>">«</a>
|
||||
</li>
|
||||
<% } %>
|
||||
|
||||
<% for (let i = 1; i <= pagination.total; i++) { %>
|
||||
<li class="page-item <%= i === pagination.current ? 'active' : '' %>">
|
||||
<a class="page-link" href="?page=<%= i %>"><%= i %></a>
|
||||
</li>
|
||||
<% } %>
|
||||
|
||||
<% if (pagination.hasNext) { %>
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page=<%= pagination.current + 1 %>">»</a>
|
||||
</li>
|
||||
<% } %>
|
||||
</ul>
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<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('Ошибка при удалении услуги');
|
||||
});
|
||||
}
|
||||
Swal.fire({
|
||||
title: 'Удалить услугу?',
|
||||
text: 'Это действие невозможно отменить!',
|
||||
icon: 'warning',
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: '#d33',
|
||||
cancelButtonColor: '#3085d6',
|
||||
confirmButtonText: 'Да, удалить!',
|
||||
cancelButtonText: 'Отмена'
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
fetch(`/api/admin/services/${id}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
Swal.fire('Удалено!', 'Услуга была удалена.', 'success').then(() => {
|
||||
location.reload();
|
||||
});
|
||||
} else {
|
||||
Swal.fire('Ошибка!', data.message || 'Ошибка при удалении услуги', 'error');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
Swal.fire('Ошибка!', 'Произошла ошибка при удалении услуги', 'error');
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
@@ -1,280 +1,362 @@
|
||||
<!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">
|
||||
<!-- 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>
|
||||
<!-- Content Header (Page header) -->
|
||||
<section class="content-header">
|
||||
<div class="container-fluid">
|
||||
<div class="row mb-2">
|
||||
<div class="col-sm-6">
|
||||
<h1><i class="fas fa-cogs mr-2"></i>Настройки сайта</h1>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<ol class="breadcrumb float-sm-right">
|
||||
<li class="breadcrumb-item"><a href="/admin">Админ</a></li>
|
||||
<li class="breadcrumb-item active">Настройки</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Main content -->
|
||||
<section class="content">
|
||||
<div class="container-fluid">
|
||||
<!-- Site Information Card -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Основная информация</h3>
|
||||
</div>
|
||||
<form id="site-settings-form">
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="site-title">Название сайта</label>
|
||||
<input type="text" class="form-control" id="site-title" name="siteTitle" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="site-tagline">Слоган</label>
|
||||
<input type="text" class="form-control" id="site-tagline" name="siteTagline">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="site-description">Описание сайта</label>
|
||||
<textarea class="form-control" id="site-description" name="siteDescription" rows="3"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="company-name">Название компании</label>
|
||||
<input type="text" class="form-control" id="company-name" name="companyName" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="company-email">Email компании</label>
|
||||
<input type="email" class="form-control" id="company-email" name="companyEmail" required>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="company-phone">Телефон</label>
|
||||
<input type="tel" class="form-control" id="company-phone" name="companyPhone">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="company-address">Адрес</label>
|
||||
<input type="text" class="form-control" id="company-address" name="companyAddress">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fas fa-save mr-1"></i>Сохранить
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- SEO Settings Card -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">SEO настройки</h3>
|
||||
</div>
|
||||
<form id="seo-settings-form">
|
||||
<div class="card-body">
|
||||
<div class="form-group">
|
||||
<label for="meta-keywords">Ключевые слова</label>
|
||||
<input type="text" class="form-control" id="meta-keywords" name="metaKeywords"
|
||||
placeholder="ключевое слово 1, ключевое слово 2, ...">
|
||||
<small class="form-text text-muted">Разделяйте ключевые слова запятыми</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="meta-description">Meta Description</label>
|
||||
<textarea class="form-control" id="meta-description" name="metaDescription"
|
||||
rows="3" maxlength="160"></textarea>
|
||||
<small class="form-text text-muted">Рекомендуемая длина: до 160 символов</small>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="google-analytics">Google Analytics ID</label>
|
||||
<input type="text" class="form-control" id="google-analytics" name="googleAnalytics"
|
||||
placeholder="G-XXXXXXXXXX">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="google-tag-manager">Google Tag Manager ID</label>
|
||||
<input type="text" class="form-control" id="google-tag-manager" name="googleTagManager"
|
||||
placeholder="GTM-XXXXXXX">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fas fa-save mr-1"></i>Сохранить SEO
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Social Media Card -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Социальные сети</h3>
|
||||
</div>
|
||||
<form id="social-settings-form">
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="facebook-url"><i class="fab fa-facebook mr-1"></i>Facebook</label>
|
||||
<input type="url" class="form-control" id="facebook-url" name="facebookUrl"
|
||||
placeholder="https://facebook.com/your-page">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="twitter-url"><i class="fab fa-twitter mr-1"></i>Twitter</label>
|
||||
<input type="url" class="form-control" id="twitter-url" name="twitterUrl"
|
||||
placeholder="https://twitter.com/your-account">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="linkedin-url"><i class="fab fa-linkedin mr-1"></i>LinkedIn</label>
|
||||
<input type="url" class="form-control" id="linkedin-url" name="linkedinUrl"
|
||||
placeholder="https://linkedin.com/company/your-company">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="instagram-url"><i class="fab fa-instagram mr-1"></i>Instagram</label>
|
||||
<input type="url" class="form-control" id="instagram-url" name="instagramUrl"
|
||||
placeholder="https://instagram.com/your-account">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="youtube-url"><i class="fab fa-youtube mr-1"></i>YouTube</label>
|
||||
<input type="url" class="form-control" id="youtube-url" name="youtubeUrl"
|
||||
placeholder="https://youtube.com/channel/your-channel">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label for="github-url"><i class="fab fa-github mr-1"></i>GitHub</label>
|
||||
<input type="url" class="form-control" id="github-url" name="githubUrl"
|
||||
placeholder="https://github.com/your-account">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fas fa-save mr-1"></i>Сохранить соцсети
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Maintenance Mode Card -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Режим обслуживания</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="form-group">
|
||||
<div class="custom-control custom-switch">
|
||||
<input type="checkbox" class="custom-control-input" id="maintenance-mode">
|
||||
<label class="custom-control-label" for="maintenance-mode">
|
||||
Включить режим обслуживания
|
||||
</label>
|
||||
</div>
|
||||
<small class="form-text text-muted">
|
||||
В режиме обслуживания сайт будет недоступен для обычных пользователей
|
||||
</small>
|
||||
</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 class="form-group">
|
||||
<label for="maintenance-message">Сообщение для пользователей</label>
|
||||
<textarea class="form-control" id="maintenance-message" rows="3"
|
||||
placeholder="Сайт временно недоступен из-за технических работ..."></textarea>
|
||||
</div>
|
||||
|
||||
<button type="button" class="btn btn-warning" onclick="toggleMaintenanceMode()">
|
||||
<i class="fas fa-tools mr-1"></i>Применить настройки
|
||||
</button>
|
||||
</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>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<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
|
||||
});
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
loadSettings();
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
alert('Настройки успешно сохранены');
|
||||
location.reload();
|
||||
} else {
|
||||
alert('Ошибка при сохранении настроек: ' + data.message);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
alert('Ошибка при сохранении настроек');
|
||||
}
|
||||
});
|
||||
// Setup form submissions
|
||||
document.getElementById('site-settings-form').addEventListener('submit', saveSiteSettings);
|
||||
document.getElementById('seo-settings-form').addEventListener('submit', saveSeoSettings);
|
||||
document.getElementById('social-settings-form').addEventListener('submit', saveSocialSettings);
|
||||
});
|
||||
|
||||
// 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>`;
|
||||
async function loadSettings() {
|
||||
try {
|
||||
const response = await fetch('/api/admin/settings');
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
const settings = data.settings;
|
||||
|
||||
// Site settings
|
||||
document.getElementById('site-title').value = settings.siteTitle || '';
|
||||
document.getElementById('site-tagline').value = settings.siteTagline || '';
|
||||
document.getElementById('site-description').value = settings.siteDescription || '';
|
||||
document.getElementById('company-name').value = settings.companyName || '';
|
||||
document.getElementById('company-email').value = settings.companyEmail || '';
|
||||
document.getElementById('company-phone').value = settings.companyPhone || '';
|
||||
document.getElementById('company-address').value = settings.companyAddress || '';
|
||||
|
||||
// SEO settings
|
||||
document.getElementById('meta-keywords').value = settings.metaKeywords || '';
|
||||
document.getElementById('meta-description').value = settings.metaDescription || '';
|
||||
document.getElementById('google-analytics').value = settings.googleAnalytics || '';
|
||||
document.getElementById('google-tag-manager').value = settings.googleTagManager || '';
|
||||
|
||||
// Social media
|
||||
document.getElementById('facebook-url').value = settings.facebookUrl || '';
|
||||
document.getElementById('twitter-url').value = settings.twitterUrl || '';
|
||||
document.getElementById('linkedin-url').value = settings.linkedinUrl || '';
|
||||
document.getElementById('instagram-url').value = settings.instagramUrl || '';
|
||||
document.getElementById('youtube-url').value = settings.youtubeUrl || '';
|
||||
document.getElementById('github-url').value = settings.githubUrl || '';
|
||||
|
||||
// Maintenance mode
|
||||
document.getElementById('maintenance-mode').checked = settings.maintenanceMode || false;
|
||||
document.getElementById('maintenance-message').value = settings.maintenanceMessage || '';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading settings:', error);
|
||||
alert('Ошибка загрузки настроек: ' + error.message);
|
||||
}
|
||||
} 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>
|
||||
async function saveSiteSettings(event) {
|
||||
event.preventDefault();
|
||||
|
||||
const formData = new FormData(event.target);
|
||||
const settings = {
|
||||
siteTitle: formData.get('siteTitle'),
|
||||
siteTagline: formData.get('siteTagline'),
|
||||
siteDescription: formData.get('siteDescription'),
|
||||
companyName: formData.get('companyName'),
|
||||
companyEmail: formData.get('companyEmail'),
|
||||
companyPhone: formData.get('companyPhone'),
|
||||
companyAddress: formData.get('companyAddress')
|
||||
};
|
||||
|
||||
await saveSettings(settings, 'Основные настройки сохранены');
|
||||
}
|
||||
|
||||
async function saveSeoSettings(event) {
|
||||
event.preventDefault();
|
||||
|
||||
const formData = new FormData(event.target);
|
||||
const settings = {
|
||||
metaKeywords: formData.get('metaKeywords'),
|
||||
metaDescription: formData.get('metaDescription'),
|
||||
googleAnalytics: formData.get('googleAnalytics'),
|
||||
googleTagManager: formData.get('googleTagManager')
|
||||
};
|
||||
|
||||
await saveSettings(settings, 'SEO настройки сохранены');
|
||||
}
|
||||
|
||||
async function saveSocialSettings(event) {
|
||||
event.preventDefault();
|
||||
|
||||
const formData = new FormData(event.target);
|
||||
const settings = {
|
||||
facebookUrl: formData.get('facebookUrl'),
|
||||
twitterUrl: formData.get('twitterUrl'),
|
||||
linkedinUrl: formData.get('linkedinUrl'),
|
||||
instagramUrl: formData.get('instagramUrl'),
|
||||
youtubeUrl: formData.get('youtubeUrl'),
|
||||
githubUrl: formData.get('githubUrl')
|
||||
};
|
||||
|
||||
await saveSettings(settings, 'Настройки соцсетей сохранены');
|
||||
}
|
||||
|
||||
async function saveSettings(settings, successMessage) {
|
||||
try {
|
||||
const response = await fetch('/api/admin/settings', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(settings)
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
alert(successMessage);
|
||||
} else {
|
||||
throw new Error(data.message);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error saving settings:', error);
|
||||
alert('Ошибка сохранения настроек: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleMaintenanceMode() {
|
||||
const isEnabled = document.getElementById('maintenance-mode').checked;
|
||||
const message = document.getElementById('maintenance-message').value;
|
||||
|
||||
const settings = {
|
||||
maintenanceMode: isEnabled,
|
||||
maintenanceMessage: message
|
||||
};
|
||||
|
||||
await saveSettings(settings,
|
||||
isEnabled ? 'Режим обслуживания включен' : 'Режим обслуживания отключен'
|
||||
);
|
||||
}
|
||||
</script>
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user