/**
* deev.space - Main JavaScript
* @author Егор Деев
*/
(function() {
'use strict';
// ===== DOM Ready =====
document.addEventListener('DOMContentLoaded', function() {
initAOS();
initNavigation();
initBackToTop();
initMessages();
initContactForm();
initCodeBlocks();
addHeaderAnchors();
});
// ===== AOS Animation =====
function initAOS() {
if (typeof AOS !== 'undefined') {
AOS.init({
duration: 600,
easing: 'ease-out-cubic',
once: true,
offset: 50,
delay: 0
});
}
}
// ===== Navigation =====
function initNavigation() {
const navbar = document.getElementById('navbar');
const navToggle = document.getElementById('navToggle');
const navMenu = document.getElementById('navMenu');
// Mobile menu toggle
if (navToggle && navMenu) {
navToggle.addEventListener('click', function() {
const isExpanded = navToggle.getAttribute('aria-expanded') === 'true';
navToggle.setAttribute('aria-expanded', !isExpanded);
navToggle.classList.toggle('active');
navMenu.classList.toggle('active');
document.body.classList.toggle('nav-open');
});
// Close menu on link click
navMenu.querySelectorAll('.nav-link').forEach(link => {
link.addEventListener('click', () => {
navToggle.setAttribute('aria-expanded', 'false');
navToggle.classList.remove('active');
navMenu.classList.remove('active');
document.body.classList.remove('nav-open');
});
});
// Close menu on outside click
document.addEventListener('click', function(e) {
if (!navMenu.contains(e.target) && !navToggle.contains(e.target)) {
navToggle.setAttribute('aria-expanded', 'false');
navToggle.classList.remove('active');
navMenu.classList.remove('active');
document.body.classList.remove('nav-open');
}
});
}
// Navbar scroll effect
let lastScroll = 0;
window.addEventListener('scroll', function() {
const currentScroll = window.pageYOffset;
if (currentScroll > 50) {
navbar.classList.add('scrolled');
} else {
navbar.classList.remove('scrolled');
}
// Hide/show on scroll
if (currentScroll > lastScroll && currentScroll > 200) {
navbar.classList.add('hidden');
} else {
navbar.classList.remove('hidden');
}
lastScroll = currentScroll;
});
// Smooth scroll for anchor links
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
anchor.addEventListener('click', function(e) {
const targetId = this.getAttribute('href');
if (targetId === '#') return;
const target = document.querySelector(targetId);
if (target) {
e.preventDefault();
const headerOffset = 100;
const elementPosition = target.getBoundingClientRect().top;
const offsetPosition = elementPosition + window.pageYOffset - headerOffset;
window.scrollTo({
top: offsetPosition,
behavior: 'smooth'
});
}
});
});
}
// ===== Back to Top Button =====
function initBackToTop() {
const backToTop = document.getElementById('backToTop');
if (!backToTop) return;
window.addEventListener('scroll', function() {
if (window.pageYOffset > 500) {
backToTop.classList.add('visible');
} else {
backToTop.classList.remove('visible');
}
});
backToTop.addEventListener('click', function() {
window.scrollTo({
top: 0,
behavior: 'smooth'
});
});
}
// ===== Messages =====
function initMessages() {
const messagesContainer = document.getElementById('messagesContainer');
if (!messagesContainer) return;
messagesContainer.querySelectorAll('.message').forEach(message => {
// Auto hide after 5 seconds
setTimeout(() => {
hideMessage(message);
}, 5000);
// Close button
const closeBtn = message.querySelector('.message-close');
if (closeBtn) {
closeBtn.addEventListener('click', () => hideMessage(message));
}
});
}
function hideMessage(message) {
message.classList.add('hiding');
setTimeout(() => message.remove(), 300);
}
// ===== Contact Form =====
function initContactForm() {
const contactForm = document.getElementById('contact-form');
if (!contactForm) return;
contactForm.addEventListener('submit', async function(e) {
e.preventDefault();
const submitBtn = this.querySelector('button[type="submit"]');
const originalContent = submitBtn.innerHTML;
// Disable button and show loading
submitBtn.disabled = true;
submitBtn.innerHTML = ' Отправка...';
try {
const formData = new FormData(this);
const response = await fetch(this.action, {
method: 'POST',
body: formData,
headers: {
'X-Requested-With': 'XMLHttpRequest',
'X-CSRFToken': getCookie('csrftoken')
}
});
const data = await response.json();
if (data.success) {
contactForm.reset();
showNotification(data.message || 'Сообщение успешно отправлено!', 'success');
// Reset reCAPTCHA if exists
if (typeof grecaptcha !== 'undefined') {
grecaptcha.reset();
}
} else {
const errorMsg = data.errors ? Object.values(data.errors).flat().join(', ') : 'Ошибка при отправке';
showNotification(errorMsg, 'error');
}
} catch (error) {
console.error('Form submission error:', error);
showNotification('Произошла ошибка. Попробуйте позже.', 'error');
} finally {
submitBtn.disabled = false;
submitBtn.innerHTML = originalContent;
}
});
}
// ===== CSRF Token =====
function getCookie(name) {
let cookieValue = null;
if (document.cookie && document.cookie !== '') {
const cookies = document.cookie.split(';');
for (let i = 0; i < cookies.length; i++) {
const cookie = cookies[i].trim();
if (cookie.substring(0, name.length + 1) === (name + '=')) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
}
// ===== Notifications =====
window.showNotification = function(message, type = 'info') {
const container = document.getElementById('messagesContainer') || createNotificationContainer();
const notification = document.createElement('div');
notification.className = `message message-${type}`;
notification.innerHTML = `
${message}
`;
container.appendChild(notification);
// Animate in
setTimeout(() => notification.classList.add('show'), 10);
// Close button
notification.querySelector('.message-close').addEventListener('click', () => {
hideMessage(notification);
});
// Auto hide
setTimeout(() => hideMessage(notification), 5000);
};
function createNotificationContainer() {
const container = document.createElement('div');
container.id = 'messagesContainer';
container.className = 'messages-container';
document.body.appendChild(container);
return container;
}
function getNotificationIcon(type) {
const icons = {
success: 'fa-check-circle',
error: 'fa-exclamation-circle',
warning: 'fa-exclamation-triangle',
info: 'fa-info-circle'
};
return icons[type] || icons.info;
}
// ===== Article Like/Dislike =====
window.toggleArticleLike = async function(articleId, isLike) {
try {
const response = await fetch(`/api/article/${articleId}/like/`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': getCookie('csrftoken')
},
body: JSON.stringify({ is_like: isLike })
});
if (!response.ok) {
if (response.status === 403) {
showNotification('Войдите, чтобы оценить статью', 'warning');
return;
}
throw new Error('Network response was not ok');
}
const data = await response.json();
if (data.success) {
updateVoteButtons(`article-${articleId}`, data);
}
} catch (error) {
console.error('Error:', error);
showNotification('Ошибка при оценке статьи', 'error');
}
};
// ===== Comment Like/Dislike =====
window.toggleCommentLike = async function(commentId, isLike) {
try {
const response = await fetch(`/api/comment/${commentId}/like/`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': getCookie('csrftoken')
},
body: JSON.stringify({ is_like: isLike })
});
if (!response.ok) {
if (response.status === 403) {
showNotification('Войдите, чтобы оценить комментарий', 'warning');
return;
}
throw new Error('Network response was not ok');
}
const data = await response.json();
if (data.success) {
updateVoteButtons(`comment-${commentId}`, data);
}
} catch (error) {
console.error('Error:', error);
showNotification('Ошибка при оценке комментария', 'error');
}
};
function updateVoteButtons(prefix, data) {
const likesEl = document.getElementById(`${prefix}-likes`);
const dislikesEl = document.getElementById(`${prefix}-dislikes`);
const likeBtn = document.getElementById(`${prefix}-like-btn`);
const dislikeBtn = document.getElementById(`${prefix}-dislike-btn`);
if (likesEl) likesEl.textContent = data.likes;
if (dislikesEl) dislikesEl.textContent = data.dislikes;
if (likeBtn && dislikeBtn) {
likeBtn.classList.remove('active');
dislikeBtn.classList.remove('active');
// Update icons
const likeIcon = likeBtn.querySelector('i');
const dislikeIcon = dislikeBtn.querySelector('i');
if (likeIcon) {
likeIcon.className = data.user_vote === true ? 'fas fa-thumbs-up' : 'far fa-thumbs-up';
}
if (dislikeIcon) {
dislikeIcon.className = data.user_vote === false ? 'fas fa-thumbs-down' : 'far fa-thumbs-down';
}
if (data.user_vote === true) {
likeBtn.classList.add('active');
} else if (data.user_vote === false) {
dislikeBtn.classList.add('active');
}
}
}
// ===== Comments =====
window.submitComment = async function(articleId, parentId = null) {
const form = parentId
? document.getElementById(`reply-form-${parentId}`)
: document.getElementById('comment-form');
if (!form) return;
const submitBtn = form.querySelector('button[type="submit"]');
const originalContent = submitBtn.innerHTML;
const textarea = form.querySelector('textarea');
const content = textarea.value.trim();
if (!content) {
showNotification('Введите текст комментария', 'warning');
return;
}
submitBtn.disabled = true;
submitBtn.innerHTML = '';
try {
const formData = new FormData(form);
if (parentId) {
formData.append('parent_id', parentId);
}
const response = await fetch(`/api/article/${articleId}/comment/`, {
method: 'POST',
body: formData,
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
});
const data = await response.json();
if (data.success) {
showNotification('Комментарий добавлен!', 'success');
setTimeout(() => location.reload(), 1000);
} else {
const errorMsg = data.errors ? Object.values(data.errors).flat().join(', ') : 'Ошибка';
showNotification(errorMsg, 'error');
}
} catch (error) {
console.error('Error:', error);
showNotification('Ошибка при добавлении комментария', 'error');
} finally {
submitBtn.disabled = false;
submitBtn.innerHTML = originalContent;
}
};
window.showReplyForm = function(commentId) {
// Hide all reply forms first
document.querySelectorAll('.reply-form-container').forEach(form => {
form.style.display = 'none';
});
// Show the specific reply form
const replyForm = document.getElementById(`reply-form-container-${commentId}`);
if (replyForm) {
replyForm.style.display = 'block';
const textarea = replyForm.querySelector('textarea');
if (textarea) {
textarea.focus();
}
}
};
window.hideReplyForm = function(commentId) {
const replyForm = document.getElementById(`reply-form-container-${commentId}`);
if (replyForm) {
replyForm.style.display = 'none';
}
};
// ===== Copy to Clipboard =====
window.copyToClipboard = function(text, button) {
navigator.clipboard.writeText(text).then(() => {
const originalHTML = button.innerHTML;
button.innerHTML = '';
button.classList.add('copied');
setTimeout(() => {
button.innerHTML = originalHTML;
button.classList.remove('copied');
}, 2000);
showNotification('Скопировано!', 'success');
}).catch(err => {
console.error('Copy failed:', err);
showNotification('Ошибка копирования', 'error');
});
};
// ===== Code Copy Functionality =====
function initCodeBlocks() {
const codeBlocks = document.querySelectorAll('.article-body pre');
codeBlocks.forEach((pre) => {
// Проверяем, не добавлена ли уже кнопка
if (pre.querySelector('.code-copy-btn')) {
return;
}
const codeElement = pre.querySelector('code');
if (!codeElement) return;
// Создаём кнопку копирования
const copyButton = document.createElement('button');
copyButton.className = 'code-copy-btn';
copyButton.type = 'button';
copyButton.setAttribute('aria-label', 'Копировать код');
copyButton.innerHTML = 'Копировать';
// Обработчик клика
copyButton.addEventListener('click', async function(e) {
e.preventDefault();
// Получаем текст кода без HTML-тегов
const codeText = codeElement.textContent || codeElement.innerText;
try {
await navigator.clipboard.writeText(codeText);
// Изменяем визуальное состояние кнопки
copyButton.innerHTML = 'Сохранено!';
copyButton.classList.add('copied');
// Возвращаем исходное состояние через 2 секунды
setTimeout(() => {
copyButton.innerHTML = 'Копировать';
copyButton.classList.remove('copied');
}, 2000);
} catch (err) {
console.error('Ошибка при копировании:', err);
// Fallback для старых браузеров
const textArea = document.createElement('textarea');
textArea.value = codeText;
textArea.style.position = 'fixed';
textArea.style.left = '-999999px';
document.body.appendChild(textArea);
textArea.select();
try {
document.execCommand('copy');
copyButton.innerHTML = 'Сохранено!';
copyButton.classList.add('copied');
setTimeout(() => {
copyButton.innerHTML = 'Копировать';
copyButton.classList.remove('copied');
}, 2000);
} catch (err2) {
showNotification('Не удалось скопировать код', 'error');
}
document.body.removeChild(textArea);
}
});
// Добавляем кнопку в блок кода
pre.style.position = 'relative';
pre.appendChild(copyButton);
});
}
function addHeaderAnchors() {
const headers = document.querySelectorAll('.article-body h2, .article-body h3');
headers.forEach(header => {
const id = header.textContent
.toLowerCase()
.replace(/[^a-zа-яё0-9]+/g, '-')
.replace(/^-|-$/g, '');
header.id = id;
const anchor = document.createElement('a');
anchor.className = 'header-anchor';
anchor.href = `#${id}`;
anchor.innerHTML = '';
anchor.setAttribute('aria-label', 'Ссылка на раздел');
header.appendChild(anchor);
});
}
// ===== Password Visibility Toggle =====
window.togglePasswordVisibility = function(inputId, button) {
const input = document.getElementById(inputId);
const icon = button.querySelector('i');
if (!input) return;
if (input.type === 'password') {
input.type = 'text';
icon.classList.remove('fa-eye');
icon.classList.add('fa-eye-slash');
button.setAttribute('aria-label', 'Скрыть пароль');
} else {
input.type = 'password';
icon.classList.remove('fa-eye-slash');
icon.classList.add('fa-eye');
button.setAttribute('aria-label', 'Показать пароль');
}
};
})();