NoteJS/public/profile.js
Fovway d89a617264 Добавлены функции для работы с AI настройками и улучшения текста
- Реализованы API для сохранения и получения AI настроек пользователя, включая OpenAI API ключ, базовый URL и модель.
- Добавлена возможность окончательного удаления всех архивных заметок с подтверждением пароля.
- Внедрена функция улучшения текста через AI, с обработкой запросов к OpenAI API.
- Обновлены интерфейсы для работы с AI настройками и добавлены уведомления для улучшения пользовательского опыта.
2025-10-26 14:45:02 +07:00

677 lines
22 KiB
JavaScript
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.

// Система стека уведомлений
let notificationStack = [];
// Универсальная функция для показа уведомлений
function showNotification(message, type = "info") {
// Создаем уведомление
const notification = document.createElement("div");
notification.className = `notification notification-${type}`;
notification.textContent = message;
notification.id = `notification-${Date.now()}-${Math.random()
.toString(36)
.substr(2, 9)}`;
// Добавляем возможность закрытия по клику
notification.style.cursor = "pointer";
notification.addEventListener("click", () => {
removeNotification(notification);
});
// Добавляем на страницу
document.body.appendChild(notification);
// Добавляем в стек
notificationStack.push(notification);
// Обновляем позиции всех уведомлений
updateNotificationPositions();
// Анимация появления
setTimeout(() => {
notification.style.transform = "translateX(-50%) translateY(0)";
}, 100);
// Автоматическое удаление через 4 секунды
setTimeout(() => {
removeNotification(notification);
}, 4000);
}
// Функция для удаления уведомления
function removeNotification(notification) {
// Анимация исчезновения
notification.style.transform = "translateX(-50%) translateY(-100%)";
setTimeout(() => {
// Удаляем из DOM
if (notification.parentNode) {
notification.parentNode.removeChild(notification);
}
// Удаляем из стека
const index = notificationStack.indexOf(notification);
if (index > -1) {
notificationStack.splice(index, 1);
}
// Обновляем позиции оставшихся уведомлений
updateNotificationPositions();
}, 300);
}
// Функция для обновления позиций уведомлений
function updateNotificationPositions() {
notificationStack.forEach((notification, index) => {
const offset = index * 70; // 70px между уведомлениями
notification.style.top = `${20 + offset}px`;
});
}
// Универсальная функция для модальных окон подтверждения
function showConfirmModal(title, message, options = {}) {
return new Promise((resolve) => {
// Создаем модальное окно
const modal = document.createElement("div");
modal.className = "modal";
modal.style.display = "block";
// Создаем содержимое модального окна
const modalContent = document.createElement("div");
modalContent.className = "modal-content";
// Создаем заголовок
const modalHeader = document.createElement("div");
modalHeader.className = "modal-header";
modalHeader.innerHTML = `
<h3>${title}</h3>
<span class="modal-close">&times;</span>
`;
// Создаем тело модального окна
const modalBody = document.createElement("div");
modalBody.className = "modal-body";
modalBody.innerHTML = `<p>${message}</p>`;
// Создаем футер с кнопками
const modalFooter = document.createElement("div");
modalFooter.className = "modal-footer";
modalFooter.innerHTML = `
<button id="confirmBtn" class="${
options.confirmType === "danger" ? "btn-danger" : "btn-primary"
}" style="margin-right: 10px">
${options.confirmText || "OK"}
</button>
<button id="cancelBtn" class="btn-secondary">
${options.cancelText || "Отмена"}
</button>
`;
// Собираем модальное окно
modalContent.appendChild(modalHeader);
modalContent.appendChild(modalBody);
modalContent.appendChild(modalFooter);
modal.appendChild(modalContent);
// Добавляем на страницу
document.body.appendChild(modal);
// Функция закрытия
function closeModal() {
modal.style.display = "none";
if (modal.parentNode) {
modal.parentNode.removeChild(modal);
}
}
// Обработчики событий
const closeBtn = modalHeader.querySelector(".modal-close");
const cancelBtn = modalFooter.querySelector("#cancelBtn");
const confirmBtn = modalFooter.querySelector("#confirmBtn");
closeBtn.addEventListener("click", () => {
closeModal();
resolve(false);
});
cancelBtn.addEventListener("click", () => {
closeModal();
resolve(false);
});
confirmBtn.addEventListener("click", () => {
closeModal();
resolve(true);
});
// Закрытие при клике вне модального окна
modal.addEventListener("click", (e) => {
if (e.target === modal) {
closeModal();
resolve(false);
}
});
// Закрытие по Escape
const handleEscape = (e) => {
if (e.key === "Escape") {
closeModal();
resolve(false);
document.removeEventListener("keydown", handleEscape);
}
};
document.addEventListener("keydown", handleEscape);
});
}
// Логика переключения темы
function initThemeToggle() {
const themeToggleBtn = document.getElementById("theme-toggle-btn");
if (!themeToggleBtn) return;
// Загружаем сохраненную тему или используем системную
const savedTheme = localStorage.getItem("theme");
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)").matches
? "dark"
: "light";
const currentTheme = savedTheme || systemTheme;
// Применяем тему
applyTheme(currentTheme);
// Обработчик клика на переключатель
themeToggleBtn.addEventListener("click", () => {
const currentTheme = document.documentElement.getAttribute("data-theme");
const newTheme = currentTheme === "dark" ? "light" : "dark";
applyTheme(newTheme);
localStorage.setItem("theme", newTheme);
});
// Слушаем изменения системной темы
window
.matchMedia("(prefers-color-scheme: dark)")
.addEventListener("change", (e) => {
if (!localStorage.getItem("theme")) {
applyTheme(e.matches ? "dark" : "light");
}
});
}
function applyTheme(theme) {
document.documentElement.setAttribute("data-theme", theme);
// Обновляем meta теги для PWA
const themeColorMeta = document.querySelector('meta[name="theme-color"]');
if (themeColorMeta) {
themeColorMeta.setAttribute(
"content",
theme === "dark" ? "#1a1a1a" : "#007bff"
);
}
// Обновляем иконку переключателя
const themeToggleBtn = document.getElementById("theme-toggle-btn");
if (themeToggleBtn) {
const icon = themeToggleBtn.querySelector(".iconify");
if (icon) {
icon.setAttribute(
"data-icon",
theme === "dark" ? "mdi:weather-sunny" : "mdi:weather-night"
);
}
}
}
// Инициализируем переключатель темы при загрузке страницы
document.addEventListener("DOMContentLoaded", initThemeToggle);
// 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 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");
});
}
}
// Загрузка данных профиля
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);
showNotification("Ошибка загрузки данных профиля", "error");
}
}
// Обработчик загрузки аватарки
avatarInput.addEventListener("change", async function (event) {
const file = event.target.files[0];
if (!file) return;
// Проверка размера файла (5MB)
if (file.size > 5 * 1024 * 1024) {
showNotification(
"Файл слишком большой. Максимальный размер: 5 МБ",
"error"
);
return;
}
// Проверка типа файла
const allowedTypes = ["image/jpeg", "image/jpg", "image/png", "image/gif"];
if (!allowedTypes.includes(file.type)) {
showNotification(
"Недопустимый формат файла. Используйте 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("Новая аватарка закэширована");
}
});
showNotification("Аватарка успешно загружена", "success");
} catch (error) {
console.error("Ошибка загрузки аватарки:", error);
showNotification(error.message, "error");
}
// Сбрасываем input для возможности повторной загрузки того же файла
event.target.value = "";
});
// Обработчик удаления аватарки
deleteAvatarBtn.addEventListener("click", async function () {
const confirmed = await showConfirmModal(
"Подтверждение удаления",
"Вы уверены, что хотите удалить аватарку?",
{ confirmType: "danger", confirmText: "Удалить" }
);
if (!confirmed) {
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();
showNotification("Аватарка успешно удалена", "success");
} catch (error) {
console.error("Ошибка удаления аватарки:", error);
showNotification(error.message, "error");
}
});
// Обработчик обновления профиля
updateProfileBtn.addEventListener("click", async function () {
const username = usernameInput.value.trim();
const email = emailInput.value.trim();
// Валидация
if (!username) {
showNotification("Логин не может быть пустым", "error");
return;
}
if (username.length < 3) {
showNotification("Логин должен быть не менее 3 символов", "error");
return;
}
if (email && !isValidEmail(email)) {
showNotification("Некорректный 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();
showNotification(result.message || "Профиль успешно обновлен", "success");
} catch (error) {
console.error("Ошибка обновления профиля:", error);
showNotification(error.message, "error");
}
});
// Обработчик изменения пароля
changePasswordBtn.addEventListener("click", async function () {
const currentPassword = currentPasswordInput.value;
const newPassword = newPasswordInput.value;
const confirmPassword = confirmPasswordInput.value;
// Валидация
if (!currentPassword) {
showNotification("Введите текущий пароль", "error");
return;
}
if (!newPassword) {
showNotification("Введите новый пароль", "error");
return;
}
if (newPassword.length < 6) {
showNotification("Новый пароль должен быть не менее 6 символов", "error");
return;
}
if (newPassword !== confirmPassword) {
showNotification("Новый пароль и подтверждение не совпадают", "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 = "";
showNotification(result.message || "Пароль успешно изменен", "success");
} catch (error) {
console.error("Ошибка изменения пароля:", error);
showNotification(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();
});