// DOM элементы const avatarImage = document.getElementById("avatarImage"); const avatarPlaceholder = document.getElementById("avatarPlaceholder"); const avatarInput = document.getElementById("avatarInput"); const deleteAvatarBtn = document.getElementById("deleteAvatarBtn"); const usernameInput = document.getElementById("username"); const emailInput = document.getElementById("email"); const updateProfileBtn = document.getElementById("updateProfileBtn"); const currentPasswordInput = document.getElementById("currentPassword"); const newPasswordInput = document.getElementById("newPassword"); const confirmPasswordInput = document.getElementById("confirmPassword"); const changePasswordBtn = document.getElementById("changePasswordBtn"); const messageContainer = document.getElementById("messageContainer"); // Кэширование аватарки const AVATAR_CACHE_KEY = "avatar_cache"; const AVATAR_TIMESTAMP_KEY = "avatar_timestamp"; // Функция для кэширования аватарки async function cacheAvatar(avatarUrl) { try { const response = await fetch(avatarUrl); if (!response.ok) return false; const blob = await response.blob(); const base64 = await blobToBase64(blob); const cacheData = { base64: base64, timestamp: Date.now(), url: avatarUrl }; localStorage.setItem(AVATAR_CACHE_KEY, JSON.stringify(cacheData)); localStorage.setItem(AVATAR_TIMESTAMP_KEY, Date.now().toString()); return true; } catch (error) { console.error("Ошибка кэширования аватарки:", error); return false; } } // Функция для получения аватарки из кэша function getCachedAvatar() { try { const cacheData = localStorage.getItem(AVATAR_CACHE_KEY); const timestamp = localStorage.getItem(AVATAR_TIMESTAMP_KEY); if (!cacheData || !timestamp) return null; const data = JSON.parse(cacheData); const cacheAge = Date.now() - parseInt(timestamp); // Кэш действителен 24 часа (86400000 мс) if (cacheAge > 24 * 60 * 60 * 1000) { clearAvatarCache(); return null; } return data; } catch (error) { console.error("Ошибка получения аватарки из кэша:", error); clearAvatarCache(); return null; } } // Функция для очистки кэша аватарки function clearAvatarCache() { localStorage.removeItem(AVATAR_CACHE_KEY); localStorage.removeItem(AVATAR_TIMESTAMP_KEY); } // Преобразование Blob в base64 function blobToBase64(blob) { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => resolve(reader.result); reader.onerror = reject; reader.readAsDataURL(blob); }); } // Lazy loading для изображений function initLazyLoading() { // Проверяем поддержку Intersection Observer API if ("IntersectionObserver" in window) { const imageObserver = new IntersectionObserver( (entries, observer) => { entries.forEach((entry) => { if (entry.isIntersecting) { const img = entry.target; // Если у изображения есть data-src, загружаем его if (img.dataset.src) { img.src = img.dataset.src; img.removeAttribute("data-src"); } img.classList.remove("lazy"); observer.unobserve(img); } }); }, { rootMargin: "50px 0px", threshold: 0.01, } ); // Наблюдаем за всеми изображениями с классом lazy document.querySelectorAll("img.lazy").forEach((img) => { imageObserver.observe(img); }); } else { // Fallback для старых браузеров document.querySelectorAll("img.lazy").forEach((img) => { if (img.dataset.src) { img.src = img.dataset.src; img.removeAttribute("data-src"); } img.classList.remove("lazy"); }); } } // Функция для показа сообщений function showMessage(message, type = "success") { messageContainer.textContent = message; messageContainer.className = `message-container ${type}`; messageContainer.style.display = "block"; setTimeout(() => { messageContainer.style.display = "none"; }, 5000); } // Загрузка данных профиля async function loadProfile() { try { const response = await fetch("/api/user"); if (!response.ok) { throw new Error("Ошибка загрузки профиля"); } const user = await response.json(); // Заполняем поля usernameInput.value = user.username || ""; emailInput.value = user.email || ""; // Применяем цветовой акцент пользователя (для отображения) const accentColor = user.accent_color || "#007bff"; document.documentElement.style.setProperty("--accent-color", accentColor); // Обрабатываем аватарку if (user.avatar) { // Проверяем, есть ли аватарка в кэше const cachedAvatar = getCachedAvatar(); if (cachedAvatar && cachedAvatar.url === user.avatar) { // Используем кэшированную аватарку avatarImage.src = cachedAvatar.base64; console.log("Аватарка загружена из кэша"); } else { // Загружаем аватарку с сервера и кэшируем avatarImage.src = user.avatar; // Кэшируем в фоне cacheAvatar(user.avatar).then(success => { if (success) { console.log("Аватарка закэширована"); } }); } avatarImage.style.display = "block"; avatarPlaceholder.style.display = "none"; deleteAvatarBtn.style.display = "inline-block"; } else { avatarImage.style.display = "none"; avatarPlaceholder.style.display = "inline-flex"; deleteAvatarBtn.style.display = "none"; // Очищаем кэш, если аватарки нет clearAvatarCache(); } } catch (error) { console.error("Ошибка загрузки профиля:", error); showMessage("Ошибка загрузки данных профиля", "error"); } } // Обработчик загрузки аватарки avatarInput.addEventListener("change", async function (event) { const file = event.target.files[0]; if (!file) return; // Проверка размера файла (5MB) if (file.size > 5 * 1024 * 1024) { showMessage("Файл слишком большой. Максимальный размер: 5 МБ", "error"); return; } // Проверка типа файла const allowedTypes = ["image/jpeg", "image/jpg", "image/png", "image/gif"]; if (!allowedTypes.includes(file.type)) { showMessage( "Недопустимый формат файла. Используйте JPG, PNG или GIF", "error" ); return; } try { const formData = new FormData(); formData.append("avatar", file); const response = await fetch("/api/user/avatar", { method: "POST", body: formData, }); if (!response.ok) { const error = await response.json(); throw new Error(error.error || "Ошибка загрузки аватарки"); } const result = await response.json(); // Обновляем отображение аватарки avatarImage.src = result.avatar + "?t=" + Date.now(); // Добавляем timestamp для обновления кэша браузера avatarImage.style.display = "block"; avatarPlaceholder.style.display = "none"; deleteAvatarBtn.style.display = "inline-block"; // Обновляем кэш с новой аватаркой cacheAvatar(result.avatar).then(success => { if (success) { console.log("Новая аватарка закэширована"); } }); showMessage("Аватарка успешно загружена", "success"); } catch (error) { console.error("Ошибка загрузки аватарки:", error); showMessage(error.message, "error"); } // Сбрасываем input для возможности повторной загрузки того же файла event.target.value = ""; }); // Обработчик удаления аватарки deleteAvatarBtn.addEventListener("click", async function () { if (!confirm("Вы уверены, что хотите удалить аватарку?")) { return; } try { const response = await fetch("/api/user/avatar", { method: "DELETE", }); if (!response.ok) { const error = await response.json(); throw new Error(error.error || "Ошибка удаления аватарки"); } // Обновляем отображение avatarImage.style.display = "none"; avatarPlaceholder.style.display = "inline-flex"; deleteAvatarBtn.style.display = "none"; // Очищаем кэш аватарки clearAvatarCache(); showMessage("Аватарка успешно удалена", "success"); } catch (error) { console.error("Ошибка удаления аватарки:", error); showMessage(error.message, "error"); } }); // Обработчик обновления профиля updateProfileBtn.addEventListener("click", async function () { const username = usernameInput.value.trim(); const email = emailInput.value.trim(); // Валидация if (!username) { showMessage("Логин не может быть пустым", "error"); return; } if (username.length < 3) { showMessage("Логин должен быть не менее 3 символов", "error"); return; } if (email && !isValidEmail(email)) { showMessage("Некорректный email адрес", "error"); return; } try { const response = await fetch("/api/user/profile", { method: "PUT", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ username, email: email || null, }), }); if (!response.ok) { const error = await response.json(); throw new Error(error.error || "Ошибка обновления профиля"); } const result = await response.json(); showMessage(result.message || "Профиль успешно обновлен", "success"); } catch (error) { console.error("Ошибка обновления профиля:", error); showMessage(error.message, "error"); } }); // Обработчик изменения пароля changePasswordBtn.addEventListener("click", async function () { const currentPassword = currentPasswordInput.value; const newPassword = newPasswordInput.value; const confirmPassword = confirmPasswordInput.value; // Валидация if (!currentPassword) { showMessage("Введите текущий пароль", "error"); return; } if (!newPassword) { showMessage("Введите новый пароль", "error"); return; } if (newPassword.length < 6) { showMessage("Новый пароль должен быть не менее 6 символов", "error"); return; } if (newPassword !== confirmPassword) { showMessage("Новый пароль и подтверждение не совпадают", "error"); return; } try { const response = await fetch("/api/user/profile", { method: "PUT", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ currentPassword, newPassword, }), }); if (!response.ok) { const error = await response.json(); throw new Error(error.error || "Ошибка изменения пароля"); } const result = await response.json(); // Очищаем поля паролей currentPasswordInput.value = ""; newPasswordInput.value = ""; confirmPasswordInput.value = ""; showMessage(result.message || "Пароль успешно изменен", "success"); } catch (error) { console.error("Ошибка изменения пароля:", error); showMessage(error.message, "error"); } }); // Функция валидации email function isValidEmail(email) { const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; return re.test(email); } // Функция для проверки аутентификации async function checkAuthentication() { const isAuthenticated = localStorage.getItem("isAuthenticated"); if (isAuthenticated !== "true") { // Если пользователь не аутентифицирован, перенаправляем на страницу входа window.location.href = "/"; return; } // Проверяем, что сессия на сервере еще действительна try { const response = await fetch("/api/auth/status"); if (!response.ok) { // Если сессия недействительна, очищаем localStorage и перенаправляем localStorage.removeItem("isAuthenticated"); localStorage.removeItem("username"); window.location.href = "/"; return; } const authData = await response.json(); if (!authData.authenticated) { // Если сервер говорит, что пользователь не аутентифицирован localStorage.removeItem("isAuthenticated"); localStorage.removeItem("username"); window.location.href = "/"; return; } } catch (error) { console.error("Ошибка проверки аутентификации:", error); // В случае ошибки сети, оставляем пользователя на странице // но показываем предупреждение console.warn("Не удалось проверить статус аутентификации"); } } // Функция для настройки обработчика выхода function setupLogoutHandler() { const logoutForms = document.querySelectorAll('form[action="/logout"]'); logoutForms.forEach((form) => { form.addEventListener("submit", function (e) { // Очищаем localStorage перед выходом localStorage.removeItem("isAuthenticated"); localStorage.removeItem("username"); }); }); } // Загружаем профиль при загрузке страницы document.addEventListener("DOMContentLoaded", function () { // Проверяем аутентификацию при загрузке страницы checkAuthentication(); loadProfile(); // Инициализируем lazy loading для изображений initLazyLoading(); // Добавляем обработчик для кнопки выхода setupLogoutHandler(); });