- Реализованы функции для кэширования аватарок пользователей с использованием localStorage. - Добавлены методы для получения, очистки и преобразования аватарок в формат base64. - Обновлены интерфейсы загрузки и отображения аватарок с поддержкой кэширования. - Обновлены зависимости, включая добавление библиотеки sharp для обработки изображений.
454 lines
15 KiB
JavaScript
454 lines
15 KiB
JavaScript
// 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();
|
||
});
|