Files
sst_site/views/admin/portfolio/add.ejs
Andrey K. Choi 9477ff6de0 feat: Реализован полный CRUD для админ-панели и улучшена функциональность
- Portfolio CRUD: добавление, редактирование, удаление, переключение публикации
- Services CRUD: полное управление услугами с возможностью активации/деактивации
- Banner system: новая модель Banner с CRUD операциями и аналитикой кликов
- Telegram integration: расширенные настройки бота, обнаружение чатов, отправка сообщений
- Media management: улучшенная загрузка файлов с оптимизацией изображений и превью
- UI improvements: обновлённые админ-панели с rich-text редактором и drag&drop загрузкой
- Database: добавлена таблица banners с полями для баннеров и аналитики
2025-10-22 20:32:16 +09:00

776 lines
39 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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