Main functions

This commit is contained in:
2025-10-26 14:44:10 +09:00
parent 6ff35e26f4
commit 291fc63a4c
901 changed files with 79783 additions and 201383 deletions

View File

@@ -1,186 +0,0 @@
<!-- Add Portfolio Item -->
<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-plus mr-2"></i>
Добавить проект
</h3>
</div>
<form id="portfolioForm" class="p-6 space-y-6">
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2">
<!-- Title -->
<div class="sm:col-span-2">
<label for="title" class="block text-sm font-medium text-gray-700">Название проекта</label>
<input type="text" name="title" id="title" required
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500">
</div>
<!-- Short Description -->
<div class="sm:col-span-2">
<label for="shortDescription" class="block text-sm font-medium text-gray-700">Краткое описание</label>
<input type="text" name="shortDescription" id="shortDescription"
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500">
</div>
<!-- Category -->
<div>
<label for="category" class="block text-sm font-medium text-gray-700">Категория</label>
<select name="category" id="category" required
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500">
<option value="">Выберите категорию</option>
<option value="web-development">Веб-разработка</option>
<option value="mobile-app">Мобильные приложения</option>
<option value="ui-ux-design">UI/UX дизайн</option>
<option value="branding">Брендинг</option>
<option value="marketing">Цифровой маркетинг</option>
</select>
</div>
<!-- Client Name -->
<div>
<label for="clientName" class="block text-sm font-medium text-gray-700">Клиент</label>
<input type="text" name="clientName" id="clientName"
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500">
</div>
<!-- Project URL -->
<div>
<label for="projectUrl" class="block text-sm font-medium text-gray-700">URL проекта</label>
<input type="url" name="projectUrl" id="projectUrl"
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500">
</div>
<!-- GitHub URL -->
<div>
<label for="githubUrl" class="block text-sm font-medium text-gray-700">GitHub URL</label>
<input type="url" name="githubUrl" id="githubUrl"
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500">
</div>
</div>
<!-- Description -->
<div>
<label for="description" class="block text-sm font-medium text-gray-700">Подробное описание</label>
<textarea name="description" id="description" rows="4" required
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500"></textarea>
</div>
<!-- Technologies -->
<div>
<label for="technologies" class="block text-sm font-medium text-gray-700">Технологии (через запятую)</label>
<input type="text" name="technologies" id="technologies"
placeholder="React, Node.js, PostgreSQL"
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500">
</div>
<!-- Images -->
<div>
<label class="block text-sm font-medium text-gray-700">Изображения проекта</label>
<div class="mt-1 flex justify-center px-6 pt-5 pb-6 border-2 border-gray-300 border-dashed rounded-md">
<div class="space-y-1 text-center">
<svg class="mx-auto h-12 w-12 text-gray-400" stroke="currentColor" fill="none" viewBox="0 0 48 48">
<path d="M28 8H12a4 4 0 00-4 4v20m32-12v8m0 0v8a4 4 0 01-4 4H12a4 4 0 01-4-4v-4m32-4l-3.172-3.172a4 4 0 00-5.656 0L28 28M8 32l9.172-9.172a4 4 0 015.656 0L28 28m0 0l4 4m4-24h8m-4-4v8m-12 4h.02" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
</svg>
<div class="flex text-sm text-gray-600">
<label for="images" class="relative cursor-pointer bg-white rounded-md font-medium text-blue-600 hover:text-blue-500">
<span>Загрузить файлы</span>
<input id="images" name="images" type="file" class="sr-only" multiple accept="image/*">
</label>
<p class="pl-1">или перетащите сюда</p>
</div>
<p class="text-xs text-gray-500">PNG, JPG, WEBP до 10MB</p>
</div>
</div>
<div id="imagePreview" class="mt-4 grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-4"></div>
</div>
<!-- Options -->
<div class="flex items-center space-x-6">
<div class="flex items-center">
<input id="featured" name="featured" type="checkbox"
class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded">
<label for="featured" class="ml-2 block text-sm text-gray-900">
Рекомендуемый проект
</label>
</div>
<div class="flex items-center">
<input id="isPublished" name="isPublished" type="checkbox" checked
class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded">
<label for="isPublished" class="ml-2 block text-sm text-gray-900">
Опубликовать
</label>
</div>
</div>
<!-- Submit Buttons -->
<div class="flex justify-end space-x-3">
<a href="/admin/portfolio" class="bg-gray-300 hover:bg-gray-400 text-gray-800 font-bold py-2 px-4 rounded">
Отмена
</a>
<button type="submit" class="bg-blue-600 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
<i class="fas fa-save mr-1"></i>
Сохранить
</button>
</div>
</form>
</div>
<script>
document.getElementById('portfolioForm').addEventListener('submit', async function(e) {
e.preventDefault();
const formData = new FormData(this);
// Convert technologies string to array
const technologies = formData.get('technologies');
if (technologies) {
formData.set('technologies', JSON.stringify(technologies.split(',').map(t => t.trim())));
}
try {
const response = await fetch('/api/admin/portfolio', {
method: 'POST',
body: formData
});
const data = await response.json();
if (data.success) {
window.location.href = '/admin/portfolio';
} else {
alert('Ошибка при создании проекта: ' + data.message);
}
} catch (error) {
console.error('Error:', error);
alert('Ошибка при создании проекта');
}
});
// Image preview
document.getElementById('images').addEventListener('change', function(e) {
const preview = document.getElementById('imagePreview');
preview.innerHTML = '';
for (let i = 0; i < e.target.files.length; i++) {
const file = e.target.files[i];
const reader = new FileReader();
reader.onload = function(e) {
const div = document.createElement('div');
div.className = 'relative';
div.innerHTML = `
<img src="${e.target.result}" class="h-24 w-full object-cover rounded-md">
<button type="button" onclick="this.parentElement.remove()"
class="absolute top-0 right-0 -mt-2 -mr-2 bg-red-500 text-white rounded-full w-6 h-6 flex items-center justify-center text-xs">
×
</button>
`;
preview.appendChild(div);
};
reader.readAsDataURL(file);
}
});
</script>

View File

@@ -1,186 +0,0 @@
<!-- Add Portfolio Item -->
<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-plus mr-2"></i>
Добавить проект
</h3>
</div>
<form id="portfolioForm" class="p-6 space-y-6">
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2">
<!-- Title -->
<div class="sm:col-span-2">
<label for="title" class="block text-sm font-medium text-gray-700">Название проекта</label>
<input type="text" name="title" id="title" required
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500">
</div>
<!-- Short Description -->
<div class="sm:col-span-2">
<label for="shortDescription" class="block text-sm font-medium text-gray-700">Краткое описание</label>
<input type="text" name="shortDescription" id="shortDescription"
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500">
</div>
<!-- Category -->
<div>
<label for="category" class="block text-sm font-medium text-gray-700">Категория</label>
<select name="category" id="category" required
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500">
<option value="">Выберите категорию</option>
<option value="web-development">Веб-разработка</option>
<option value="mobile-app">Мобильные приложения</option>
<option value="ui-ux-design">UI/UX дизайн</option>
<option value="branding">Брендинг</option>
<option value="marketing">Цифровой маркетинг</option>
</select>
</div>
<!-- Client Name -->
<div>
<label for="clientName" class="block text-sm font-medium text-gray-700">Клиент</label>
<input type="text" name="clientName" id="clientName"
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500">
</div>
<!-- Project URL -->
<div>
<label for="projectUrl" class="block text-sm font-medium text-gray-700">URL проекта</label>
<input type="url" name="projectUrl" id="projectUrl"
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500">
</div>
<!-- GitHub URL -->
<div>
<label for="githubUrl" class="block text-sm font-medium text-gray-700">GitHub URL</label>
<input type="url" name="githubUrl" id="githubUrl"
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500">
</div>
</div>
<!-- Description -->
<div>
<label for="description" class="block text-sm font-medium text-gray-700">Подробное описание</label>
<textarea name="description" id="description" rows="4" required
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500"></textarea>
</div>
<!-- Technologies -->
<div>
<label for="technologies" class="block text-sm font-medium text-gray-700">Технологии (через запятую)</label>
<input type="text" name="technologies" id="technologies"
placeholder="React, Node.js, PostgreSQL"
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500">
</div>
<!-- Images -->
<div>
<label class="block text-sm font-medium text-gray-700">Изображения проекта</label>
<div class="mt-1 flex justify-center px-6 pt-5 pb-6 border-2 border-gray-300 border-dashed rounded-md">
<div class="space-y-1 text-center">
<svg class="mx-auto h-12 w-12 text-gray-400" stroke="currentColor" fill="none" viewBox="0 0 48 48">
<path d="M28 8H12a4 4 0 00-4 4v20m32-12v8m0 0v8a4 4 0 01-4 4H12a4 4 0 01-4-4v-4m32-4l-3.172-3.172a4 4 0 00-5.656 0L28 28M8 32l9.172-9.172a4 4 0 015.656 0L28 28m0 0l4 4m4-24h8m-4-4v8m-12 4h.02" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
</svg>
<div class="flex text-sm text-gray-600">
<label for="images" class="relative cursor-pointer bg-white rounded-md font-medium text-blue-600 hover:text-blue-500">
<span>Загрузить файлы</span>
<input id="images" name="images" type="file" class="sr-only" multiple accept="image/*">
</label>
<p class="pl-1">или перетащите сюда</p>
</div>
<p class="text-xs text-gray-500">PNG, JPG, WEBP до 10MB</p>
</div>
</div>
<div id="imagePreview" class="mt-4 grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-4"></div>
</div>
<!-- Options -->
<div class="flex items-center space-x-6">
<div class="flex items-center">
<input id="featured" name="featured" type="checkbox"
class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded">
<label for="featured" class="ml-2 block text-sm text-gray-900">
Рекомендуемый проект
</label>
</div>
<div class="flex items-center">
<input id="isPublished" name="isPublished" type="checkbox" checked
class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded">
<label for="isPublished" class="ml-2 block text-sm text-gray-900">
Опубликовать
</label>
</div>
</div>
<!-- Submit Buttons -->
<div class="flex justify-end space-x-3">
<a href="/admin/portfolio" class="bg-gray-300 hover:bg-gray-400 text-gray-800 font-bold py-2 px-4 rounded">
Отмена
</a>
<button type="submit" class="bg-blue-600 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
<i class="fas fa-save mr-1"></i>
Сохранить
</button>
</div>
</form>
</div>
<script>
document.getElementById('portfolioForm').addEventListener('submit', async function(e) {
e.preventDefault();
const formData = new FormData(this);
// Convert technologies string to array
const technologies = formData.get('technologies');
if (technologies) {
formData.set('technologies', JSON.stringify(technologies.split(',').map(t => t.trim())));
}
try {
const response = await fetch('/api/admin/portfolio', {
method: 'POST',
body: formData
});
const data = await response.json();
if (data.success) {
window.location.href = '/admin/portfolio';
} else {
alert('Ошибка при создании проекта: ' + data.message);
}
} catch (error) {
console.error('Error:', error);
alert('Ошибка при создании проекта');
}
});
// Image preview
document.getElementById('images').addEventListener('change', function(e) {
const preview = document.getElementById('imagePreview');
preview.innerHTML = '';
for (let i = 0; i < e.target.files.length; i++) {
const file = e.target.files[i];
const reader = new FileReader();
reader.onload = function(e) {
const div = document.createElement('div');
div.className = 'relative';
div.innerHTML = `
<img src="${e.target.result}" class="h-24 w-full object-cover rounded-md">
<button type="button" onclick="this.parentElement.remove()"
class="absolute top-0 right-0 -mt-2 -mr-2 bg-red-500 text-white rounded-full w-6 h-6 flex items-center justify-center text-xs">
×
</button>
`;
preview.appendChild(div);
};
reader.readAsDataURL(file);
}
});
</script>

View File

@@ -1,223 +0,0 @@
<!-- Add Portfolio Item -->
<div class="max-w-5xl mx-auto">
<div class="bg-white shadow rounded-lg">
<div class="px-6 py-4 border-b border-gray-200">
<div class="flex items-center justify-between">
<div>
<h3 class="text-xl font-semibold text-gray-900">
<i class="fas fa-plus-circle mr-2 text-blue-600"></i>
Добавить новый проект
</h3>
<p class="mt-1 text-sm text-gray-500">Заполните информацию о проекте для добавления в портфолио</p>
</div>
<div class="flex space-x-3">
<button type="button" onclick="saveDraft()" class="inline-flex items-center px-3 py-2 border border-gray-300 shadow-sm text-sm leading-4 font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
<i class="fas fa-save mr-2"></i>
Сохранить черновик
</button>
<a href="/admin/portfolio" class="inline-flex items-center px-3 py-2 border border-gray-300 shadow-sm text-sm leading-4 font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50">
<i class="fas fa-arrow-left mr-2"></i>
Назад к списку
</a>
</div>
</div>
</div>
<form id="portfolioForm" class="p-6">
<!-- Progress indicator -->
<div class="mb-8">
<div class="flex items-center">
<div class="flex items-center text-blue-600 relative">
<div class="rounded-full h-8 w-8 bg-blue-600 text-white flex items-center justify-center text-sm font-medium">1</div>
<span class="ml-3 text-sm font-medium text-blue-600">Основная информация</span>
</div>
<div class="flex-1 mx-4 h-1 bg-gray-200"></div>
<div class="flex items-center text-gray-500">
<div class="rounded-full h-8 w-8 bg-gray-200 text-gray-600 flex items-center justify-center text-sm font-medium">2</div>
<span class="ml-3 text-sm font-medium text-gray-500">Медиа и изображения</span>
</div>
<div class="flex-1 mx-4 h-1 bg-gray-200"></div>
<div class="flex items-center text-gray-500">
<div class="rounded-full h-8 w-8 bg-gray-200 text-gray-600 flex items-center justify-center text-sm font-medium">3</div>
<span class="ml-3 text-sm font-medium text-gray-500">Публикация</span>
</div>
</div>
</div>
<div class="space-y-8">
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2">
<!-- Title -->
<div class="sm:col-span-2">
<label for="title" class="block text-sm font-medium text-gray-700">Название проекта</label>
<input type="text" name="title" id="title" required
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500">
</div>
<!-- Short Description -->
<div class="sm:col-span-2">
<label for="shortDescription" class="block text-sm font-medium text-gray-700">Краткое описание</label>
<input type="text" name="shortDescription" id="shortDescription"
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500">
</div>
<!-- Category -->
<div>
<label for="category" class="block text-sm font-medium text-gray-700">Категория</label>
<select name="category" id="category" required
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500">
<option value="">Выберите категорию</option>
<option value="web-development">Веб-разработка</option>
<option value="mobile-app">Мобильные приложения</option>
<option value="ui-ux-design">UI/UX дизайн</option>
<option value="branding">Брендинг</option>
<option value="marketing">Цифровой маркетинг</option>
</select>
</div>
<!-- Client Name -->
<div>
<label for="clientName" class="block text-sm font-medium text-gray-700">Клиент</label>
<input type="text" name="clientName" id="clientName"
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500">
</div>
<!-- Project URL -->
<div>
<label for="projectUrl" class="block text-sm font-medium text-gray-700">URL проекта</label>
<input type="url" name="projectUrl" id="projectUrl"
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500">
</div>
<!-- GitHub URL -->
<div>
<label for="githubUrl" class="block text-sm font-medium text-gray-700">GitHub URL</label>
<input type="url" name="githubUrl" id="githubUrl"
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500">
</div>
</div>
<!-- Description -->
<div>
<label for="description" class="block text-sm font-medium text-gray-700">Подробное описание</label>
<textarea name="description" id="description" rows="4" required
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500"></textarea>
</div>
<!-- Technologies -->
<div>
<label for="technologies" class="block text-sm font-medium text-gray-700">Технологии (через запятую)</label>
<input type="text" name="technologies" id="technologies"
placeholder="React, Node.js, PostgreSQL"
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500">
</div>
<!-- Images -->
<div>
<label class="block text-sm font-medium text-gray-700">Изображения проекта</label>
<div class="mt-1 flex justify-center px-6 pt-5 pb-6 border-2 border-gray-300 border-dashed rounded-md">
<div class="space-y-1 text-center">
<svg class="mx-auto h-12 w-12 text-gray-400" stroke="currentColor" fill="none" viewBox="0 0 48 48">
<path d="M28 8H12a4 4 0 00-4 4v20m32-12v8m0 0v8a4 4 0 01-4 4H12a4 4 0 01-4-4v-4m32-4l-3.172-3.172a4 4 0 00-5.656 0L28 28M8 32l9.172-9.172a4 4 0 015.656 0L28 28m0 0l4 4m4-24h8m-4-4v8m-12 4h.02" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
</svg>
<div class="flex text-sm text-gray-600">
<label for="images" class="relative cursor-pointer bg-white rounded-md font-medium text-blue-600 hover:text-blue-500">
<span>Загрузить файлы</span>
<input id="images" name="images" type="file" class="sr-only" multiple accept="image/*">
</label>
<p class="pl-1">или перетащите сюда</p>
</div>
<p class="text-xs text-gray-500">PNG, JPG, WEBP до 10MB</p>
</div>
</div>
<div id="imagePreview" class="mt-4 grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-4"></div>
</div>
<!-- Options -->
<div class="flex items-center space-x-6">
<div class="flex items-center">
<input id="featured" name="featured" type="checkbox"
class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded">
<label for="featured" class="ml-2 block text-sm text-gray-900">
Рекомендуемый проект
</label>
</div>
<div class="flex items-center">
<input id="isPublished" name="isPublished" type="checkbox" checked
class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded">
<label for="isPublished" class="ml-2 block text-sm text-gray-900">
Опубликовать
</label>
</div>
</div>
<!-- Submit Buttons -->
<div class="flex justify-end space-x-3">
<a href="/admin/portfolio" class="bg-gray-300 hover:bg-gray-400 text-gray-800 font-bold py-2 px-4 rounded">
Отмена
</a>
<button type="submit" class="bg-blue-600 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
<i class="fas fa-save mr-1"></i>
Сохранить
</button>
</div>
</form>
</div>
<script>
document.getElementById('portfolioForm').addEventListener('submit', async function(e) {
e.preventDefault();
const formData = new FormData(this);
// Convert technologies string to array
const technologies = formData.get('technologies');
if (technologies) {
formData.set('technologies', JSON.stringify(technologies.split(',').map(t => t.trim())));
}
try {
const response = await fetch('/api/admin/portfolio', {
method: 'POST',
body: formData
});
const data = await response.json();
if (data.success) {
window.location.href = '/admin/portfolio';
} else {
alert('Ошибка при создании проекта: ' + data.message);
}
} catch (error) {
console.error('Error:', error);
alert('Ошибка при создании проекта');
}
});
// Image preview
document.getElementById('images').addEventListener('change', function(e) {
const preview = document.getElementById('imagePreview');
preview.innerHTML = '';
for (let i = 0; i < e.target.files.length; i++) {
const file = e.target.files[i];
const reader = new FileReader();
reader.onload = function(e) {
const div = document.createElement('div');
div.className = 'relative';
div.innerHTML = `
<img src="${e.target.result}" class="h-24 w-full object-cover rounded-md">
<button type="button" onclick="this.parentElement.remove()"
class="absolute top-0 right-0 -mt-2 -mr-2 bg-red-500 text-white rounded-full w-6 h-6 flex items-center justify-center text-xs">
×
</button>
`;
preview.appendChild(div);
};
reader.readAsDataURL(file);
}
});
</script>

View File

@@ -1,266 +0,0 @@
<!-- Add Portfolio Item -->
<div class="max-w-5xl mx-auto">
<div class="bg-white shadow rounded-lg">
<div class="px-6 py-4 border-b border-gray-200">
<div class="flex items-center justify-between">
<div>
<h3 class="text-xl font-semibold text-gray-900">
<i class="fas fa-plus-circle mr-2 text-blue-600"></i>
Добавить новый проект
</h3>
<p class="mt-1 text-sm text-gray-500">Заполните информацию о проекте для добавления в портфолио</p>
</div>
<div class="flex space-x-3">
<button type="button" onclick="saveDraft()" class="inline-flex items-center px-3 py-2 border border-gray-300 shadow-sm text-sm leading-4 font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
<i class="fas fa-save mr-2"></i>
Сохранить черновик
</button>
<a href="/admin/portfolio" class="inline-flex items-center px-3 py-2 border border-gray-300 shadow-sm text-sm leading-4 font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50">
<i class="fas fa-arrow-left mr-2"></i>
Назад к списку
</a>
</div>
</div>
</div>
<form id="portfolioForm" class="p-6">
<!-- Progress indicator -->
<div class="mb-8">
<div class="flex items-center">
<div class="flex items-center text-blue-600 relative">
<div class="rounded-full h-8 w-8 bg-blue-600 text-white flex items-center justify-center text-sm font-medium">1</div>
<span class="ml-3 text-sm font-medium text-blue-600">Основная информация</span>
</div>
<div class="flex-1 mx-4 h-1 bg-gray-200"></div>
<div class="flex items-center text-gray-500">
<div class="rounded-full h-8 w-8 bg-gray-200 text-gray-600 flex items-center justify-center text-sm font-medium">2</div>
<span class="ml-3 text-sm font-medium text-gray-500">Медиа и изображения</span>
</div>
<div class="flex-1 mx-4 h-1 bg-gray-200"></div>
<div class="flex items-center text-gray-500">
<div class="rounded-full h-8 w-8 bg-gray-200 text-gray-600 flex items-center justify-center text-sm font-medium">3</div>
<span class="ml-3 text-sm font-medium text-gray-500">Публикация</span>
</div>
</div>
</div>
<div class="space-y-8">
<!-- Шаг 1: Основная информация -->
<div class="bg-gray-50 rounded-lg p-6">
<h4 class="text-lg font-medium text-gray-900 mb-6">
<i class="fas fa-info-circle mr-2 text-blue-600"></i>
Основная информация о проекте
</h4>
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2">
<!-- Title -->
<div class="sm:col-span-2">
<label for="title" class="block text-sm font-medium text-gray-700 mb-2">
Название проекта *
<span class="text-gray-500 font-normal">(отображается в портфолио)</span>
</label>
<input type="text" name="title" id="title" required
placeholder="Например: Интернет-магазин электроники TechStore"
class="block w-full border-gray-300 rounded-lg shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm">
<p class="mt-1 text-sm text-gray-500">Используйте описательное название, которое четко передает суть проекта</p>
</div>
<!-- Short Description -->
<div class="sm:col-span-2">
<label for="shortDescription" class="block text-sm font-medium text-gray-700 mb-2">
Краткое описание *
<span class="text-gray-500 font-normal">(1-2 предложения)</span>
</label>
<textarea name="shortDescription" id="shortDescription" rows="2" required
placeholder="Современный интернет-магазин с удобной системой каталогов и интеграцией платежных систем"
class="block w-full border-gray-300 rounded-lg shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
maxlength="200"></textarea>
<div class="mt-1 flex justify-between text-sm text-gray-500">
<span>Отображается в превью проекта</span>
<span id="shortDescCount">0/200</span>
</div>
</div>
<!-- Category -->
<div>
<label for="category" class="block text-sm font-medium text-gray-700 mb-2">Категория *</label>
<select name="category" id="category" required
class="block w-full border-gray-300 rounded-lg shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm">
<option value="">Выберите категорию</option>
<% categories.forEach(cat => { %>
<option value="<%= cat %>"><%= getCategoryName(cat) %></option>
<% }); %>
</select>
</div>
<!-- Duration -->
<div>
<label for="duration" class="block text-sm font-medium text-gray-700 mb-2">
Длительность проекта
<span class="text-gray-500 font-normal">(в днях)</span>
</label>
<input type="number" name="duration" id="duration" min="1" max="365"
placeholder="30"
class="block w-full border-gray-300 rounded-lg shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm">
</div>
<!-- Client Name -->
<div>
<label for="clientName" class="block text-sm font-medium text-gray-700 mb-2">
Название клиента
<span class="text-gray-500 font-normal">(необязательно)</span>
</label>
<input type="text" name="clientName" id="clientName"
placeholder="ООО «ТехноСтор»"
class="block w-full border-gray-300 rounded-lg shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm">
</div>
<!-- Demo URL -->
<div>
<label for="demoUrl" class="block text-sm font-medium text-gray-700 mb-2">
Ссылка на демо
<span class="text-gray-500 font-normal">(живая версия)</span>
</label>
<input type="url" name="demoUrl" id="demoUrl"
placeholder="https://techstore.example.com"
class="block w-full border-gray-300 rounded-lg shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm">
</div>
<!-- GitHub URL -->
<div>
<label for="githubUrl" class="block text-sm font-medium text-gray-700 mb-2">
GitHub репозиторий
<span class="text-gray-500 font-normal">(если публичный)</span>
</label>
<input type="url" name="githubUrl" id="githubUrl"
placeholder="https://github.com/username/project"
class="block w-full border-gray-300 rounded-lg shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm">
</div>
</div>
</div>
<!-- Description -->
<div>
<label for="description" class="block text-sm font-medium text-gray-700">Подробное описание</label>
<textarea name="description" id="description" rows="4" required
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500"></textarea>
</div>
<!-- Technologies -->
<div>
<label for="technologies" class="block text-sm font-medium text-gray-700">Технологии (через запятую)</label>
<input type="text" name="technologies" id="technologies"
placeholder="React, Node.js, PostgreSQL"
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500">
</div>
<!-- Images -->
<div>
<label class="block text-sm font-medium text-gray-700">Изображения проекта</label>
<div class="mt-1 flex justify-center px-6 pt-5 pb-6 border-2 border-gray-300 border-dashed rounded-md">
<div class="space-y-1 text-center">
<svg class="mx-auto h-12 w-12 text-gray-400" stroke="currentColor" fill="none" viewBox="0 0 48 48">
<path d="M28 8H12a4 4 0 00-4 4v20m32-12v8m0 0v8a4 4 0 01-4 4H12a4 4 0 01-4-4v-4m32-4l-3.172-3.172a4 4 0 00-5.656 0L28 28M8 32l9.172-9.172a4 4 0 015.656 0L28 28m0 0l4 4m4-24h8m-4-4v8m-12 4h.02" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
</svg>
<div class="flex text-sm text-gray-600">
<label for="images" class="relative cursor-pointer bg-white rounded-md font-medium text-blue-600 hover:text-blue-500">
<span>Загрузить файлы</span>
<input id="images" name="images" type="file" class="sr-only" multiple accept="image/*">
</label>
<p class="pl-1">или перетащите сюда</p>
</div>
<p class="text-xs text-gray-500">PNG, JPG, WEBP до 10MB</p>
</div>
</div>
<div id="imagePreview" class="mt-4 grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-4"></div>
</div>
<!-- Options -->
<div class="flex items-center space-x-6">
<div class="flex items-center">
<input id="featured" name="featured" type="checkbox"
class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded">
<label for="featured" class="ml-2 block text-sm text-gray-900">
Рекомендуемый проект
</label>
</div>
<div class="flex items-center">
<input id="isPublished" name="isPublished" type="checkbox" checked
class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded">
<label for="isPublished" class="ml-2 block text-sm text-gray-900">
Опубликовать
</label>
</div>
</div>
<!-- Submit Buttons -->
<div class="flex justify-end space-x-3">
<a href="/admin/portfolio" class="bg-gray-300 hover:bg-gray-400 text-gray-800 font-bold py-2 px-4 rounded">
Отмена
</a>
<button type="submit" class="bg-blue-600 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
<i class="fas fa-save mr-1"></i>
Сохранить
</button>
</div>
</form>
</div>
<script>
document.getElementById('portfolioForm').addEventListener('submit', async function(e) {
e.preventDefault();
const formData = new FormData(this);
// Convert technologies string to array
const technologies = formData.get('technologies');
if (technologies) {
formData.set('technologies', JSON.stringify(technologies.split(',').map(t => t.trim())));
}
try {
const response = await fetch('/api/admin/portfolio', {
method: 'POST',
body: formData
});
const data = await response.json();
if (data.success) {
window.location.href = '/admin/portfolio';
} else {
alert('Ошибка при создании проекта: ' + data.message);
}
} catch (error) {
console.error('Error:', error);
alert('Ошибка при создании проекта');
}
});
// Image preview
document.getElementById('images').addEventListener('change', function(e) {
const preview = document.getElementById('imagePreview');
preview.innerHTML = '';
for (let i = 0; i < e.target.files.length; i++) {
const file = e.target.files[i];
const reader = new FileReader();
reader.onload = function(e) {
const div = document.createElement('div');
div.className = 'relative';
div.innerHTML = `
<img src="${e.target.result}" class="h-24 w-full object-cover rounded-md">
<button type="button" onclick="this.parentElement.remove()"
class="absolute top-0 right-0 -mt-2 -mr-2 bg-red-500 text-white rounded-full w-6 h-6 flex items-center justify-center text-xs">
×
</button>
`;
preview.appendChild(div);
};
reader.readAsDataURL(file);
}
});
</script>

View File

@@ -1,385 +0,0 @@
<!-- Add Portfolio Item -->
<div class="max-w-5xl mx-auto">
<div class="bg-white shadow rounded-lg">
<div class="px-6 py-4 border-b border-gray-200">
<div class="flex items-center justify-between">
<div>
<h3 class="text-xl font-semibold text-gray-900">
<i class="fas fa-plus-circle mr-2 text-blue-600"></i>
Добавить новый проект
</h3>
<p class="mt-1 text-sm text-gray-500">Заполните информацию о проекте для добавления в портфолио</p>
</div>
<div class="flex space-x-3">
<button type="button" onclick="saveDraft()" class="inline-flex items-center px-3 py-2 border border-gray-300 shadow-sm text-sm leading-4 font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
<i class="fas fa-save mr-2"></i>
Сохранить черновик
</button>
<a href="/admin/portfolio" class="inline-flex items-center px-3 py-2 border border-gray-300 shadow-sm text-sm leading-4 font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50">
<i class="fas fa-arrow-left mr-2"></i>
Назад к списку
</a>
</div>
</div>
</div>
<form id="portfolioForm" class="p-6">
<!-- Progress indicator -->
<div class="mb-8">
<div class="flex items-center">
<div class="flex items-center text-blue-600 relative">
<div class="rounded-full h-8 w-8 bg-blue-600 text-white flex items-center justify-center text-sm font-medium">1</div>
<span class="ml-3 text-sm font-medium text-blue-600">Основная информация</span>
</div>
<div class="flex-1 mx-4 h-1 bg-gray-200"></div>
<div class="flex items-center text-gray-500">
<div class="rounded-full h-8 w-8 bg-gray-200 text-gray-600 flex items-center justify-center text-sm font-medium">2</div>
<span class="ml-3 text-sm font-medium text-gray-500">Медиа и изображения</span>
</div>
<div class="flex-1 mx-4 h-1 bg-gray-200"></div>
<div class="flex items-center text-gray-500">
<div class="rounded-full h-8 w-8 bg-gray-200 text-gray-600 flex items-center justify-center text-sm font-medium">3</div>
<span class="ml-3 text-sm font-medium text-gray-500">Публикация</span>
</div>
</div>
</div>
<div class="space-y-8">
<!-- Шаг 1: Основная информация -->
<div class="bg-gray-50 rounded-lg p-6">
<h4 class="text-lg font-medium text-gray-900 mb-6">
<i class="fas fa-info-circle mr-2 text-blue-600"></i>
Основная информация о проекте
</h4>
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2">
<!-- Title -->
<div class="sm:col-span-2">
<label for="title" class="block text-sm font-medium text-gray-700 mb-2">
Название проекта *
<span class="text-gray-500 font-normal">(отображается в портфолио)</span>
</label>
<input type="text" name="title" id="title" required
placeholder="Например: Интернет-магазин электроники TechStore"
class="block w-full border-gray-300 rounded-lg shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm">
<p class="mt-1 text-sm text-gray-500">Используйте описательное название, которое четко передает суть проекта</p>
</div>
<!-- Short Description -->
<div class="sm:col-span-2">
<label for="shortDescription" class="block text-sm font-medium text-gray-700 mb-2">
Краткое описание *
<span class="text-gray-500 font-normal">(1-2 предложения)</span>
</label>
<textarea name="shortDescription" id="shortDescription" rows="2" required
placeholder="Современный интернет-магазин с удобной системой каталогов и интеграцией платежных систем"
class="block w-full border-gray-300 rounded-lg shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
maxlength="200"></textarea>
<div class="mt-1 flex justify-between text-sm text-gray-500">
<span>Отображается в превью проекта</span>
<span id="shortDescCount">0/200</span>
</div>
</div>
<!-- Category -->
<div>
<label for="category" class="block text-sm font-medium text-gray-700 mb-2">Категория *</label>
<select name="category" id="category" required
class="block w-full border-gray-300 rounded-lg shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm">
<option value="">Выберите категорию</option>
<% categories.forEach(cat => { %>
<option value="<%= cat %>"><%= getCategoryName(cat) %></option>
<% }); %>
</select>
</div>
<!-- Duration -->
<div>
<label for="duration" class="block text-sm font-medium text-gray-700 mb-2">
Длительность проекта
<span class="text-gray-500 font-normal">(в днях)</span>
</label>
<input type="number" name="duration" id="duration" min="1" max="365"
placeholder="30"
class="block w-full border-gray-300 rounded-lg shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm">
</div>
<!-- Client Name -->
<div>
<label for="clientName" class="block text-sm font-medium text-gray-700 mb-2">
Название клиента
<span class="text-gray-500 font-normal">(необязательно)</span>
</label>
<input type="text" name="clientName" id="clientName"
placeholder="ООО «ТехноСтор»"
class="block w-full border-gray-300 rounded-lg shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm">
</div>
<!-- Demo URL -->
<div>
<label for="demoUrl" class="block text-sm font-medium text-gray-700 mb-2">
Ссылка на демо
<span class="text-gray-500 font-normal">(живая версия)</span>
</label>
<input type="url" name="demoUrl" id="demoUrl"
placeholder="https://techstore.example.com"
class="block w-full border-gray-300 rounded-lg shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm">
</div>
<!-- GitHub URL -->
<div>
<label for="githubUrl" class="block text-sm font-medium text-gray-700 mb-2">
GitHub репозиторий
<span class="text-gray-500 font-normal">(если публичный)</span>
</label>
<input type="url" name="githubUrl" id="githubUrl"
placeholder="https://github.com/username/project"
class="block w-full border-gray-300 rounded-lg shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm">
</div>
</div>
</div>
<!-- Шаг 2: Детальное описание и технологии -->
<div class="bg-gray-50 rounded-lg p-6">
<h4 class="text-lg font-medium text-gray-900 mb-6">
<i class="fas fa-align-left mr-2 text-blue-600"></i>
Описание и технические детали
</h4>
<!-- Description -->
<div class="mb-6">
<label for="description" class="block text-sm font-medium text-gray-700 mb-2">
Подробное описание проекта *
</label>
<div id="descriptionEditor" class="min-h-40 border border-gray-300 rounded-lg"></div>
<textarea name="description" id="description" style="display: none;" required></textarea>
<p class="mt-2 text-sm text-gray-500">Опишите задачи, решения и результаты проекта</p>
</div>
<!-- Technologies -->
<div>
<label for="technologies" class="block text-sm font-medium text-gray-700 mb-2">
Используемые технологии *
</label>
<div class="relative">
<input type="text" id="technologyInput"
placeholder="Введите технологию и нажмите Enter"
class="block w-full border-gray-300 rounded-lg shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm">
<div class="absolute right-2 top-2">
<button type="button" onclick="addTechnology()" class="text-blue-600 hover:text-blue-800">
<i class="fas fa-plus"></i>
</button>
</div>
</div>
<div id="technologiesList" class="mt-3 flex flex-wrap gap-2"></div>
<input type="hidden" name="technologies" id="technologiesHidden">
<!-- Популярные технологии для быстрого добавления -->
<div class="mt-4">
<p class="text-sm text-gray-600 mb-2">Популярные технологии:</p>
<div class="flex flex-wrap gap-2">
<% const popularTechs = ['React', 'Vue.js', 'Node.js', 'Express.js', 'MongoDB', 'PostgreSQL', 'MySQL', 'JavaScript', 'TypeScript', 'HTML5', 'CSS3', 'Tailwind CSS', 'Bootstrap', 'Webpack', 'Docker', 'Git', 'AWS', 'Figma', 'Photoshop']; %>
<% popularTechs.forEach(tech => { %>
<button type="button" onclick="addTechnologyFromList('<%= tech %>')"
class="px-2 py-1 text-xs border border-gray-300 rounded-md text-gray-600 hover:bg-gray-100">
<%= tech %>
</button>
<% }); %>
</div>
</div>
</div>
</div>
<!-- Шаг 3: Медиа контент -->
<div class="bg-gray-50 rounded-lg p-6">
<h4 class="text-lg font-medium text-gray-900 mb-6">
<i class="fas fa-images mr-2 text-blue-600"></i>
Изображения и медиа контент
</h4>
<div class="space-y-6">
<!-- Main Image Upload -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-3">
Изображения проекта
<span class="text-red-500">*</span>
</label>
<div id="dropZone" class="relative border-2 border-dashed border-gray-300 rounded-lg p-8 text-center hover:border-blue-500 transition-colors">
<div class="space-y-4">
<div class="mx-auto w-16 h-16 text-gray-400">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"/>
</svg>
</div>
<div>
<label for="images" class="cursor-pointer">
<span class="text-blue-600 font-medium hover:text-blue-500">Загрузить изображения</span>
<input id="images" name="images" type="file" class="sr-only" multiple accept="image/*">
</label>
<span class="text-gray-600"> или перетащите файлы сюда</span>
</div>
<p class="text-sm text-gray-500">
PNG, JPG, WEBP до 10MB каждый. Первое изображение будет использоваться как главное.
</p>
</div>
</div>
</div>
<!-- Image Preview and Management -->
<div id="imagePreviewContainer" style="display: none;">
<div class="flex items-center justify-between mb-4">
<h5 class="text-sm font-medium text-gray-900">Загруженные изображения</h5>
<button type="button" onclick="clearAllImages()" class="text-red-600 hover:text-red-800 text-sm">
<i class="fas fa-trash mr-1"></i>
Удалить все
</button>
</div>
<div id="imagePreview" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4"></div>
<p class="mt-2 text-sm text-gray-500">
<i class="fas fa-info-circle mr-1"></i>
Перетаскивайте изображения для изменения порядка. Первое изображение будет главным.
</p>
</div>
</div>
</div>
<!-- Шаг 4: Настройки публикации -->
<div class="bg-gray-50 rounded-lg p-6">
<h4 class="text-lg font-medium text-gray-900 mb-6">
<i class="fas fa-cog mr-2 text-blue-600"></i>
Настройки публикации
</h4>
<div class="space-y-6">
<!-- Publication Options -->
<div class="grid grid-cols-1 sm:grid-cols-2 gap-6">
<div class="flex items-start">
<div class="flex items-center h-5">
<input id="isPublished" name="isPublished" type="checkbox"
class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded">
</div>
<div class="ml-3">
<label for="isPublished" class="text-sm font-medium text-gray-900">Опубликовать проект</label>
<p class="text-sm text-gray-500">Проект будет виден посетителям сайта</p>
</div>
</div>
<div class="flex items-start">
<div class="flex items-center h-5">
<input id="featured" name="featured" type="checkbox"
class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded">
</div>
<div class="ml-3">
<label for="featured" class="text-sm font-medium text-gray-900">Рекомендуемый проект</label>
<p class="text-sm text-gray-500">Проект будет показан в топе портфолио</p>
</div>
</div>
</div>
<!-- SEO Fields -->
<div class="border-t border-gray-200 pt-6">
<h5 class="text-sm font-medium text-gray-900 mb-4">SEO настройки (необязательно)</h5>
<div class="space-y-4">
<div>
<label for="seoTitle" class="block text-sm font-medium text-gray-700 mb-1">SEO заголовок</label>
<input type="text" name="seoTitle" id="seoTitle" maxlength="60"
placeholder="Будет использован заголовок проекта"
class="block w-full border-gray-300 rounded-lg shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm">
<p class="mt-1 text-sm text-gray-500">Рекомендуется до 60 символов</p>
</div>
<div>
<label for="seoDescription" class="block text-sm font-medium text-gray-700 mb-1">SEO описание</label>
<textarea name="seoDescription" id="seoDescription" rows="2" maxlength="160"
placeholder="Будет использовано краткое описание"
class="block w-full border-gray-300 rounded-lg shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm"></textarea>
<p class="mt-1 text-sm text-gray-500">Рекомендуется до 160 символов</p>
</div>
</div>
</div>
</div>
</div>
<!-- Submit Buttons -->
<div class="flex justify-between items-center pt-6 border-t border-gray-200">
<div class="flex space-x-3">
<button type="button" onclick="previewProject()" class="inline-flex items-center px-4 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50">
<i class="fas fa-eye mr-2"></i>
Предпросмотр
</button>
</div>
<div class="flex space-x-3">
<a href="/admin/portfolio" class="inline-flex items-center px-4 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50">
Отмена
</a>
<button type="button" onclick="saveDraft()" class="inline-flex items-center px-4 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50">
<i class="fas fa-save mr-2"></i>
Сохранить как черновик
</button>
<button type="submit" id="submitBtn" class="inline-flex items-center px-6 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
<i class="fas fa-check mr-2"></i>
Сохранить проект
</button>
</div>
</div>
</form>
</div>
<script>
document.getElementById('portfolioForm').addEventListener('submit', async function(e) {
e.preventDefault();
const formData = new FormData(this);
// Convert technologies string to array
const technologies = formData.get('technologies');
if (technologies) {
formData.set('technologies', JSON.stringify(technologies.split(',').map(t => t.trim())));
}
try {
const response = await fetch('/api/admin/portfolio', {
method: 'POST',
body: formData
});
const data = await response.json();
if (data.success) {
window.location.href = '/admin/portfolio';
} else {
alert('Ошибка при создании проекта: ' + data.message);
}
} catch (error) {
console.error('Error:', error);
alert('Ошибка при создании проекта');
}
});
// Image preview
document.getElementById('images').addEventListener('change', function(e) {
const preview = document.getElementById('imagePreview');
preview.innerHTML = '';
for (let i = 0; i < e.target.files.length; i++) {
const file = e.target.files[i];
const reader = new FileReader();
reader.onload = function(e) {
const div = document.createElement('div');
div.className = 'relative';
div.innerHTML = `
<img src="${e.target.result}" class="h-24 w-full object-cover rounded-md">
<button type="button" onclick="this.parentElement.remove()"
class="absolute top-0 right-0 -mt-2 -mr-2 bg-red-500 text-white rounded-full w-6 h-6 flex items-center justify-center text-xs">
×
</button>
`;
preview.appendChild(div);
};
reader.readAsDataURL(file);
}
});
</script>

View File

@@ -1,776 +0,0 @@
<!-- Add Portfolio Item -->
<div class="max-w-5xl mx-auto">
<div class="bg-white shadow rounded-lg">
<div class="px-6 py-4 border-b border-gray-200">
<div class="flex items-center justify-between">
<div>
<h3 class="text-xl font-semibold text-gray-900">
<i class="fas fa-plus-circle mr-2 text-blue-600"></i>
Добавить новый проект
</h3>
<p class="mt-1 text-sm text-gray-500">Заполните информацию о проекте для добавления в портфолио</p>
</div>
<div class="flex space-x-3">
<button type="button" onclick="saveDraft()" class="inline-flex items-center px-3 py-2 border border-gray-300 shadow-sm text-sm leading-4 font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
<i class="fas fa-save mr-2"></i>
Сохранить черновик
</button>
<a href="/admin/portfolio" class="inline-flex items-center px-3 py-2 border border-gray-300 shadow-sm text-sm leading-4 font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50">
<i class="fas fa-arrow-left mr-2"></i>
Назад к списку
</a>
</div>
</div>
</div>
<form id="portfolioForm" class="p-6">
<!-- Progress indicator -->
<div class="mb-8">
<div class="flex items-center">
<div class="flex items-center text-blue-600 relative">
<div class="rounded-full h-8 w-8 bg-blue-600 text-white flex items-center justify-center text-sm font-medium">1</div>
<span class="ml-3 text-sm font-medium text-blue-600">Основная информация</span>
</div>
<div class="flex-1 mx-4 h-1 bg-gray-200"></div>
<div class="flex items-center text-gray-500">
<div class="rounded-full h-8 w-8 bg-gray-200 text-gray-600 flex items-center justify-center text-sm font-medium">2</div>
<span class="ml-3 text-sm font-medium text-gray-500">Медиа и изображения</span>
</div>
<div class="flex-1 mx-4 h-1 bg-gray-200"></div>
<div class="flex items-center text-gray-500">
<div class="rounded-full h-8 w-8 bg-gray-200 text-gray-600 flex items-center justify-center text-sm font-medium">3</div>
<span class="ml-3 text-sm font-medium text-gray-500">Публикация</span>
</div>
</div>
</div>
<div class="space-y-8">
<!-- Шаг 1: Основная информация -->
<div class="bg-gray-50 rounded-lg p-6">
<h4 class="text-lg font-medium text-gray-900 mb-6">
<i class="fas fa-info-circle mr-2 text-blue-600"></i>
Основная информация о проекте
</h4>
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2">
<!-- Title -->
<div class="sm:col-span-2">
<label for="title" class="block text-sm font-medium text-gray-700 mb-2">
Название проекта *
<span class="text-gray-500 font-normal">(отображается в портфолио)</span>
</label>
<input type="text" name="title" id="title" required
placeholder="Например: Интернет-магазин электроники TechStore"
class="block w-full border-gray-300 rounded-lg shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm">
<p class="mt-1 text-sm text-gray-500">Используйте описательное название, которое четко передает суть проекта</p>
</div>
<!-- Short Description -->
<div class="sm:col-span-2">
<label for="shortDescription" class="block text-sm font-medium text-gray-700 mb-2">
Краткое описание *
<span class="text-gray-500 font-normal">(1-2 предложения)</span>
</label>
<textarea name="shortDescription" id="shortDescription" rows="2" required
placeholder="Современный интернет-магазин с удобной системой каталогов и интеграцией платежных систем"
class="block w-full border-gray-300 rounded-lg shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
maxlength="200"></textarea>
<div class="mt-1 flex justify-between text-sm text-gray-500">
<span>Отображается в превью проекта</span>
<span id="shortDescCount">0/200</span>
</div>
</div>
<!-- Category -->
<div>
<label for="category" class="block text-sm font-medium text-gray-700 mb-2">Категория *</label>
<select name="category" id="category" required
class="block w-full border-gray-300 rounded-lg shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm">
<option value="">Выберите категорию</option>
<% categories.forEach(cat => { %>
<option value="<%= cat %>"><%= getCategoryName(cat) %></option>
<% }); %>
</select>
</div>
<!-- Duration -->
<div>
<label for="duration" class="block text-sm font-medium text-gray-700 mb-2">
Длительность проекта
<span class="text-gray-500 font-normal">(в днях)</span>
</label>
<input type="number" name="duration" id="duration" min="1" max="365"
placeholder="30"
class="block w-full border-gray-300 rounded-lg shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm">
</div>
<!-- Client Name -->
<div>
<label for="clientName" class="block text-sm font-medium text-gray-700 mb-2">
Название клиента
<span class="text-gray-500 font-normal">(необязательно)</span>
</label>
<input type="text" name="clientName" id="clientName"
placeholder="ООО «ТехноСтор»"
class="block w-full border-gray-300 rounded-lg shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm">
</div>
<!-- Demo URL -->
<div>
<label for="demoUrl" class="block text-sm font-medium text-gray-700 mb-2">
Ссылка на демо
<span class="text-gray-500 font-normal">(живая версия)</span>
</label>
<input type="url" name="demoUrl" id="demoUrl"
placeholder="https://techstore.example.com"
class="block w-full border-gray-300 rounded-lg shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm">
</div>
<!-- GitHub URL -->
<div>
<label for="githubUrl" class="block text-sm font-medium text-gray-700 mb-2">
GitHub репозиторий
<span class="text-gray-500 font-normal">(если публичный)</span>
</label>
<input type="url" name="githubUrl" id="githubUrl"
placeholder="https://github.com/username/project"
class="block w-full border-gray-300 rounded-lg shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm">
</div>
</div>
</div>
<!-- Шаг 2: Детальное описание и технологии -->
<div class="bg-gray-50 rounded-lg p-6">
<h4 class="text-lg font-medium text-gray-900 mb-6">
<i class="fas fa-align-left mr-2 text-blue-600"></i>
Описание и технические детали
</h4>
<!-- Description -->
<div class="mb-6">
<label for="description" class="block text-sm font-medium text-gray-700 mb-2">
Подробное описание проекта *
</label>
<div id="descriptionEditor" class="min-h-40 border border-gray-300 rounded-lg"></div>
<textarea name="description" id="description" style="display: none;" required></textarea>
<p class="mt-2 text-sm text-gray-500">Опишите задачи, решения и результаты проекта</p>
</div>
<!-- Technologies -->
<div>
<label for="technologies" class="block text-sm font-medium text-gray-700 mb-2">
Используемые технологии *
</label>
<div class="relative">
<input type="text" id="technologyInput"
placeholder="Введите технологию и нажмите Enter"
class="block w-full border-gray-300 rounded-lg shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm">
<div class="absolute right-2 top-2">
<button type="button" onclick="addTechnology()" class="text-blue-600 hover:text-blue-800">
<i class="fas fa-plus"></i>
</button>
</div>
</div>
<div id="technologiesList" class="mt-3 flex flex-wrap gap-2"></div>
<input type="hidden" name="technologies" id="technologiesHidden">
<!-- Популярные технологии для быстрого добавления -->
<div class="mt-4">
<p class="text-sm text-gray-600 mb-2">Популярные технологии:</p>
<div class="flex flex-wrap gap-2">
<% const popularTechs = ['React', 'Vue.js', 'Node.js', 'Express.js', 'MongoDB', 'PostgreSQL', 'MySQL', 'JavaScript', 'TypeScript', 'HTML5', 'CSS3', 'Tailwind CSS', 'Bootstrap', 'Webpack', 'Docker', 'Git', 'AWS', 'Figma', 'Photoshop']; %>
<% popularTechs.forEach(tech => { %>
<button type="button" onclick="addTechnologyFromList('<%= tech %>')"
class="px-2 py-1 text-xs border border-gray-300 rounded-md text-gray-600 hover:bg-gray-100">
<%= tech %>
</button>
<% }); %>
</div>
</div>
</div>
</div>
<!-- Шаг 3: Медиа контент -->
<div class="bg-gray-50 rounded-lg p-6">
<h4 class="text-lg font-medium text-gray-900 mb-6">
<i class="fas fa-images mr-2 text-blue-600"></i>
Изображения и медиа контент
</h4>
<div class="space-y-6">
<!-- Main Image Upload -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-3">
Изображения проекта
<span class="text-red-500">*</span>
</label>
<div id="dropZone" class="relative border-2 border-dashed border-gray-300 rounded-lg p-8 text-center hover:border-blue-500 transition-colors">
<div class="space-y-4">
<div class="mx-auto w-16 h-16 text-gray-400">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"/>
</svg>
</div>
<div>
<label for="images" class="cursor-pointer">
<span class="text-blue-600 font-medium hover:text-blue-500">Загрузить изображения</span>
<input id="images" name="images" type="file" class="sr-only" multiple accept="image/*">
</label>
<span class="text-gray-600"> или перетащите файлы сюда</span>
</div>
<p class="text-sm text-gray-500">
PNG, JPG, WEBP до 10MB каждый. Первое изображение будет использоваться как главное.
</p>
</div>
</div>
</div>
<!-- Image Preview and Management -->
<div id="imagePreviewContainer" style="display: none;">
<div class="flex items-center justify-between mb-4">
<h5 class="text-sm font-medium text-gray-900">Загруженные изображения</h5>
<button type="button" onclick="clearAllImages()" class="text-red-600 hover:text-red-800 text-sm">
<i class="fas fa-trash mr-1"></i>
Удалить все
</button>
</div>
<div id="imagePreview" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4"></div>
<p class="mt-2 text-sm text-gray-500">
<i class="fas fa-info-circle mr-1"></i>
Перетаскивайте изображения для изменения порядка. Первое изображение будет главным.
</p>
</div>
</div>
</div>
<!-- Шаг 4: Настройки публикации -->
<div class="bg-gray-50 rounded-lg p-6">
<h4 class="text-lg font-medium text-gray-900 mb-6">
<i class="fas fa-cog mr-2 text-blue-600"></i>
Настройки публикации
</h4>
<div class="space-y-6">
<!-- Publication Options -->
<div class="grid grid-cols-1 sm:grid-cols-2 gap-6">
<div class="flex items-start">
<div class="flex items-center h-5">
<input id="isPublished" name="isPublished" type="checkbox"
class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded">
</div>
<div class="ml-3">
<label for="isPublished" class="text-sm font-medium text-gray-900">Опубликовать проект</label>
<p class="text-sm text-gray-500">Проект будет виден посетителям сайта</p>
</div>
</div>
<div class="flex items-start">
<div class="flex items-center h-5">
<input id="featured" name="featured" type="checkbox"
class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded">
</div>
<div class="ml-3">
<label for="featured" class="text-sm font-medium text-gray-900">Рекомендуемый проект</label>
<p class="text-sm text-gray-500">Проект будет показан в топе портфолио</p>
</div>
</div>
</div>
<!-- SEO Fields -->
<div class="border-t border-gray-200 pt-6">
<h5 class="text-sm font-medium text-gray-900 mb-4">SEO настройки (необязательно)</h5>
<div class="space-y-4">
<div>
<label for="seoTitle" class="block text-sm font-medium text-gray-700 mb-1">SEO заголовок</label>
<input type="text" name="seoTitle" id="seoTitle" maxlength="60"
placeholder="Будет использован заголовок проекта"
class="block w-full border-gray-300 rounded-lg shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm">
<p class="mt-1 text-sm text-gray-500">Рекомендуется до 60 символов</p>
</div>
<div>
<label for="seoDescription" class="block text-sm font-medium text-gray-700 mb-1">SEO описание</label>
<textarea name="seoDescription" id="seoDescription" rows="2" maxlength="160"
placeholder="Будет использовано краткое описание"
class="block w-full border-gray-300 rounded-lg shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm"></textarea>
<p class="mt-1 text-sm text-gray-500">Рекомендуется до 160 символов</p>
</div>
</div>
</div>
</div>
</div>
<!-- Submit Buttons -->
<div class="flex justify-between items-center pt-6 border-t border-gray-200">
<div class="flex space-x-3">
<button type="button" onclick="previewProject()" class="inline-flex items-center px-4 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50">
<i class="fas fa-eye mr-2"></i>
Предпросмотр
</button>
</div>
<div class="flex space-x-3">
<a href="/admin/portfolio" class="inline-flex items-center px-4 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50">
Отмена
</a>
<button type="button" onclick="saveDraft()" class="inline-flex items-center px-4 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50">
<i class="fas fa-save mr-2"></i>
Сохранить как черновик
</button>
<button type="submit" id="submitBtn" class="inline-flex items-center px-6 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
<i class="fas fa-check mr-2"></i>
Сохранить проект
</button>
</div>
</div>
</form>
</div>
<!-- Include Rich Text Editor -->
<link href="https://cdn.quilljs.com/1.3.6/quill.snow.css" rel="stylesheet">
<script src="https://cdn.quilljs.com/1.3.6/quill.min.js"></script>
<script>
let quillEditor;
let selectedTechnologies = [];
let uploadedImages = [];
// Initialize page
document.addEventListener('DOMContentLoaded', function() {
initializeRichTextEditor();
initializeImageUpload();
initializeTechnologyInput();
initializeFormValidation();
setupCharacterCounters();
});
// Initialize Rich Text Editor
function initializeRichTextEditor() {
quillEditor = new Quill('#descriptionEditor', {
theme: 'snow',
placeholder: 'Опишите проект: цели, задачи, решения, результаты...',
modules: {
toolbar: [
[{ 'header': [1, 2, 3, false] }],
['bold', 'italic', 'underline', 'strike'],
['blockquote', 'code-block'],
[{ 'list': 'ordered'}, { 'list': 'bullet' }],
[{ 'script': 'sub'}, { 'script': 'super' }],
[{ 'indent': '-1'}, { 'indent': '+1' }],
['link', 'image'],
['clean']
]
}
});
// Sync with hidden textarea
quillEditor.on('text-change', function() {
document.getElementById('description').value = quillEditor.root.innerHTML;
});
}
// Technology management
function initializeTechnologyInput() {
const input = document.getElementById('technologyInput');
input.addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
e.preventDefault();
addTechnology();
}
});
}
function addTechnology() {
const input = document.getElementById('technologyInput');
const technology = input.value.trim();
if (technology && !selectedTechnologies.includes(technology)) {
selectedTechnologies.push(technology);
input.value = '';
updateTechnologiesList();
updateTechnologiesInput();
}
}
function addTechnologyFromList(tech) {
if (!selectedTechnologies.includes(tech)) {
selectedTechnologies.push(tech);
updateTechnologiesList();
updateTechnologiesInput();
}
}
function removeTechnology(index) {
selectedTechnologies.splice(index, 1);
updateTechnologiesList();
updateTechnologiesInput();
}
function updateTechnologiesList() {
const container = document.getElementById('technologiesList');
container.innerHTML = selectedTechnologies.map((tech, index) => `
<span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-blue-100 text-blue-800">
${tech}
<button type="button" onclick="removeTechnology(${index})" class="ml-2 text-blue-600 hover:text-blue-800">
<i class="fas fa-times"></i>
</button>
</span>
`).join('');
}
function updateTechnologiesInput() {
document.getElementById('technologiesHidden').value = JSON.stringify(selectedTechnologies);
}
// Image upload functionality
function initializeImageUpload() {
const dropZone = document.getElementById('dropZone');
const fileInput = document.getElementById('images');
// Drag and drop functionality
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
dropZone.addEventListener(eventName, preventDefaults, false);
document.body.addEventListener(eventName, preventDefaults, false);
});
['dragenter', 'dragover'].forEach(eventName => {
dropZone.addEventListener(eventName, () => dropZone.classList.add('border-blue-500', 'bg-blue-50'), false);
});
['dragleave', 'drop'].forEach(eventName => {
dropZone.addEventListener(eventName, () => dropZone.classList.remove('border-blue-500', 'bg-blue-50'), false);
});
dropZone.addEventListener('drop', handleDrop, false);
fileInput.addEventListener('change', (e) => handleFiles(e.target.files), false);
}
function preventDefaults(e) {
e.preventDefault();
e.stopPropagation();
}
function handleDrop(e) {
const dt = e.dataTransfer;
const files = dt.files;
handleFiles(files);
}
async function handleFiles(files) {
const validFiles = Array.from(files).filter(file => {
if (!file.type.startsWith('image/')) {
showNotification('Можно загружать только изображения', 'error');
return false;
}
if (file.size > 10 * 1024 * 1024) {
showNotification(`Файл ${file.name} слишком большой (максимум 10MB)`, 'error');
return false;
}
return true;
});
if (validFiles.length === 0) return;
// Show loading state
showLoadingState();
for (const file of validFiles) {
await uploadSingleImage(file);
}
hideLoadingState();
updateImagePreview();
}
async function uploadSingleImage(file) {
return new Promise((resolve) => {
const reader = new FileReader();
reader.onload = function(e) {
const imageData = {
file: file,
url: e.target.result,
name: file.name,
size: file.size
};
uploadedImages.push(imageData);
resolve();
};
reader.readAsDataURL(file);
});
}
function updateImagePreview() {
const container = document.getElementById('imagePreview');
const previewContainer = document.getElementById('imagePreviewContainer');
if (uploadedImages.length === 0) {
previewContainer.style.display = 'none';
return;
}
previewContainer.style.display = 'block';
container.innerHTML = uploadedImages.map((img, index) => `
<div class="relative group" data-index="${index}">
<div class="aspect-w-16 aspect-h-9 rounded-lg overflow-hidden border-2 ${index === 0 ? 'border-blue-500' : 'border-gray-200'}">
<img src="${img.url}" alt="${img.name}" class="w-full h-32 object-cover">
</div>
${index === 0 ? '<div class="absolute top-2 left-2 bg-blue-600 text-white text-xs px-2 py-1 rounded">Главное</div>' : ''}
<div class="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity space-x-1">
${index > 0 ? '<button type="button" onclick="moveImage(' + index + ', -1)" class="bg-white text-gray-600 rounded-full w-6 h-6 flex items-center justify-center text-xs shadow hover:bg-gray-50" title="Переместить влево"><i class="fas fa-chevron-left"></i></button>' : ''}
${index < uploadedImages.length - 1 ? '<button type="button" onclick="moveImage(' + index + ', 1)" class="bg-white text-gray-600 rounded-full w-6 h-6 flex items-center justify-center text-xs shadow hover:bg-gray-50" title="Переместить вправо"><i class="fas fa-chevron-right"></i></button>' : ''}
<button type="button" onclick="removeImage(${index})" class="bg-red-600 text-white rounded-full w-6 h-6 flex items-center justify-center text-xs shadow hover:bg-red-700" title="Удалить">
<i class="fas fa-times"></i>
</button>
</div>
<div class="mt-2 text-xs text-gray-600 truncate">${img.name}</div>
</div>
`).join('');
}
function moveImage(index, direction) {
const newIndex = index + direction;
if (newIndex >= 0 && newIndex < uploadedImages.length) {
[uploadedImages[index], uploadedImages[newIndex]] = [uploadedImages[newIndex], uploadedImages[index]];
updateImagePreview();
}
}
function removeImage(index) {
uploadedImages.splice(index, 1);
updateImagePreview();
}
function clearAllImages() {
if (confirm('Удалить все загруженные изображения?')) {
uploadedImages = [];
updateImagePreview();
}
}
// Character counters
function setupCharacterCounters() {
const shortDescInput = document.getElementById('shortDescription');
const shortDescCounter = document.getElementById('shortDescCount');
shortDescInput.addEventListener('input', function() {
shortDescCounter.textContent = `${this.value.length}/200`;
});
}
// Form validation and submission
function initializeFormValidation() {
const form = document.getElementById('portfolioForm');
form.addEventListener('submit', handleFormSubmit);
}
async function handleFormSubmit(e) {
e.preventDefault();
// Validate required fields
if (!validateForm()) {
return;
}
const formData = new FormData();
// Add text fields
const textFields = ['title', 'shortDescription', 'category', 'clientName', 'demoUrl', 'githubUrl', 'seoTitle', 'seoDescription', 'duration'];
textFields.forEach(field => {
const value = document.getElementById(field)?.value;
if (value) formData.append(field, value);
});
// Add description from Quill editor
formData.append('description', quillEditor.root.innerHTML);
// Add technologies
formData.append('technologies', JSON.stringify(selectedTechnologies));
// Add checkboxes
formData.append('featured', document.getElementById('featured').checked);
formData.append('isPublished', document.getElementById('isPublished').checked);
// Add images
uploadedImages.forEach((img, index) => {
formData.append('images', img.file);
});
try {
showSubmitLoading();
const response = await fetch('/admin/portfolio/add', {
method: 'POST',
body: formData
});
const data = await response.json();
if (data.success) {
showNotification('Проект успешно создан!', 'success');
setTimeout(() => {
window.location.href = '/admin/portfolio';
}, 1500);
} else {
showNotification(data.message || 'Ошибка при создании проекта', 'error');
hideSubmitLoading();
}
} catch (error) {
console.error('Error:', error);
showNotification('Ошибка при создании проекта', 'error');
hideSubmitLoading();
}
}
function validateForm() {
const errors = [];
// Required fields validation
const title = document.getElementById('title').value.trim();
const shortDescription = document.getElementById('shortDescription').value.trim();
const category = document.getElementById('category').value;
const description = quillEditor.getText().trim();
if (!title) errors.push('Введите название проекта');
if (!shortDescription) errors.push('Введите краткое описание');
if (!category) errors.push('Выберите категорию');
if (!description) errors.push('Введите подробное описание');
if (selectedTechnologies.length === 0) errors.push('Добавьте хотя бы одну технологию');
if (uploadedImages.length === 0) errors.push('Загрузите хотя бы одно изображение');
if (errors.length > 0) {
showNotification('Заполните все обязательные поля:\n' + errors.join('\n'), 'error');
return false;
}
return true;
}
// Save as draft
async function saveDraft() {
// Set isPublished to false
document.getElementById('isPublished').checked = false;
// Submit form
await handleFormSubmit({ preventDefault: () => {} });
}
// Preview functionality
function previewProject() {
const title = document.getElementById('title').value;
const shortDescription = document.getElementById('shortDescription').value;
const category = document.getElementById('category').value;
if (!title || !shortDescription || !category) {
showNotification('Заполните основную информацию для предпросмотра', 'warning');
return;
}
// Open preview in new window/modal
const previewData = {
title,
shortDescription,
description: quillEditor.root.innerHTML,
category,
technologies: selectedTechnologies,
images: uploadedImages.map(img => img.url)
};
// Here you can implement preview modal or new window
console.log('Preview data:', previewData);
showNotification('Функция предпросмотра будет реализована позже', 'info');
}
// UI helpers
function showSubmitLoading() {
const btn = document.getElementById('submitBtn');
btn.innerHTML = '<i class="fas fa-spinner fa-spin mr-2"></i>Сохранение...';
btn.disabled = true;
}
function hideSubmitLoading() {
const btn = document.getElementById('submitBtn');
btn.innerHTML = '<i class="fas fa-check mr-2"></i>Сохранить проект';
btn.disabled = false;
}
function showLoadingState() {
const dropZone = document.getElementById('dropZone');
dropZone.innerHTML = '<div class="text-center"><i class="fas fa-spinner fa-spin text-2xl text-blue-600 mb-2"></i><p class="text-blue-600">Обработка изображений...</p></div>';
}
function hideLoadingState() {
const dropZone = document.getElementById('dropZone');
dropZone.innerHTML = `
<div class="space-y-4">
<div class="mx-auto w-16 h-16 text-gray-400">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"/>
</svg>
</div>
<div>
<label for="images" class="cursor-pointer">
<span class="text-blue-600 font-medium hover:text-blue-500">Загрузить изображения</span>
<input id="images" name="images" type="file" class="sr-only" multiple accept="image/*">
</label>
<span class="text-gray-600"> или перетащите файлы сюда</span>
</div>
<p class="text-sm text-gray-500">
PNG, JPG, WEBP до 10MB каждый. Первое изображение будет использоваться как главное.
</p>
</div>
`;
// Re-initialize file input
document.getElementById('images').addEventListener('change', (e) => handleFiles(e.target.files), false);
}
function showNotification(message, type = 'info') {
// Create notification
const notification = document.createElement('div');
notification.className = `fixed top-4 right-4 z-50 px-6 py-4 rounded-lg shadow-lg text-white max-w-md transform transition-all duration-300 translate-x-full`;
switch(type) {
case 'success':
notification.classList.add('bg-green-600');
break;
case 'error':
notification.classList.add('bg-red-600');
break;
case 'warning':
notification.classList.add('bg-yellow-600');
break;
case 'info':
default:
notification.classList.add('bg-blue-600');
break;
}
notification.innerHTML = `
<div class="flex items-start">
<i class="fas ${type === 'success' ? 'fa-check-circle' : type === 'error' ? 'fa-exclamation-circle' : type === 'warning' ? 'fa-exclamation-triangle' : 'fa-info-circle'} mr-3 mt-0.5"></i>
<div>
<div class="font-medium">${type.charAt(0).toUpperCase() + type.slice(1)}</div>
<div class="mt-1 text-sm opacity-90 whitespace-pre-line">${message}</div>
</div>
</div>
`;
document.body.appendChild(notification);
// Show notification
setTimeout(() => {
notification.classList.remove('translate-x-full');
}, 100);
// Hide notification
setTimeout(() => {
notification.classList.add('translate-x-full');
setTimeout(() => {
if (document.body.contains(notification)) {
document.body.removeChild(notification);
}
}, 300);
}, 5000);
}
</script>

View File

@@ -1,776 +0,0 @@
<!-- Add Portfolio Item -->
<div class="max-w-5xl mx-auto">
<div class="bg-white shadow rounded-lg">
<div class="px-6 py-4 border-b border-gray-200">
<div class="flex items-center justify-between">
<div>
<h3 class="text-xl font-semibold text-gray-900">
<i class="fas fa-plus-circle mr-2 text-blue-600"></i>
Добавить новый проект
</h3>
<p class="mt-1 text-sm text-gray-500">Заполните информацию о проекте для добавления в портфолио</p>
</div>
<div class="flex space-x-3">
<button type="button" onclick="saveDraft()" class="inline-flex items-center px-3 py-2 border border-gray-300 shadow-sm text-sm leading-4 font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
<i class="fas fa-save mr-2"></i>
Сохранить черновик
</button>
<a href="/admin/portfolio" class="inline-flex items-center px-3 py-2 border border-gray-300 shadow-sm text-sm leading-4 font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50">
<i class="fas fa-arrow-left mr-2"></i>
Назад к списку
</a>
</div>
</div>
</div>
<form id="portfolioForm" class="p-6">
<!-- Progress indicator -->
<div class="mb-8">
<div class="flex items-center">
<div class="flex items-center text-blue-600 relative">
<div class="rounded-full h-8 w-8 bg-blue-600 text-white flex items-center justify-center text-sm font-medium">1</div>
<span class="ml-3 text-sm font-medium text-blue-600">Основная информация</span>
</div>
<div class="flex-1 mx-4 h-1 bg-gray-200"></div>
<div class="flex items-center text-gray-500">
<div class="rounded-full h-8 w-8 bg-gray-200 text-gray-600 flex items-center justify-center text-sm font-medium">2</div>
<span class="ml-3 text-sm font-medium text-gray-500">Медиа и изображения</span>
</div>
<div class="flex-1 mx-4 h-1 bg-gray-200"></div>
<div class="flex items-center text-gray-500">
<div class="rounded-full h-8 w-8 bg-gray-200 text-gray-600 flex items-center justify-center text-sm font-medium">3</div>
<span class="ml-3 text-sm font-medium text-gray-500">Публикация</span>
</div>
</div>
</div>
<div class="space-y-8">
<!-- Шаг 1: Основная информация -->
<div class="bg-gray-50 rounded-lg p-6">
<h4 class="text-lg font-medium text-gray-900 mb-6">
<i class="fas fa-info-circle mr-2 text-blue-600"></i>
Основная информация о проекте
</h4>
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2">
<!-- Title -->
<div class="sm:col-span-2">
<label for="title" class="block text-sm font-medium text-gray-700 mb-2">
Название проекта *
<span class="text-gray-500 font-normal">(отображается в портфолио)</span>
</label>
<input type="text" name="title" id="title" required
placeholder="Например: Интернет-магазин электроники TechStore"
class="block w-full border-gray-300 rounded-lg shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm">
<p class="mt-1 text-sm text-gray-500">Используйте описательное название, которое четко передает суть проекта</p>
</div>
<!-- Short Description -->
<div class="sm:col-span-2">
<label for="shortDescription" class="block text-sm font-medium text-gray-700 mb-2">
Краткое описание *
<span class="text-gray-500 font-normal">(1-2 предложения)</span>
</label>
<textarea name="shortDescription" id="shortDescription" rows="2" required
placeholder="Современный интернет-магазин с удобной системой каталогов и интеграцией платежных систем"
class="block w-full border-gray-300 rounded-lg shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
maxlength="200"></textarea>
<div class="mt-1 flex justify-between text-sm text-gray-500">
<span>Отображается в превью проекта</span>
<span id="shortDescCount">0/200</span>
</div>
</div>
<!-- Category -->
<div>
<label for="category" class="block text-sm font-medium text-gray-700 mb-2">Категория *</label>
<select name="category" id="category" required
class="block w-full border-gray-300 rounded-lg shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm">
<option value="">Выберите категорию</option>
<% categories.forEach(cat => { %>
<option value="<%= cat %>"><%= getCategoryName(cat) %></option>
<% }); %>
</select>
</div>
<!-- Duration -->
<div>
<label for="duration" class="block text-sm font-medium text-gray-700 mb-2">
Длительность проекта
<span class="text-gray-500 font-normal">(в днях)</span>
</label>
<input type="number" name="duration" id="duration" min="1" max="365"
placeholder="30"
class="block w-full border-gray-300 rounded-lg shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm">
</div>
<!-- Client Name -->
<div>
<label for="clientName" class="block text-sm font-medium text-gray-700 mb-2">
Название клиента
<span class="text-gray-500 font-normal">(необязательно)</span>
</label>
<input type="text" name="clientName" id="clientName"
placeholder="ООО «ТехноСтор»"
class="block w-full border-gray-300 rounded-lg shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm">
</div>
<!-- Demo URL -->
<div>
<label for="demoUrl" class="block text-sm font-medium text-gray-700 mb-2">
Ссылка на демо
<span class="text-gray-500 font-normal">(живая версия)</span>
</label>
<input type="url" name="demoUrl" id="demoUrl"
placeholder="https://techstore.example.com"
class="block w-full border-gray-300 rounded-lg shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm">
</div>
<!-- GitHub URL -->
<div>
<label for="githubUrl" class="block text-sm font-medium text-gray-700 mb-2">
GitHub репозиторий
<span class="text-gray-500 font-normal">(если публичный)</span>
</label>
<input type="url" name="githubUrl" id="githubUrl"
placeholder="https://github.com/username/project"
class="block w-full border-gray-300 rounded-lg shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm">
</div>
</div>
</div>
<!-- Шаг 2: Детальное описание и технологии -->
<div class="bg-gray-50 rounded-lg p-6">
<h4 class="text-lg font-medium text-gray-900 mb-6">
<i class="fas fa-align-left mr-2 text-blue-600"></i>
Описание и технические детали
</h4>
<!-- Description -->
<div class="mb-6">
<label for="description" class="block text-sm font-medium text-gray-700 mb-2">
Подробное описание проекта *
</label>
<div id="descriptionEditor" class="min-h-40 border border-gray-300 rounded-lg"></div>
<textarea name="description" id="description" style="display: none;" required></textarea>
<p class="mt-2 text-sm text-gray-500">Опишите задачи, решения и результаты проекта</p>
</div>
<!-- Technologies -->
<div>
<label for="technologies" class="block text-sm font-medium text-gray-700 mb-2">
Используемые технологии *
</label>
<div class="relative">
<input type="text" id="technologyInput"
placeholder="Введите технологию и нажмите Enter"
class="block w-full border-gray-300 rounded-lg shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm">
<div class="absolute right-2 top-2">
<button type="button" onclick="addTechnology()" class="text-blue-600 hover:text-blue-800">
<i class="fas fa-plus"></i>
</button>
</div>
</div>
<div id="technologiesList" class="mt-3 flex flex-wrap gap-2"></div>
<input type="hidden" name="technologies" id="technologiesHidden">
<!-- Популярные технологии для быстрого добавления -->
<div class="mt-4">
<p class="text-sm text-gray-600 mb-2">Популярные технологии:</p>
<div class="flex flex-wrap gap-2">
<% const popularTechs = ['React', 'Vue.js', 'Node.js', 'Express.js', 'MongoDB', 'PostgreSQL', 'MySQL', 'JavaScript', 'TypeScript', 'HTML5', 'CSS3', 'Tailwind CSS', 'Bootstrap', 'Webpack', 'Docker', 'Git', 'AWS', 'Figma', 'Photoshop']; %>
<% popularTechs.forEach(tech => { %>
<button type="button" onclick="addTechnologyFromList('<%= tech %>')"
class="px-2 py-1 text-xs border border-gray-300 rounded-md text-gray-600 hover:bg-gray-100">
<%= tech %>
</button>
<% }); %>
</div>
</div>
</div>
</div>
<!-- Шаг 3: Медиа контент -->
<div class="bg-gray-50 rounded-lg p-6">
<h4 class="text-lg font-medium text-gray-900 mb-6">
<i class="fas fa-images mr-2 text-blue-600"></i>
Изображения и медиа контент
</h4>
<div class="space-y-6">
<!-- Main Image Upload -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-3">
Изображения проекта
<span class="text-red-500">*</span>
</label>
<div id="dropZone" class="relative border-2 border-dashed border-gray-300 rounded-lg p-8 text-center hover:border-blue-500 transition-colors">
<div class="space-y-4">
<div class="mx-auto w-16 h-16 text-gray-400">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"/>
</svg>
</div>
<div>
<label for="images" class="cursor-pointer">
<span class="text-blue-600 font-medium hover:text-blue-500">Загрузить изображения</span>
<input id="images" name="images" type="file" class="sr-only" multiple accept="image/*">
</label>
<span class="text-gray-600"> или перетащите файлы сюда</span>
</div>
<p class="text-sm text-gray-500">
PNG, JPG, WEBP до 10MB каждый. Первое изображение будет использоваться как главное.
</p>
</div>
</div>
</div>
<!-- Image Preview and Management -->
<div id="imagePreviewContainer" style="display: none;">
<div class="flex items-center justify-between mb-4">
<h5 class="text-sm font-medium text-gray-900">Загруженные изображения</h5>
<button type="button" onclick="clearAllImages()" class="text-red-600 hover:text-red-800 text-sm">
<i class="fas fa-trash mr-1"></i>
Удалить все
</button>
</div>
<div id="imagePreview" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4"></div>
<p class="mt-2 text-sm text-gray-500">
<i class="fas fa-info-circle mr-1"></i>
Перетаскивайте изображения для изменения порядка. Первое изображение будет главным.
</p>
</div>
</div>
</div>
<!-- Шаг 4: Настройки публикации -->
<div class="bg-gray-50 rounded-lg p-6">
<h4 class="text-lg font-medium text-gray-900 mb-6">
<i class="fas fa-cog mr-2 text-blue-600"></i>
Настройки публикации
</h4>
<div class="space-y-6">
<!-- Publication Options -->
<div class="grid grid-cols-1 sm:grid-cols-2 gap-6">
<div class="flex items-start">
<div class="flex items-center h-5">
<input id="isPublished" name="isPublished" type="checkbox"
class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded">
</div>
<div class="ml-3">
<label for="isPublished" class="text-sm font-medium text-gray-900">Опубликовать проект</label>
<p class="text-sm text-gray-500">Проект будет виден посетителям сайта</p>
</div>
</div>
<div class="flex items-start">
<div class="flex items-center h-5">
<input id="featured" name="featured" type="checkbox"
class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded">
</div>
<div class="ml-3">
<label for="featured" class="text-sm font-medium text-gray-900">Рекомендуемый проект</label>
<p class="text-sm text-gray-500">Проект будет показан в топе портфолио</p>
</div>
</div>
</div>
<!-- SEO Fields -->
<div class="border-t border-gray-200 pt-6">
<h5 class="text-sm font-medium text-gray-900 mb-4">SEO настройки (необязательно)</h5>
<div class="space-y-4">
<div>
<label for="seoTitle" class="block text-sm font-medium text-gray-700 mb-1">SEO заголовок</label>
<input type="text" name="seoTitle" id="seoTitle" maxlength="60"
placeholder="Будет использован заголовок проекта"
class="block w-full border-gray-300 rounded-lg shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm">
<p class="mt-1 text-sm text-gray-500">Рекомендуется до 60 символов</p>
</div>
<div>
<label for="seoDescription" class="block text-sm font-medium text-gray-700 mb-1">SEO описание</label>
<textarea name="seoDescription" id="seoDescription" rows="2" maxlength="160"
placeholder="Будет использовано краткое описание"
class="block w-full border-gray-300 rounded-lg shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm"></textarea>
<p class="mt-1 text-sm text-gray-500">Рекомендуется до 160 символов</p>
</div>
</div>
</div>
</div>
</div>
<!-- Submit Buttons -->
<div class="flex justify-between items-center pt-6 border-t border-gray-200">
<div class="flex space-x-3">
<button type="button" onclick="previewProject()" class="inline-flex items-center px-4 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50">
<i class="fas fa-eye mr-2"></i>
Предпросмотр
</button>
</div>
<div class="flex space-x-3">
<a href="/admin/portfolio" class="inline-flex items-center px-4 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50">
Отмена
</a>
<button type="button" onclick="saveDraft()" class="inline-flex items-center px-4 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50">
<i class="fas fa-save mr-2"></i>
Сохранить как черновик
</button>
<button type="submit" id="submitBtn" class="inline-flex items-center px-6 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
<i class="fas fa-check mr-2"></i>
Сохранить проект
</button>
</div>
</div>
</form>
</div>
<!-- Include Rich Text Editor -->
<link href="https://cdn.quilljs.com/1.3.6/quill.snow.css" rel="stylesheet">
<script src="https://cdn.quilljs.com/1.3.6/quill.min.js"></script>
<script>
let quillEditor;
let selectedTechnologies = [];
let uploadedImages = [];
// Initialize page
document.addEventListener('DOMContentLoaded', function() {
initializeRichTextEditor();
initializeImageUpload();
initializeTechnologyInput();
initializeFormValidation();
setupCharacterCounters();
});
// Initialize Rich Text Editor
function initializeRichTextEditor() {
quillEditor = new Quill('#descriptionEditor', {
theme: 'snow',
placeholder: 'Опишите проект: цели, задачи, решения, результаты...',
modules: {
toolbar: [
[{ 'header': [1, 2, 3, false] }],
['bold', 'italic', 'underline', 'strike'],
['blockquote', 'code-block'],
[{ 'list': 'ordered'}, { 'list': 'bullet' }],
[{ 'script': 'sub'}, { 'script': 'super' }],
[{ 'indent': '-1'}, { 'indent': '+1' }],
['link', 'image'],
['clean']
]
}
});
// Sync with hidden textarea
quillEditor.on('text-change', function() {
document.getElementById('description').value = quillEditor.root.innerHTML;
});
}
// Technology management
function initializeTechnologyInput() {
const input = document.getElementById('technologyInput');
input.addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
e.preventDefault();
addTechnology();
}
});
}
function addTechnology() {
const input = document.getElementById('technologyInput');
const technology = input.value.trim();
if (technology && !selectedTechnologies.includes(technology)) {
selectedTechnologies.push(technology);
input.value = '';
updateTechnologiesList();
updateTechnologiesInput();
}
}
function addTechnologyFromList(tech) {
if (!selectedTechnologies.includes(tech)) {
selectedTechnologies.push(tech);
updateTechnologiesList();
updateTechnologiesInput();
}
}
function removeTechnology(index) {
selectedTechnologies.splice(index, 1);
updateTechnologiesList();
updateTechnologiesInput();
}
function updateTechnologiesList() {
const container = document.getElementById('technologiesList');
container.innerHTML = selectedTechnologies.map((tech, index) => `
<span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-blue-100 text-blue-800">
${tech}
<button type="button" onclick="removeTechnology(${index})" class="ml-2 text-blue-600 hover:text-blue-800">
<i class="fas fa-times"></i>
</button>
</span>
`).join('');
}
function updateTechnologiesInput() {
document.getElementById('technologiesHidden').value = JSON.stringify(selectedTechnologies);
}
// Image upload functionality
function initializeImageUpload() {
const dropZone = document.getElementById('dropZone');
const fileInput = document.getElementById('images');
// Drag and drop functionality
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
dropZone.addEventListener(eventName, preventDefaults, false);
document.body.addEventListener(eventName, preventDefaults, false);
});
['dragenter', 'dragover'].forEach(eventName => {
dropZone.addEventListener(eventName, () => dropZone.classList.add('border-blue-500', 'bg-blue-50'), false);
});
['dragleave', 'drop'].forEach(eventName => {
dropZone.addEventListener(eventName, () => dropZone.classList.remove('border-blue-500', 'bg-blue-50'), false);
});
dropZone.addEventListener('drop', handleDrop, false);
fileInput.addEventListener('change', (e) => handleFiles(e.target.files), false);
}
function preventDefaults(e) {
e.preventDefault();
e.stopPropagation();
}
function handleDrop(e) {
const dt = e.dataTransfer;
const files = dt.files;
handleFiles(files);
}
async function handleFiles(files) {
const validFiles = Array.from(files).filter(file => {
if (!file.type.startsWith('image/')) {
showNotification('Можно загружать только изображения', 'error');
return false;
}
if (file.size > 10 * 1024 * 1024) {
showNotification(`Файл ${file.name} слишком большой (максимум 10MB)`, 'error');
return false;
}
return true;
});
if (validFiles.length === 0) return;
// Show loading state
showLoadingState();
for (const file of validFiles) {
await uploadSingleImage(file);
}
hideLoadingState();
updateImagePreview();
}
async function uploadSingleImage(file) {
return new Promise((resolve) => {
const reader = new FileReader();
reader.onload = function(e) {
const imageData = {
file: file,
url: e.target.result,
name: file.name,
size: file.size
};
uploadedImages.push(imageData);
resolve();
};
reader.readAsDataURL(file);
});
}
function updateImagePreview() {
const container = document.getElementById('imagePreview');
const previewContainer = document.getElementById('imagePreviewContainer');
if (uploadedImages.length === 0) {
previewContainer.style.display = 'none';
return;
}
previewContainer.style.display = 'block';
container.innerHTML = uploadedImages.map((img, index) => `
<div class="relative group" data-index="${index}">
<div class="aspect-w-16 aspect-h-9 rounded-lg overflow-hidden border-2 ${index === 0 ? 'border-blue-500' : 'border-gray-200'}">
<img src="${img.url}" alt="${img.name}" class="w-full h-32 object-cover">
</div>
${index === 0 ? '<div class="absolute top-2 left-2 bg-blue-600 text-white text-xs px-2 py-1 rounded">Главное</div>' : ''}
<div class="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity space-x-1">
${index > 0 ? '<button type="button" onclick="moveImage(' + index + ', -1)" class="bg-white text-gray-600 rounded-full w-6 h-6 flex items-center justify-center text-xs shadow hover:bg-gray-50" title="Переместить влево"><i class="fas fa-chevron-left"></i></button>' : ''}
${index < uploadedImages.length - 1 ? '<button type="button" onclick="moveImage(' + index + ', 1)" class="bg-white text-gray-600 rounded-full w-6 h-6 flex items-center justify-center text-xs shadow hover:bg-gray-50" title="Переместить вправо"><i class="fas fa-chevron-right"></i></button>' : ''}
<button type="button" onclick="removeImage(${index})" class="bg-red-600 text-white rounded-full w-6 h-6 flex items-center justify-center text-xs shadow hover:bg-red-700" title="Удалить">
<i class="fas fa-times"></i>
</button>
</div>
<div class="mt-2 text-xs text-gray-600 truncate">${img.name}</div>
</div>
`).join('');
}
function moveImage(index, direction) {
const newIndex = index + direction;
if (newIndex >= 0 && newIndex < uploadedImages.length) {
[uploadedImages[index], uploadedImages[newIndex]] = [uploadedImages[newIndex], uploadedImages[index]];
updateImagePreview();
}
}
function removeImage(index) {
uploadedImages.splice(index, 1);
updateImagePreview();
}
function clearAllImages() {
if (confirm('Удалить все загруженные изображения?')) {
uploadedImages = [];
updateImagePreview();
}
}
// Character counters
function setupCharacterCounters() {
const shortDescInput = document.getElementById('shortDescription');
const shortDescCounter = document.getElementById('shortDescCount');
shortDescInput.addEventListener('input', function() {
shortDescCounter.textContent = `${this.value.length}/200`;
});
}
// Form validation and submission
function initializeFormValidation() {
const form = document.getElementById('portfolioForm');
form.addEventListener('submit', handleFormSubmit);
}
async function handleFormSubmit(e) {
e.preventDefault();
// Validate required fields
if (!validateForm()) {
return;
}
const formData = new FormData();
// Add text fields
const textFields = ['title', 'shortDescription', 'category', 'clientName', 'demoUrl', 'githubUrl', 'seoTitle', 'seoDescription', 'duration'];
textFields.forEach(field => {
const value = document.getElementById(field)?.value;
if (value) formData.append(field, value);
});
// Add description from Quill editor
formData.append('description', quillEditor.root.innerHTML);
// Add technologies
formData.append('technologies', JSON.stringify(selectedTechnologies));
// Add checkboxes
formData.append('featured', document.getElementById('featured').checked);
formData.append('isPublished', document.getElementById('isPublished').checked);
// Add images
uploadedImages.forEach((img, index) => {
formData.append('images', img.file);
});
try {
showSubmitLoading();
const response = await fetch('/admin/portfolio/add', {
method: 'POST',
body: formData
});
const data = await response.json();
if (data.success) {
showNotification('Проект успешно создан!', 'success');
setTimeout(() => {
window.location.href = '/admin/portfolio';
}, 1500);
} else {
showNotification(data.message || 'Ошибка при создании проекта', 'error');
hideSubmitLoading();
}
} catch (error) {
console.error('Error:', error);
showNotification('Ошибка при создании проекта', 'error');
hideSubmitLoading();
}
}
function validateForm() {
const errors = [];
// Required fields validation
const title = document.getElementById('title').value.trim();
const shortDescription = document.getElementById('shortDescription').value.trim();
const category = document.getElementById('category').value;
const description = quillEditor.getText().trim();
if (!title) errors.push('Введите название проекта');
if (!shortDescription) errors.push('Введите краткое описание');
if (!category) errors.push('Выберите категорию');
if (!description) errors.push('Введите подробное описание');
if (selectedTechnologies.length === 0) errors.push('Добавьте хотя бы одну технологию');
if (uploadedImages.length === 0) errors.push('Загрузите хотя бы одно изображение');
if (errors.length > 0) {
showNotification('Заполните все обязательные поля:\n' + errors.join('\n'), 'error');
return false;
}
return true;
}
// Save as draft
async function saveDraft() {
// Set isPublished to false
document.getElementById('isPublished').checked = false;
// Submit form
await handleFormSubmit({ preventDefault: () => {} });
}
// Preview functionality
function previewProject() {
const title = document.getElementById('title').value;
const shortDescription = document.getElementById('shortDescription').value;
const category = document.getElementById('category').value;
if (!title || !shortDescription || !category) {
showNotification('Заполните основную информацию для предпросмотра', 'warning');
return;
}
// Open preview in new window/modal
const previewData = {
title,
shortDescription,
description: quillEditor.root.innerHTML,
category,
technologies: selectedTechnologies,
images: uploadedImages.map(img => img.url)
};
// Here you can implement preview modal or new window
console.log('Preview data:', previewData);
showNotification('Функция предпросмотра будет реализована позже', 'info');
}
// UI helpers
function showSubmitLoading() {
const btn = document.getElementById('submitBtn');
btn.innerHTML = '<i class="fas fa-spinner fa-spin mr-2"></i>Сохранение...';
btn.disabled = true;
}
function hideSubmitLoading() {
const btn = document.getElementById('submitBtn');
btn.innerHTML = '<i class="fas fa-check mr-2"></i>Сохранить проект';
btn.disabled = false;
}
function showLoadingState() {
const dropZone = document.getElementById('dropZone');
dropZone.innerHTML = '<div class="text-center"><i class="fas fa-spinner fa-spin text-2xl text-blue-600 mb-2"></i><p class="text-blue-600">Обработка изображений...</p></div>';
}
function hideLoadingState() {
const dropZone = document.getElementById('dropZone');
dropZone.innerHTML = `
<div class="space-y-4">
<div class="mx-auto w-16 h-16 text-gray-400">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"/>
</svg>
</div>
<div>
<label for="images" class="cursor-pointer">
<span class="text-blue-600 font-medium hover:text-blue-500">Загрузить изображения</span>
<input id="images" name="images" type="file" class="sr-only" multiple accept="image/*">
</label>
<span class="text-gray-600"> или перетащите файлы сюда</span>
</div>
<p class="text-sm text-gray-500">
PNG, JPG, WEBP до 10MB каждый. Первое изображение будет использоваться как главное.
</p>
</div>
`;
// Re-initialize file input
document.getElementById('images').addEventListener('change', (e) => handleFiles(e.target.files), false);
}
function showNotification(message, type = 'info') {
// Create notification
const notification = document.createElement('div');
notification.className = `fixed top-4 right-4 z-50 px-6 py-4 rounded-lg shadow-lg text-white max-w-md transform transition-all duration-300 translate-x-full`;
switch(type) {
case 'success':
notification.classList.add('bg-green-600');
break;
case 'error':
notification.classList.add('bg-red-600');
break;
case 'warning':
notification.classList.add('bg-yellow-600');
break;
case 'info':
default:
notification.classList.add('bg-blue-600');
break;
}
notification.innerHTML = `
<div class="flex items-start">
<i class="fas ${type === 'success' ? 'fa-check-circle' : type === 'error' ? 'fa-exclamation-circle' : type === 'warning' ? 'fa-exclamation-triangle' : 'fa-info-circle'} mr-3 mt-0.5"></i>
<div>
<div class="font-medium">${type.charAt(0).toUpperCase() + type.slice(1)}</div>
<div class="mt-1 text-sm opacity-90 whitespace-pre-line">${message}</div>
</div>
</div>
`;
document.body.appendChild(notification);
// Show notification
setTimeout(() => {
notification.classList.remove('translate-x-full');
}, 100);
// Hide notification
setTimeout(() => {
notification.classList.add('translate-x-full');
setTimeout(() => {
if (document.body.contains(notification)) {
document.body.removeChild(notification);
}
}, 300);
}, 5000);
}
</script>

View File

@@ -1,123 +0,0 @@
<!-- 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">
<h3 class="text-lg leading-6 font-medium text-gray-900">
<i class="fas fa-briefcase mr-2"></i>
Управление портфолио
</h3>
<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">
<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 (portfolio && portfolio.length > 0) { %>
<% portfolio.forEach(item => { %>
<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">
<% if (item.images && item.images.length > 0) { %>
<img class="h-10 w-10 rounded-full object-cover" src="<%= item.images[0].url %>" alt="">
<% } else { %>
<div class="h-10 w-10 rounded-full bg-gray-300 flex items-center justify-center">
<i class="fas fa-image text-gray-500"></i>
</div>
<% } %>
</div>
<div class="ml-4">
<div class="flex items-center">
<div class="text-sm font-medium text-gray-900"><%= item.title %></div>
<% if (item.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 (!item.isPublished) { %>
<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">
<%= item.category %> •
<%= new Date(item.createdAt).toLocaleDateString('ru-RU') %>
</div>
</div>
</div>
<div class="flex items-center space-x-2">
<a href="/portfolio/<%= item.id %>" target="_blank" class="text-blue-600 hover:text-blue-900">
<i class="fas fa-external-link-alt"></i>
</a>
<a href="/admin/portfolio/edit/<%= item.id %>" class="text-indigo-600 hover:text-indigo-900">
<i class="fas fa-edit"></i>
</a>
<button onclick="deletePortfolio('<%= item.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-briefcase text-4xl text-gray-300 mb-4"></i>
<p class="text-gray-500">Проекты не найдены</p>
<a href="/admin/portfolio/add" class="mt-2 inline-block bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md text-sm font-medium">
Добавить первый проект
</a>
</div>
</li>
<% } %>
</ul>
</div>
<!-- Pagination -->
<% if (pagination && pagination.total > 1) { %>
<div class="bg-white px-4 py-3 flex items-center justify-between border-t border-gray-200 sm:px-6">
<div class="flex-1 flex justify-between sm:hidden">
<% if (pagination.hasPrev) { %>
<a href="?page=<%= pagination.current - 1 %>" class="relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50">
Предыдущая
</a>
<% } %>
<% if (pagination.hasNext) { %>
<a href="?page=<%= pagination.current + 1 %>" class="ml-3 relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50">
Следующая
</a>
<% } %>
</div>
</div>
<% } %>
</div>
<script>
function deletePortfolio(id) {
if (confirm('Вы уверены, что хотите удалить этот проект?')) {
fetch(`/api/admin/portfolio/${id}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
location.reload();
} else {
alert('Ошибка при удалении проекта');
}
})
.catch(error => {
console.error('Error:', error);
alert('Ошибка при удалении проекта');
});
}
}
</script>

View File

@@ -1,123 +0,0 @@
<!-- 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">
<h3 class="text-lg leading-6 font-medium text-gray-900">
<i class="fas fa-briefcase mr-2"></i>
Управление портфолио
</h3>
<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">
<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 (portfolio && portfolio.length > 0) { %>
<% portfolio.forEach(item => { %>
<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">
<% if (item.images && item.images.length > 0) { %>
<img class="h-10 w-10 rounded-full object-cover" src="<%= item.images[0].url %>" alt="">
<% } else { %>
<div class="h-10 w-10 rounded-full bg-gray-300 flex items-center justify-center">
<i class="fas fa-image text-gray-500"></i>
</div>
<% } %>
</div>
<div class="ml-4">
<div class="flex items-center">
<div class="text-sm font-medium text-gray-900"><%= item.title %></div>
<% if (item.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 (!item.isPublished) { %>
<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">
<%= item.category %> •
<%= new Date(item.createdAt).toLocaleDateString('ru-RU') %>
</div>
</div>
</div>
<div class="flex items-center space-x-2">
<a href="/portfolio/<%= item.id %>" target="_blank" class="text-blue-600 hover:text-blue-900">
<i class="fas fa-external-link-alt"></i>
</a>
<a href="/admin/portfolio/edit/<%= item.id %>" class="text-indigo-600 hover:text-indigo-900">
<i class="fas fa-edit"></i>
</a>
<button onclick="deletePortfolio('<%= item.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-briefcase text-4xl text-gray-300 mb-4"></i>
<p class="text-gray-500">Проекты не найдены</p>
<a href="/admin/portfolio/add" class="mt-2 inline-block bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md text-sm font-medium">
Добавить первый проект
</a>
</div>
</li>
<% } %>
</ul>
</div>
<!-- Pagination -->
<% if (pagination && pagination.total > 1) { %>
<div class="bg-white px-4 py-3 flex items-center justify-between border-t border-gray-200 sm:px-6">
<div class="flex-1 flex justify-between sm:hidden">
<% if (pagination.hasPrev) { %>
<a href="?page=<%= pagination.current - 1 %>" class="relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50">
Предыдущая
</a>
<% } %>
<% if (pagination.hasNext) { %>
<a href="?page=<%= pagination.current + 1 %>" class="ml-3 relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50">
Следующая
</a>
<% } %>
</div>
</div>
<% } %>
</div>
<script>
function deletePortfolio(id) {
if (confirm('Вы уверены, что хотите удалить этот проект?')) {
fetch(`/api/admin/portfolio/${id}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
location.reload();
} else {
alert('Ошибка при удалении проекта');
}
})
.catch(error => {
console.error('Error:', error);
alert('Ошибка при удалении проекта');
});
}
}
</script>

View File

@@ -1,147 +0,0 @@
<!-- Portfolio List -->
<div class="bg-white shadow rounded-lg">
<div class="px-4 py-5 sm:px-6 border-b border-gray-200">
<div class="flex items-center justify-between">
<div>
<h3 class="text-lg leading-6 font-medium text-gray-900">
<i class="fas fa-briefcase mr-2"></i>
Управление портфолио
</h3>
<p class="mt-1 text-sm text-gray-500">
Всего проектов: <%= portfolio ? portfolio.length : 0 %>
</p>
</div>
<div class="flex space-x-3">
<div class="flex rounded-md shadow-sm">
<input type="text" id="searchInput" placeholder="Поиск проектов..."
class="block w-full rounded-l-md border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm">
<button type="button" onclick="searchProjects()"
class="relative -ml-px inline-flex items-center px-3 py-2 rounded-r-md border border-l-0 border-gray-300 bg-gray-50 text-gray-500 hover:bg-gray-100 focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500">
<i class="fas fa-search"></i>
</button>
</div>
<select id="categoryFilter" onchange="filterByCategory()" class="rounded-md border-gray-300 py-2 pl-3 pr-10 text-base focus:border-indigo-500 focus:outline-none focus:ring-indigo-500 sm:text-sm">
<option value="">Все категории</option>
<option value="web-development">Веб-разработка</option>
<option value="mobile-app">Мобильное приложение</option>
<option value="ui-ux-design">UI/UX дизайн</option>
<option value="e-commerce">E-commerce</option>
<option value="enterprise">Корпоративное</option>
<option value="other">Другое</option>
</select>
<a href="/admin/portfolio/add" class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md text-sm font-medium inline-flex items-center">
<i class="fas fa-plus mr-2"></i>
Добавить проект
</a>
</div>
</div>
</div>
<div class="bg-white shadow overflow-hidden sm:rounded-md">
<ul role="list" class="divide-y divide-gray-200">
<% if (portfolio && portfolio.length > 0) { %>
<% portfolio.forEach(item => { %>
<li>
<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">
<% if (item.images && item.images.length > 0) { %>
<img class="h-10 w-10 rounded-full object-cover" src="<%= item.images[0].url %>" alt="">
<% } else { %>
<div class="h-10 w-10 rounded-full bg-gray-300 flex items-center justify-center">
<i class="fas fa-image text-gray-500"></i>
</div>
<% } %>
</div>
<div class="ml-4">
<div class="flex items-center">
<div class="text-sm font-medium text-gray-900"><%= item.title %></div>
<% if (item.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 (!item.isPublished) { %>
<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">
<%= item.category %> •
<%= new Date(item.createdAt).toLocaleDateString('ru-RU') %>
</div>
</div>
</div>
<div class="flex items-center space-x-2">
<a href="/portfolio/<%= item.id %>" target="_blank" class="text-blue-600 hover:text-blue-900">
<i class="fas fa-external-link-alt"></i>
</a>
<a href="/admin/portfolio/edit/<%= item.id %>" class="text-indigo-600 hover:text-indigo-900">
<i class="fas fa-edit"></i>
</a>
<button onclick="deletePortfolio('<%= item.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-briefcase text-4xl text-gray-300 mb-4"></i>
<p class="text-gray-500">Проекты не найдены</p>
<a href="/admin/portfolio/add" class="mt-2 inline-block bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md text-sm font-medium">
Добавить первый проект
</a>
</div>
</li>
<% } %>
</ul>
</div>
<!-- Pagination -->
<% if (pagination && pagination.total > 1) { %>
<div class="bg-white px-4 py-3 flex items-center justify-between border-t border-gray-200 sm:px-6">
<div class="flex-1 flex justify-between sm:hidden">
<% if (pagination.hasPrev) { %>
<a href="?page=<%= pagination.current - 1 %>" class="relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50">
Предыдущая
</a>
<% } %>
<% if (pagination.hasNext) { %>
<a href="?page=<%= pagination.current + 1 %>" class="ml-3 relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50">
Следующая
</a>
<% } %>
</div>
</div>
<% } %>
</div>
<script>
function deletePortfolio(id) {
if (confirm('Вы уверены, что хотите удалить этот проект?')) {
fetch(`/api/admin/portfolio/${id}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
location.reload();
} else {
alert('Ошибка при удалении проекта');
}
})
.catch(error => {
console.error('Error:', error);
alert('Ошибка при удалении проекта');
});
}
}
</script>

View File

@@ -1,186 +0,0 @@
<!-- Portfolio List -->
<div class="bg-white shadow rounded-lg">
<div class="px-4 py-5 sm:px-6 border-b border-gray-200">
<div class="flex items-center justify-between">
<div>
<h3 class="text-lg leading-6 font-medium text-gray-900">
<i class="fas fa-briefcase mr-2"></i>
Управление портфолио
</h3>
<p class="mt-1 text-sm text-gray-500">
Всего проектов: <%= portfolio ? portfolio.length : 0 %>
</p>
</div>
<div class="flex space-x-3">
<div class="flex rounded-md shadow-sm">
<input type="text" id="searchInput" placeholder="Поиск проектов..."
class="block w-full rounded-l-md border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm">
<button type="button" onclick="searchProjects()"
class="relative -ml-px inline-flex items-center px-3 py-2 rounded-r-md border border-l-0 border-gray-300 bg-gray-50 text-gray-500 hover:bg-gray-100 focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500">
<i class="fas fa-search"></i>
</button>
</div>
<select id="categoryFilter" onchange="filterByCategory()" class="rounded-md border-gray-300 py-2 pl-3 pr-10 text-base focus:border-indigo-500 focus:outline-none focus:ring-indigo-500 sm:text-sm">
<option value="">Все категории</option>
<option value="web-development">Веб-разработка</option>
<option value="mobile-app">Мобильное приложение</option>
<option value="ui-ux-design">UI/UX дизайн</option>
<option value="e-commerce">E-commerce</option>
<option value="enterprise">Корпоративное</option>
<option value="other">Другое</option>
</select>
<a href="/admin/portfolio/add" class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md text-sm font-medium inline-flex items-center">
<i class="fas fa-plus mr-2"></i>
Добавить проект
</a>
</div>
</div>
</div>
<div class="bg-white shadow overflow-hidden sm:rounded-md">
<ul role="list" class="divide-y divide-gray-200">
<% if (portfolio && portfolio.length > 0) { %>
<% portfolio.forEach(item => { %>
<li class="portfolio-item" data-category="<%= item.category %>" data-title="<%= item.title.toLowerCase() %>">
<div class="px-4 py-4">
<div class="flex items-start justify-between">
<div class="flex items-start space-x-4">
<div class="flex-shrink-0">
<% if (item.images && item.images.length > 0) { %>
<img class="h-16 w-16 rounded-lg object-cover border border-gray-200" src="<%= item.images[0].url %>" alt="<%= item.title %>">
<% } else { %>
<div class="h-16 w-16 rounded-lg bg-gray-100 border border-gray-200 flex items-center justify-center">
<i class="fas fa-image text-gray-400 text-xl"></i>
</div>
<% } %>
</div>
<div class="min-w-0 flex-1">
<div class="flex items-center">
<h4 class="text-base font-medium text-gray-900 truncate"><%= item.title %></h4>
<div class="ml-3 flex items-center space-x-2">
<% if (item.featured) { %>
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800">
<i class="fas fa-star mr-1"></i>
Рекомендуемое
</span>
<% } %>
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium <%= item.isPublished ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800' %>">
<i class="fas <%= item.isPublished ? 'fa-check-circle' : 'fa-clock' %> mr-1"></i>
<%= item.isPublished ? 'Опубликовано' : 'Черновик' %>
</span>
</div>
</div>
<p class="mt-1 text-sm text-gray-600 line-clamp-2"><%= item.shortDescription || 'Описание не указано' %></p>
<div class="mt-2 flex items-center text-sm text-gray-500 space-x-4">
<div class="flex items-center">
<i class="fas fa-folder mr-1"></i>
<span class="capitalize"><%= item.category.replace('-', ' ') %></span>
</div>
<div class="flex items-center">
<i class="fas fa-calendar mr-1"></i>
<%= new Date(item.createdAt).toLocaleDateString('ru-RU') %>
</div>
<% if (item.viewCount && item.viewCount > 0) { %>
<div class="flex items-center">
<i class="fas fa-eye mr-1"></i>
<%= item.viewCount %> просмотров
</div>
<% } %>
<% if (item.technologies && item.technologies.length > 0) { %>
<div class="flex items-center">
<i class="fas fa-code mr-1"></i>
<%= item.technologies.slice(0, 2).join(', ') %><%= item.technologies.length > 2 ? '...' : '' %>
</div>
<% } %>
</div>
</div>
</div>
<div class="flex items-center space-x-1 ml-4">
<% if (item.isPublished) { %>
<a href="/portfolio/<%= item.id %>" target="_blank"
class="inline-flex items-center justify-center w-8 h-8 rounded-md text-gray-400 hover:text-blue-600 hover:bg-blue-50 transition-colors">
<i class="fas fa-external-link-alt text-sm"></i>
</a>
<% } %>
<button onclick="togglePublish('<%= item.id %>', <%= item.isPublished %>)"
class="inline-flex items-center justify-center w-8 h-8 rounded-md text-gray-400 hover:text-green-600 hover:bg-green-50 transition-colors"
title="<%= item.isPublished ? 'Скрыть' : 'Опубликовать' %>">
<i class="fas <%= item.isPublished ? 'fa-eye-slash' : 'fa-eye' %> text-sm"></i>
</button>
<a href="/admin/portfolio/edit/<%= item.id %>"
class="inline-flex items-center justify-center w-8 h-8 rounded-md text-gray-400 hover:text-indigo-600 hover:bg-indigo-50 transition-colors"
title="Редактировать">
<i class="fas fa-edit text-sm"></i>
</a>
<button onclick="duplicatePortfolio('<%= item.id %>')"
class="inline-flex items-center justify-center w-8 h-8 rounded-md text-gray-400 hover:text-yellow-600 hover:bg-yellow-50 transition-colors"
title="Дублировать">
<i class="fas fa-copy text-sm"></i>
</button>
<button onclick="deletePortfolio('<%= item.id %>', '<%= item.title %>')"
class="inline-flex items-center justify-center w-8 h-8 rounded-md text-gray-400 hover:text-red-600 hover:bg-red-50 transition-colors"
title="Удалить">
<i class="fas fa-trash text-sm"></i>
</button>
</div>
</div>
</div>
</li>
<% }) %>
<% } else { %>
<li>
<div class="px-4 py-8 text-center">
<i class="fas fa-briefcase text-4xl text-gray-300 mb-4"></i>
<p class="text-gray-500">Проекты не найдены</p>
<a href="/admin/portfolio/add" class="mt-2 inline-block bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md text-sm font-medium">
Добавить первый проект
</a>
</div>
</li>
<% } %>
</ul>
</div>
<!-- Pagination -->
<% if (pagination && pagination.total > 1) { %>
<div class="bg-white px-4 py-3 flex items-center justify-between border-t border-gray-200 sm:px-6">
<div class="flex-1 flex justify-between sm:hidden">
<% if (pagination.hasPrev) { %>
<a href="?page=<%= pagination.current - 1 %>" class="relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50">
Предыдущая
</a>
<% } %>
<% if (pagination.hasNext) { %>
<a href="?page=<%= pagination.current + 1 %>" class="ml-3 relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50">
Следующая
</a>
<% } %>
</div>
</div>
<% } %>
</div>
<script>
function deletePortfolio(id) {
if (confirm('Вы уверены, что хотите удалить этот проект?')) {
fetch(`/api/admin/portfolio/${id}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
location.reload();
} else {
alert('Ошибка при удалении проекта');
}
})
.catch(error => {
console.error('Error:', error);
alert('Ошибка при удалении проекта');
});
}
}
</script>

View File

@@ -1,186 +0,0 @@
<!-- Portfolio List -->
<div class="bg-white shadow rounded-lg">
<div class="px-4 py-5 sm:px-6 border-b border-gray-200">
<div class="flex items-center justify-between">
<div>
<h3 class="text-lg leading-6 font-medium text-gray-900">
<i class="fas fa-briefcase mr-2"></i>
Управление портфолио
</h3>
<p class="mt-1 text-sm text-gray-500">
Всего проектов: <%= portfolio ? portfolio.length : 0 %>
</p>
</div>
<div class="flex space-x-3">
<div class="flex rounded-md shadow-sm">
<input type="text" id="searchInput" placeholder="Поиск проектов..."
class="block w-full rounded-l-md border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm">
<button type="button" onclick="searchProjects()"
class="relative -ml-px inline-flex items-center px-3 py-2 rounded-r-md border border-l-0 border-gray-300 bg-gray-50 text-gray-500 hover:bg-gray-100 focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500">
<i class="fas fa-search"></i>
</button>
</div>
<select id="categoryFilter" onchange="filterByCategory()" class="rounded-md border-gray-300 py-2 pl-3 pr-10 text-base focus:border-indigo-500 focus:outline-none focus:ring-indigo-500 sm:text-sm">
<option value="">Все категории</option>
<option value="web-development">Веб-разработка</option>
<option value="mobile-app">Мобильное приложение</option>
<option value="ui-ux-design">UI/UX дизайн</option>
<option value="e-commerce">E-commerce</option>
<option value="enterprise">Корпоративное</option>
<option value="other">Другое</option>
</select>
<a href="/admin/portfolio/add" class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md text-sm font-medium inline-flex items-center">
<i class="fas fa-plus mr-2"></i>
Добавить проект
</a>
</div>
</div>
</div>
<div class="bg-white shadow overflow-hidden sm:rounded-md">
<ul role="list" class="divide-y divide-gray-200">
<% if (portfolio && portfolio.length > 0) { %>
<% portfolio.forEach(item => { %>
<li class="portfolio-item" data-category="<%= item.category %>" data-title="<%= item.title.toLowerCase() %>">
<div class="px-4 py-4">
<div class="flex items-start justify-between">
<div class="flex items-start space-x-4">
<div class="flex-shrink-0">
<% if (item.images && item.images.length > 0) { %>
<img class="h-16 w-16 rounded-lg object-cover border border-gray-200" src="<%= item.images[0].url %>" alt="<%= item.title %>">
<% } else { %>
<div class="h-16 w-16 rounded-lg bg-gray-100 border border-gray-200 flex items-center justify-center">
<i class="fas fa-image text-gray-400 text-xl"></i>
</div>
<% } %>
</div>
<div class="min-w-0 flex-1">
<div class="flex items-center">
<h4 class="text-base font-medium text-gray-900 truncate"><%= item.title %></h4>
<div class="ml-3 flex items-center space-x-2">
<% if (item.featured) { %>
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800">
<i class="fas fa-star mr-1"></i>
Рекомендуемое
</span>
<% } %>
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium <%= item.isPublished ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800' %>">
<i class="fas <%= item.isPublished ? 'fa-check-circle' : 'fa-clock' %> mr-1"></i>
<%= item.isPublished ? 'Опубликовано' : 'Черновик' %>
</span>
</div>
</div>
<p class="mt-1 text-sm text-gray-600 line-clamp-2"><%= item.shortDescription || 'Описание не указано' %></p>
<div class="mt-2 flex items-center text-sm text-gray-500 space-x-4">
<div class="flex items-center">
<i class="fas fa-folder mr-1"></i>
<span class="capitalize"><%= item.category.replace('-', ' ') %></span>
</div>
<div class="flex items-center">
<i class="fas fa-calendar mr-1"></i>
<%= new Date(item.createdAt).toLocaleDateString('ru-RU') %>
</div>
<% if (item.viewCount && item.viewCount > 0) { %>
<div class="flex items-center">
<i class="fas fa-eye mr-1"></i>
<%= item.viewCount %> просмотров
</div>
<% } %>
<% if (item.technologies && item.technologies.length > 0) { %>
<div class="flex items-center">
<i class="fas fa-code mr-1"></i>
<%= item.technologies.slice(0, 2).join(', ') %><%= item.technologies.length > 2 ? '...' : '' %>
</div>
<% } %>
</div>
</div>
</div>
<div class="flex items-center space-x-1 ml-4">
<% if (item.isPublished) { %>
<a href="/portfolio/<%= item.id %>" target="_blank"
class="inline-flex items-center justify-center w-8 h-8 rounded-md text-gray-400 hover:text-blue-600 hover:bg-blue-50 transition-colors">
<i class="fas fa-external-link-alt text-sm"></i>
</a>
<% } %>
<button onclick="togglePublish('<%= item.id %>', '<%= item.isPublished %>')"
class="inline-flex items-center justify-center w-8 h-8 rounded-md text-gray-400 hover:text-green-600 hover:bg-green-50 transition-colors"
title="<%= item.isPublished ? 'Скрыть' : 'Опубликовать' %>">
<i class="fas <%= item.isPublished ? 'fa-eye-slash' : 'fa-eye' %> text-sm"></i>
</button>
<a href="/admin/portfolio/edit/<%= item.id %>"
class="inline-flex items-center justify-center w-8 h-8 rounded-md text-gray-400 hover:text-indigo-600 hover:bg-indigo-50 transition-colors"
title="Редактировать">
<i class="fas fa-edit text-sm"></i>
</a>
<button onclick="duplicatePortfolio('<%= item.id %>')"
class="inline-flex items-center justify-center w-8 h-8 rounded-md text-gray-400 hover:text-yellow-600 hover:bg-yellow-50 transition-colors"
title="Дублировать">
<i class="fas fa-copy text-sm"></i>
</button>
<button onclick="deletePortfolio('<%= item.id %>', '<%= item.title %>')"
class="inline-flex items-center justify-center w-8 h-8 rounded-md text-gray-400 hover:text-red-600 hover:bg-red-50 transition-colors"
title="Удалить">
<i class="fas fa-trash text-sm"></i>
</button>
</div>
</div>
</div>
</li>
<% }) %>
<% } else { %>
<li>
<div class="px-4 py-8 text-center">
<i class="fas fa-briefcase text-4xl text-gray-300 mb-4"></i>
<p class="text-gray-500">Проекты не найдены</p>
<a href="/admin/portfolio/add" class="mt-2 inline-block bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md text-sm font-medium">
Добавить первый проект
</a>
</div>
</li>
<% } %>
</ul>
</div>
<!-- Pagination -->
<% if (pagination && pagination.total > 1) { %>
<div class="bg-white px-4 py-3 flex items-center justify-between border-t border-gray-200 sm:px-6">
<div class="flex-1 flex justify-between sm:hidden">
<% if (pagination.hasPrev) { %>
<a href="?page=<%= pagination.current - 1 %>" class="relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50">
Предыдущая
</a>
<% } %>
<% if (pagination.hasNext) { %>
<a href="?page=<%= pagination.current + 1 %>" class="ml-3 relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50">
Следующая
</a>
<% } %>
</div>
</div>
<% } %>
</div>
<script>
function deletePortfolio(id) {
if (confirm('Вы уверены, что хотите удалить этот проект?')) {
fetch(`/api/admin/portfolio/${id}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
location.reload();
} else {
alert('Ошибка при удалении проекта');
}
})
.catch(error => {
console.error('Error:', error);
alert('Ошибка при удалении проекта');
});
}
}
</script>

View File

@@ -1,358 +0,0 @@
<!-- Portfolio List -->
<div class="bg-white shadow rounded-lg">
<div class="px-4 py-5 sm:px-6 border-b border-gray-200">
<div class="flex items-center justify-between">
<div>
<h3 class="text-lg leading-6 font-medium text-gray-900">
<i class="fas fa-briefcase mr-2"></i>
Управление портфолио
</h3>
<p class="mt-1 text-sm text-gray-500">
Всего проектов: <%= portfolio ? portfolio.length : 0 %>
</p>
</div>
<div class="flex space-x-3">
<div class="flex rounded-md shadow-sm">
<input type="text" id="searchInput" placeholder="Поиск проектов..."
class="block w-full rounded-l-md border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm">
<button type="button" onclick="searchProjects()"
class="relative -ml-px inline-flex items-center px-3 py-2 rounded-r-md border border-l-0 border-gray-300 bg-gray-50 text-gray-500 hover:bg-gray-100 focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500">
<i class="fas fa-search"></i>
</button>
</div>
<select id="categoryFilter" onchange="filterByCategory()" class="rounded-md border-gray-300 py-2 pl-3 pr-10 text-base focus:border-indigo-500 focus:outline-none focus:ring-indigo-500 sm:text-sm">
<option value="">Все категории</option>
<option value="web-development">Веб-разработка</option>
<option value="mobile-app">Мобильное приложение</option>
<option value="ui-ux-design">UI/UX дизайн</option>
<option value="e-commerce">E-commerce</option>
<option value="enterprise">Корпоративное</option>
<option value="other">Другое</option>
</select>
<a href="/admin/portfolio/add" class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md text-sm font-medium inline-flex items-center">
<i class="fas fa-plus mr-2"></i>
Добавить проект
</a>
</div>
</div>
</div>
<div class="bg-white shadow overflow-hidden sm:rounded-md">
<ul role="list" class="divide-y divide-gray-200">
<% if (portfolio && portfolio.length > 0) { %>
<% portfolio.forEach(item => { %>
<li class="portfolio-item" data-category="<%= item.category %>" data-title="<%= item.title.toLowerCase() %>">
<div class="px-4 py-4">
<div class="flex items-start justify-between">
<div class="flex items-start space-x-4">
<div class="flex-shrink-0">
<% if (item.images && item.images.length > 0) { %>
<img class="h-16 w-16 rounded-lg object-cover border border-gray-200" src="<%= item.images[0].url %>" alt="<%= item.title %>">
<% } else { %>
<div class="h-16 w-16 rounded-lg bg-gray-100 border border-gray-200 flex items-center justify-center">
<i class="fas fa-image text-gray-400 text-xl"></i>
</div>
<% } %>
</div>
<div class="min-w-0 flex-1">
<div class="flex items-center">
<h4 class="text-base font-medium text-gray-900 truncate"><%= item.title %></h4>
<div class="ml-3 flex items-center space-x-2">
<% if (item.featured) { %>
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800">
<i class="fas fa-star mr-1"></i>
Рекомендуемое
</span>
<% } %>
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium <%= item.isPublished ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800' %>">
<i class="fas <%= item.isPublished ? 'fa-check-circle' : 'fa-clock' %> mr-1"></i>
<%= item.isPublished ? 'Опубликовано' : 'Черновик' %>
</span>
</div>
</div>
<p class="mt-1 text-sm text-gray-600 line-clamp-2"><%= item.shortDescription || 'Описание не указано' %></p>
<div class="mt-2 flex items-center text-sm text-gray-500 space-x-4">
<div class="flex items-center">
<i class="fas fa-folder mr-1"></i>
<span class="capitalize"><%= item.category.replace('-', ' ') %></span>
</div>
<div class="flex items-center">
<i class="fas fa-calendar mr-1"></i>
<%= new Date(item.createdAt).toLocaleDateString('ru-RU') %>
</div>
<% if (item.viewCount && item.viewCount > 0) { %>
<div class="flex items-center">
<i class="fas fa-eye mr-1"></i>
<%= item.viewCount %> просмотров
</div>
<% } %>
<% if (item.technologies && item.technologies.length > 0) { %>
<div class="flex items-center">
<i class="fas fa-code mr-1"></i>
<%= item.technologies.slice(0, 2).join(', ') %><%= item.technologies.length > 2 ? '...' : '' %>
</div>
<% } %>
</div>
</div>
</div>
<div class="flex items-center space-x-1 ml-4">
<% if (item.isPublished) { %>
<a href="/portfolio/<%= item.id %>" target="_blank"
class="inline-flex items-center justify-center w-8 h-8 rounded-md text-gray-400 hover:text-blue-600 hover:bg-blue-50 transition-colors">
<i class="fas fa-external-link-alt text-sm"></i>
</a>
<% } %>
<button onclick="togglePublish('<%= item.id %>', '<%= item.isPublished %>')"
class="inline-flex items-center justify-center w-8 h-8 rounded-md text-gray-400 hover:text-green-600 hover:bg-green-50 transition-colors"
title="<%= item.isPublished ? 'Скрыть' : 'Опубликовать' %>">
<i class="fas <%= item.isPublished ? 'fa-eye-slash' : 'fa-eye' %> text-sm"></i>
</button>
<a href="/admin/portfolio/edit/<%= item.id %>"
class="inline-flex items-center justify-center w-8 h-8 rounded-md text-gray-400 hover:text-indigo-600 hover:bg-indigo-50 transition-colors"
title="Редактировать">
<i class="fas fa-edit text-sm"></i>
</a>
<button onclick="duplicatePortfolio('<%= item.id %>')"
class="inline-flex items-center justify-center w-8 h-8 rounded-md text-gray-400 hover:text-yellow-600 hover:bg-yellow-50 transition-colors"
title="Дублировать">
<i class="fas fa-copy text-sm"></i>
</button>
<button onclick="deletePortfolio('<%= item.id %>', '<%= item.title %>')"
class="inline-flex items-center justify-center w-8 h-8 rounded-md text-gray-400 hover:text-red-600 hover:bg-red-50 transition-colors"
title="Удалить">
<i class="fas fa-trash text-sm"></i>
</button>
</div>
</div>
</div>
</li>
<% }) %>
<% } else { %>
<li>
<div class="px-4 py-8 text-center">
<i class="fas fa-briefcase text-4xl text-gray-300 mb-4"></i>
<p class="text-gray-500">Проекты не найдены</p>
<a href="/admin/portfolio/add" class="mt-2 inline-block bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md text-sm font-medium">
Добавить первый проект
</a>
</div>
</li>
<% } %>
</ul>
</div>
<!-- Pagination -->
<% if (pagination && pagination.total > 1) { %>
<div class="bg-white px-4 py-3 flex items-center justify-between border-t border-gray-200 sm:px-6">
<div class="flex-1 flex justify-between sm:hidden">
<% if (pagination.hasPrev) { %>
<a href="?page=<%= pagination.current - 1 %>" class="relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50">
Предыдущая
</a>
<% } %>
<% if (pagination.hasNext) { %>
<a href="?page=<%= pagination.current + 1 %>" class="ml-3 relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50">
Следующая
</a>
<% } %>
</div>
</div>
<% } %>
</div>
<script>
function deletePortfolio(id, title) {
if (confirm(`Вы уверены, что хотите удалить проект "${title}"?\n\nЭто действие нельзя отменить.`)) {
const button = event.target.closest('button');
const originalContent = button.innerHTML;
button.innerHTML = '<i class="fas fa-spinner fa-spin text-sm"></i>';
button.disabled = true;
fetch(`/admin/portfolio/${id}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
showNotification('Проект успешно удален', 'success');
// Плавное удаление элемента
const listItem = button.closest('li');
listItem.style.opacity = '0.5';
listItem.style.transform = 'scale(0.95)';
setTimeout(() => {
listItem.remove();
updateProjectCount();
}, 300);
} else {
showNotification(data.message || 'Ошибка при удалении проекта', 'error');
button.innerHTML = originalContent;
button.disabled = false;
}
})
.catch(error => {
console.error('Error:', error);
showNotification('Ошибка при удалении проекта', 'error');
button.innerHTML = originalContent;
button.disabled = false;
});
}
}
function togglePublish(id, currentStatus) {
const button = event.target.closest('button');
const originalContent = button.innerHTML;
button.innerHTML = '<i class="fas fa-spinner fa-spin text-sm"></i>';
button.disabled = true;
fetch(`/admin/portfolio/${id}/toggle-publish`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
showNotification(data.message, 'success');
// Обновляем интерфейс
const listItem = button.closest('li');
const statusSpan = listItem.querySelector('.inline-flex');
const newStatus = data.isPublished;
// Обновляем иконку кнопки
button.innerHTML = `<i class="fas ${newStatus ? 'fa-eye-slash' : 'fa-eye'} text-sm"></i>`;
button.title = newStatus ? 'Скрыть' : 'Опубликовать';
// Обновляем статус
statusSpan.className = `inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${newStatus ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800'}`;
statusSpan.innerHTML = `<i class="fas ${newStatus ? 'fa-check-circle' : 'fa-clock'} mr-1"></i>${newStatus ? 'Опубликовано' : 'Черновик'}`;
button.disabled = false;
} else {
showNotification(data.message || 'Ошибка при изменении статуса', 'error');
button.innerHTML = originalContent;
button.disabled = false;
}
})
.catch(error => {
console.error('Error:', error);
showNotification('Ошибка при изменении статуса', 'error');
button.innerHTML = originalContent;
button.disabled = false;
});
}
function duplicatePortfolio(id) {
if (confirm('Создать копию этого проекта?')) {
const button = event.target.closest('button');
const originalContent = button.innerHTML;
button.innerHTML = '<i class="fas fa-spinner fa-spin text-sm"></i>';
button.disabled = true;
// Здесь можно добавить API для дублирования
showNotification('Функция дублирования будет добавлена позже', 'info');
button.innerHTML = originalContent;
button.disabled = false;
}
}
function searchProjects() {
const searchTerm = document.getElementById('searchInput').value.toLowerCase();
const items = document.querySelectorAll('.portfolio-item');
items.forEach(item => {
const title = item.dataset.title;
if (title.includes(searchTerm)) {
item.style.display = 'block';
} else {
item.style.display = 'none';
}
});
updateProjectCount();
}
function filterByCategory() {
const selectedCategory = document.getElementById('categoryFilter').value;
const items = document.querySelectorAll('.portfolio-item');
items.forEach(item => {
const category = item.dataset.category;
if (!selectedCategory || category === selectedCategory) {
item.style.display = 'block';
} else {
item.style.display = 'none';
}
});
updateProjectCount();
}
function updateProjectCount() {
const visibleItems = document.querySelectorAll('.portfolio-item[style="display: block"], .portfolio-item:not([style*="display: none"])').length;
const totalItems = document.querySelectorAll('.portfolio-item').length;
const countElement = document.querySelector('h3 + p');
if (countElement) {
countElement.textContent = `Показано проектов: ${visibleItems} из ${totalItems}`;
}
}
function showNotification(message, type = 'info') {
// Создаем уведомление
const notification = document.createElement('div');
notification.className = `fixed top-4 right-4 z-50 px-4 py-3 rounded-md shadow-lg text-white max-w-sm transform transition-all duration-300 translate-x-full`;
switch(type) {
case 'success':
notification.classList.add('bg-green-600');
break;
case 'error':
notification.classList.add('bg-red-600');
break;
case 'info':
notification.classList.add('bg-blue-600');
break;
default:
notification.classList.add('bg-gray-600');
}
notification.innerHTML = `
<div class="flex items-center">
<i class="fas ${type === 'success' ? 'fa-check-circle' : type === 'error' ? 'fa-exclamation-circle' : 'fa-info-circle'} mr-2"></i>
<span>${message}</span>
</div>
`;
document.body.appendChild(notification);
// Показываем уведомление
setTimeout(() => {
notification.classList.remove('translate-x-full');
}, 100);
// Скрываем уведомление через 3 секунды
setTimeout(() => {
notification.classList.add('translate-x-full');
setTimeout(() => {
document.body.removeChild(notification);
}, 300);
}, 3000);
}
// Обработчик для поиска по Enter
document.getElementById('searchInput').addEventListener('keyup', function(event) {
if (event.key === 'Enter') {
searchProjects();
}
});
// Инициализация
document.addEventListener('DOMContentLoaded', function() {
updateProjectCount();
});
</script>

View File

@@ -1,358 +0,0 @@
<!-- Portfolio List -->
<div class="bg-white shadow rounded-lg">
<div class="px-4 py-5 sm:px-6 border-b border-gray-200">
<div class="flex items-center justify-between">
<div>
<h3 class="text-lg leading-6 font-medium text-gray-900">
<i class="fas fa-briefcase mr-2"></i>
Управление портфолио
</h3>
<p class="mt-1 text-sm text-gray-500">
Всего проектов: <%= portfolio ? portfolio.length : 0 %>
</p>
</div>
<div class="flex space-x-3">
<div class="flex rounded-md shadow-sm">
<input type="text" id="searchInput" placeholder="Поиск проектов..."
class="block w-full rounded-l-md border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm">
<button type="button" onclick="searchProjects()"
class="relative -ml-px inline-flex items-center px-3 py-2 rounded-r-md border border-l-0 border-gray-300 bg-gray-50 text-gray-500 hover:bg-gray-100 focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500">
<i class="fas fa-search"></i>
</button>
</div>
<select id="categoryFilter" onchange="filterByCategory()" class="rounded-md border-gray-300 py-2 pl-3 pr-10 text-base focus:border-indigo-500 focus:outline-none focus:ring-indigo-500 sm:text-sm">
<option value="">Все категории</option>
<option value="web-development">Веб-разработка</option>
<option value="mobile-app">Мобильное приложение</option>
<option value="ui-ux-design">UI/UX дизайн</option>
<option value="e-commerce">E-commerce</option>
<option value="enterprise">Корпоративное</option>
<option value="other">Другое</option>
</select>
<a href="/admin/portfolio/add" class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md text-sm font-medium inline-flex items-center">
<i class="fas fa-plus mr-2"></i>
Добавить проект
</a>
</div>
</div>
</div>
<div class="bg-white shadow overflow-hidden sm:rounded-md">
<ul role="list" class="divide-y divide-gray-200">
<% if (portfolio && portfolio.length > 0) { %>
<% portfolio.forEach(item => { %>
<li class="portfolio-item" data-category="<%= item.category %>" data-title="<%= item.title.toLowerCase() %>">
<div class="px-4 py-4">
<div class="flex items-start justify-between">
<div class="flex items-start space-x-4">
<div class="flex-shrink-0">
<% if (item.images && item.images.length > 0) { %>
<img class="h-16 w-16 rounded-lg object-cover border border-gray-200" src="<%= item.images[0].url %>" alt="<%= item.title %>">
<% } else { %>
<div class="h-16 w-16 rounded-lg bg-gray-100 border border-gray-200 flex items-center justify-center">
<i class="fas fa-image text-gray-400 text-xl"></i>
</div>
<% } %>
</div>
<div class="min-w-0 flex-1">
<div class="flex items-center">
<h4 class="text-base font-medium text-gray-900 truncate"><%= item.title %></h4>
<div class="ml-3 flex items-center space-x-2">
<% if (item.featured) { %>
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800">
<i class="fas fa-star mr-1"></i>
Рекомендуемое
</span>
<% } %>
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium <%= item.isPublished ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800' %>">
<i class="fas <%= item.isPublished ? 'fa-check-circle' : 'fa-clock' %> mr-1"></i>
<%= item.isPublished ? 'Опубликовано' : 'Черновик' %>
</span>
</div>
</div>
<p class="mt-1 text-sm text-gray-600 line-clamp-2"><%= item.shortDescription || 'Описание не указано' %></p>
<div class="mt-2 flex items-center text-sm text-gray-500 space-x-4">
<div class="flex items-center">
<i class="fas fa-folder mr-1"></i>
<span class="capitalize"><%= item.category.replace('-', ' ') %></span>
</div>
<div class="flex items-center">
<i class="fas fa-calendar mr-1"></i>
<%= new Date(item.createdAt).toLocaleDateString('ru-RU') %>
</div>
<% if (item.viewCount && item.viewCount > 0) { %>
<div class="flex items-center">
<i class="fas fa-eye mr-1"></i>
<%= item.viewCount %> просмотров
</div>
<% } %>
<% if (item.technologies && item.technologies.length > 0) { %>
<div class="flex items-center">
<i class="fas fa-code mr-1"></i>
<%= item.technologies.slice(0, 2).join(', ') %><%= item.technologies.length > 2 ? '...' : '' %>
</div>
<% } %>
</div>
</div>
</div>
<div class="flex items-center space-x-1 ml-4">
<% if (item.isPublished) { %>
<a href="/portfolio/<%= item.id %>" target="_blank"
class="inline-flex items-center justify-center w-8 h-8 rounded-md text-gray-400 hover:text-blue-600 hover:bg-blue-50 transition-colors">
<i class="fas fa-external-link-alt text-sm"></i>
</a>
<% } %>
<button onclick="togglePublish('<%= item.id %>', '<%= item.isPublished %>')"
class="inline-flex items-center justify-center w-8 h-8 rounded-md text-gray-400 hover:text-green-600 hover:bg-green-50 transition-colors"
title="<%= item.isPublished ? 'Скрыть' : 'Опубликовать' %>">
<i class="fas <%= item.isPublished ? 'fa-eye-slash' : 'fa-eye' %> text-sm"></i>
</button>
<a href="/admin/portfolio/edit/<%= item.id %>"
class="inline-flex items-center justify-center w-8 h-8 rounded-md text-gray-400 hover:text-indigo-600 hover:bg-indigo-50 transition-colors"
title="Редактировать">
<i class="fas fa-edit text-sm"></i>
</a>
<button onclick="duplicatePortfolio('<%= item.id %>')"
class="inline-flex items-center justify-center w-8 h-8 rounded-md text-gray-400 hover:text-yellow-600 hover:bg-yellow-50 transition-colors"
title="Дублировать">
<i class="fas fa-copy text-sm"></i>
</button>
<button onclick="deletePortfolio('<%= item.id %>', '<%= item.title %>')"
class="inline-flex items-center justify-center w-8 h-8 rounded-md text-gray-400 hover:text-red-600 hover:bg-red-50 transition-colors"
title="Удалить">
<i class="fas fa-trash text-sm"></i>
</button>
</div>
</div>
</div>
</li>
<% }) %>
<% } else { %>
<li>
<div class="px-4 py-8 text-center">
<i class="fas fa-briefcase text-4xl text-gray-300 mb-4"></i>
<p class="text-gray-500">Проекты не найдены</p>
<a href="/admin/portfolio/add" class="mt-2 inline-block bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md text-sm font-medium">
Добавить первый проект
</a>
</div>
</li>
<% } %>
</ul>
</div>
<!-- Pagination -->
<% if (pagination && pagination.total > 1) { %>
<div class="bg-white px-4 py-3 flex items-center justify-between border-t border-gray-200 sm:px-6">
<div class="flex-1 flex justify-between sm:hidden">
<% if (pagination.hasPrev) { %>
<a href="?page=<%= pagination.current - 1 %>" class="relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50">
Предыдущая
</a>
<% } %>
<% if (pagination.hasNext) { %>
<a href="?page=<%= pagination.current + 1 %>" class="ml-3 relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50">
Следующая
</a>
<% } %>
</div>
</div>
<% } %>
</div>
<script>
function deletePortfolio(id, title) {
if (confirm(`Вы уверены, что хотите удалить проект "${title}"?\n\nЭто действие нельзя отменить.`)) {
const button = event.target.closest('button');
const originalContent = button.innerHTML;
button.innerHTML = '<i class="fas fa-spinner fa-spin text-sm"></i>';
button.disabled = true;
fetch(`/admin/portfolio/${id}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
showNotification('Проект успешно удален', 'success');
// Плавное удаление элемента
const listItem = button.closest('li');
listItem.style.opacity = '0.5';
listItem.style.transform = 'scale(0.95)';
setTimeout(() => {
listItem.remove();
updateProjectCount();
}, 300);
} else {
showNotification(data.message || 'Ошибка при удалении проекта', 'error');
button.innerHTML = originalContent;
button.disabled = false;
}
})
.catch(error => {
console.error('Error:', error);
showNotification('Ошибка при удалении проекта', 'error');
button.innerHTML = originalContent;
button.disabled = false;
});
}
}
function togglePublish(id, currentStatus) {
const button = event.target.closest('button');
const originalContent = button.innerHTML;
button.innerHTML = '<i class="fas fa-spinner fa-spin text-sm"></i>';
button.disabled = true;
fetch(`/admin/portfolio/${id}/toggle-publish`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
showNotification(data.message, 'success');
// Обновляем интерфейс
const listItem = button.closest('li');
const statusSpan = listItem.querySelector('.inline-flex');
const newStatus = data.isPublished;
// Обновляем иконку кнопки
button.innerHTML = `<i class="fas ${newStatus ? 'fa-eye-slash' : 'fa-eye'} text-sm"></i>`;
button.title = newStatus ? 'Скрыть' : 'Опубликовать';
// Обновляем статус
statusSpan.className = `inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${newStatus ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800'}`;
statusSpan.innerHTML = `<i class="fas ${newStatus ? 'fa-check-circle' : 'fa-clock'} mr-1"></i>${newStatus ? 'Опубликовано' : 'Черновик'}`;
button.disabled = false;
} else {
showNotification(data.message || 'Ошибка при изменении статуса', 'error');
button.innerHTML = originalContent;
button.disabled = false;
}
})
.catch(error => {
console.error('Error:', error);
showNotification('Ошибка при изменении статуса', 'error');
button.innerHTML = originalContent;
button.disabled = false;
});
}
function duplicatePortfolio(id) {
if (confirm('Создать копию этого проекта?')) {
const button = event.target.closest('button');
const originalContent = button.innerHTML;
button.innerHTML = '<i class="fas fa-spinner fa-spin text-sm"></i>';
button.disabled = true;
// Здесь можно добавить API для дублирования
showNotification('Функция дублирования будет добавлена позже', 'info');
button.innerHTML = originalContent;
button.disabled = false;
}
}
function searchProjects() {
const searchTerm = document.getElementById('searchInput').value.toLowerCase();
const items = document.querySelectorAll('.portfolio-item');
items.forEach(item => {
const title = item.dataset.title;
if (title.includes(searchTerm)) {
item.style.display = 'block';
} else {
item.style.display = 'none';
}
});
updateProjectCount();
}
function filterByCategory() {
const selectedCategory = document.getElementById('categoryFilter').value;
const items = document.querySelectorAll('.portfolio-item');
items.forEach(item => {
const category = item.dataset.category;
if (!selectedCategory || category === selectedCategory) {
item.style.display = 'block';
} else {
item.style.display = 'none';
}
});
updateProjectCount();
}
function updateProjectCount() {
const visibleItems = document.querySelectorAll('.portfolio-item[style="display: block"], .portfolio-item:not([style*="display: none"])').length;
const totalItems = document.querySelectorAll('.portfolio-item').length;
const countElement = document.querySelector('h3 + p');
if (countElement) {
countElement.textContent = `Показано проектов: ${visibleItems} из ${totalItems}`;
}
}
function showNotification(message, type = 'info') {
// Создаем уведомление
const notification = document.createElement('div');
notification.className = `fixed top-4 right-4 z-50 px-4 py-3 rounded-md shadow-lg text-white max-w-sm transform transition-all duration-300 translate-x-full`;
switch(type) {
case 'success':
notification.classList.add('bg-green-600');
break;
case 'error':
notification.classList.add('bg-red-600');
break;
case 'info':
notification.classList.add('bg-blue-600');
break;
default:
notification.classList.add('bg-gray-600');
}
notification.innerHTML = `
<div class="flex items-center">
<i class="fas ${type === 'success' ? 'fa-check-circle' : type === 'error' ? 'fa-exclamation-circle' : 'fa-info-circle'} mr-2"></i>
<span>${message}</span>
</div>
`;
document.body.appendChild(notification);
// Показываем уведомление
setTimeout(() => {
notification.classList.remove('translate-x-full');
}, 100);
// Скрываем уведомление через 3 секунды
setTimeout(() => {
notification.classList.add('translate-x-full');
setTimeout(() => {
document.body.removeChild(notification);
}, 300);
}, 3000);
}
// Обработчик для поиска по Enter
document.getElementById('searchInput').addEventListener('keyup', function(event) {
if (event.key === 'Enter') {
searchProjects();
}
});
// Инициализация
document.addEventListener('DOMContentLoaded', function() {
updateProjectCount();
});
</script>