Добавлены функции предпросмотра заметок и улучшены стили интерфейса

- Реализована возможность предпросмотра заметок с поддержкой Markdown и кликабельных тегов.
- Добавлены кнопки для переключения между режимами редактирования и предпросмотра.
- Обновлены стили для контейнера предпросмотра и элементов управления.
- Оптимизированы обработчики событий для новых функций предпросмотра.
This commit is contained in:
Fovway 2025-10-24 13:01:20 +07:00
parent f9ba1796dc
commit 5b76167c3d
7 changed files with 577 additions and 206 deletions

View File

@ -17,6 +17,7 @@ const codeBtn = document.getElementById("codeBtn");
const linkBtn = document.getElementById("linkBtn"); const linkBtn = document.getElementById("linkBtn");
const checkboxBtn = document.getElementById("checkboxBtn"); const checkboxBtn = document.getElementById("checkboxBtn");
const imageBtn = document.getElementById("imageBtn"); const imageBtn = document.getElementById("imageBtn");
const previewBtn = document.getElementById("previewBtn");
// Кнопка настроек // Кнопка настроек
const settingsBtn = document.getElementById("settings-btn"); const settingsBtn = document.getElementById("settings-btn");
@ -27,6 +28,10 @@ const imagePreviewContainer = document.getElementById("imagePreviewContainer");
const imagePreviewList = document.getElementById("imagePreviewList"); const imagePreviewList = document.getElementById("imagePreviewList");
const clearImagesBtn = document.getElementById("clearImagesBtn"); const clearImagesBtn = document.getElementById("clearImagesBtn");
// Элементы для предпросмотра заметки
const notePreviewContainer = document.getElementById("notePreviewContainer");
const notePreviewContent = document.getElementById("notePreviewContent");
// Модальное окно для просмотра изображений // Модальное окно для просмотра изображений
const imageModal = document.getElementById("imageModal"); const imageModal = document.getElementById("imageModal");
const modalImage = document.getElementById("modalImage"); const modalImage = document.getElementById("modalImage");
@ -35,6 +40,9 @@ const modalClose = document.querySelector(".image-modal-close");
// Массив для хранения выбранных изображений // Массив для хранения выбранных изображений
let selectedImages = []; let selectedImages = [];
// Флаг режима предпросмотра
let isPreviewMode = false;
// Глобальные переменные для заметок и фильтрации // Глобальные переменные для заметок и фильтрации
let allNotes = []; let allNotes = [];
let selectedDateFilter = null; let selectedDateFilter = null;
@ -771,6 +779,52 @@ imageBtn.addEventListener("touchend", function (event) {
imageInput.click(); imageInput.click();
}); });
// Обработчик для кнопки предпросмотра
previewBtn.addEventListener("click", function () {
togglePreview();
});
// Функция переключения режима предпросмотра
function togglePreview() {
isPreviewMode = !isPreviewMode;
if (isPreviewMode) {
// Показываем предпросмотр
noteInput.style.display = "none";
notePreviewContainer.style.display = "block";
// Получаем содержимое и рендерим его
const content = noteInput.value;
if (content.trim()) {
// Парсим markdown и делаем теги кликабельными
const htmlContent = marked.parse(content);
const contentWithTags = makeTagsClickable(htmlContent);
notePreviewContent.innerHTML = contentWithTags;
// Инициализируем lazy loading для изображений в превью
setTimeout(() => {
initLazyLoading();
}, 0);
} else {
notePreviewContent.innerHTML =
'<p style="color: #999; font-style: italic;">Нет содержимого для предпросмотра</p>';
}
// Меняем иконку кнопки
previewBtn.innerHTML =
'<span class="iconify" data-icon="mdi:eye-off"></span>';
previewBtn.title = "Закрыть предпросмотр";
} else {
// Возвращаемся к редактированию
noteInput.style.display = "block";
notePreviewContainer.style.display = "none";
// Меняем иконку обратно
previewBtn.innerHTML = '<span class="iconify" data-icon="mdi:eye"></span>';
previewBtn.title = "Предпросмотр";
}
}
// Обработчик выбора файлов // Обработчик выбора файлов
imageInput.addEventListener("change", function (event) { imageInput.addEventListener("change", function (event) {
const files = Array.from(event.target.files); const files = Array.from(event.target.files);
@ -1278,14 +1332,24 @@ async function renderNotes(notes) {
<div class="date"> <div class="date">
<span class="date-text">${dateDisplay}${pinIndicator}</span> <span class="date-text">${dateDisplay}${pinIndicator}</span>
<div class="note-actions"> <div class="note-actions">
<div id="pinBtn" class="notesHeaderBtn" data-id="${note.id}" title="${note.is_pinned ? "Открепить" : "Закрепить"}"> <div id="pinBtn" class="notesHeaderBtn" data-id="${
<span class="iconify" data-icon="mdi:pin${note.is_pinned ? "-off" : ""}"></span> note.id
}" title="${note.is_pinned ? "Открепить" : "Закрепить"}">
<span class="iconify" data-icon="mdi:pin${
note.is_pinned ? "-off" : ""
}"></span>
</div> </div>
<div id="archiveBtn" class="notesHeaderBtn" data-id="${note.id}" title="Архивировать"> <div id="archiveBtn" class="notesHeaderBtn" data-id="${
note.id
}" title="Архивировать">
<span class="iconify" data-icon="mdi:archive"></span> <span class="iconify" data-icon="mdi:archive"></span>
</div> </div>
<div id="editBtn" class="notesHeaderBtn" data-id="${note.id}">Ред.</div> <div id="editBtn" class="notesHeaderBtn" data-id="${
<div id="deleteBtn" class="notesHeaderBtn" data-id="${note.id}">Удал.</div> note.id
}">Ред.</div>
<div id="deleteBtn" class="notesHeaderBtn" data-id="${
note.id
}">Удал.</div>
</div> </div>
</div> </div>
<div class="textNote" data-original-content="${note.content.replace( <div class="textNote" data-original-content="${note.content.replace(
@ -1377,7 +1441,7 @@ function addNoteEventListeners() {
} }
const result = await response.json(); const result = await response.json();
// Перезагружаем заметки // Перезагружаем заметки
await loadNotes(true); await loadNotes(true);
} catch (error) { } catch (error) {
@ -1391,7 +1455,11 @@ function addNoteEventListeners() {
document.querySelectorAll("#archiveBtn").forEach((btn) => { document.querySelectorAll("#archiveBtn").forEach((btn) => {
btn.addEventListener("click", async function (event) { btn.addEventListener("click", async function (event) {
const noteId = event.target.closest("#archiveBtn").dataset.id; const noteId = event.target.closest("#archiveBtn").dataset.id;
if (confirm("Архивировать эту заметку? Её можно будет восстановить из настроек.")) { if (
confirm(
"Архивировать эту заметку? Её можно будет восстановить из настроек."
)
) {
try { try {
const response = await fetch(`/api/notes/${noteId}/archive`, { const response = await fetch(`/api/notes/${noteId}/archive`, {
method: "PUT", method: "PUT",
@ -1476,6 +1544,7 @@ function addNoteEventListeners() {
tag: "- [ ] ", tag: "- [ ] ",
}, },
{ id: "editImageBtn", icon: "mdi:image-plus", tag: "image" }, { id: "editImageBtn", icon: "mdi:image-plus", tag: "image" },
{ id: "editPreviewBtn", icon: "mdi:eye", tag: "preview" },
]; ];
markdownButtons.forEach((button) => { markdownButtons.forEach((button) => {
@ -1741,6 +1810,13 @@ function addNoteEventListeners() {
const saveEditNote = async function () { const saveEditNote = async function () {
if (textarea.value.trim() !== "" || editSelectedImages.length > 0) { if (textarea.value.trim() !== "" || editSelectedImages.length > 0) {
try { try {
// Сбрасываем режим предпросмотра перед сохранением
if (isEditPreviewMode) {
isEditPreviewMode = false;
textarea.style.display = "block";
editPreviewContainer.style.display = "none";
}
const response = await fetch(`/api/notes/${noteId}`, { const response = await fetch(`/api/notes/${noteId}`, {
method: "PUT", method: "PUT",
headers: { headers: {
@ -1780,6 +1856,13 @@ function addNoteEventListeners() {
if (!ok) return; if (!ok) return;
} }
// Сбрасываем режим предпросмотра
if (isEditPreviewMode) {
isEditPreviewMode = false;
textarea.style.display = "block";
editPreviewContainer.style.display = "none";
}
// Очистить выбранные новые изображения и превью // Очистить выбранные новые изображения и превью
editSelectedImages.length = 0; editSelectedImages.length = 0;
if (editImagePreviewContainer) { if (editImagePreviewContainer) {
@ -1829,10 +1912,29 @@ function addNoteEventListeners() {
editImageInput.value = ""; editImageInput.value = "";
}); });
// Создаем контейнер для предпросмотра в режиме редактирования
const editPreviewContainer = document.createElement("div");
editPreviewContainer.classList.add("note-preview-container");
editPreviewContainer.style.display = "none";
const editPreviewHeader = document.createElement("div");
editPreviewHeader.classList.add("note-preview-header");
editPreviewHeader.innerHTML = "<span>Предпросмотр:</span>";
const editPreviewContent = document.createElement("div");
editPreviewContent.classList.add("note-preview-content");
editPreviewContainer.appendChild(editPreviewHeader);
editPreviewContainer.appendChild(editPreviewContent);
// Флаг для режима предпросмотра редактирования
let isEditPreviewMode = false;
// Очищаем текущий контент и вставляем markdown кнопки, textarea, элементы для изображений и контейнер с кнопкой сохранить // Очищаем текущий контент и вставляем markdown кнопки, textarea, элементы для изображений и контейнер с кнопкой сохранить
noteContent.innerHTML = ""; noteContent.innerHTML = "";
noteContent.appendChild(markdownButtonsContainer); noteContent.appendChild(markdownButtonsContainer);
noteContent.appendChild(textarea); noteContent.appendChild(textarea);
noteContent.appendChild(editPreviewContainer);
noteContent.appendChild(editImageInput); noteContent.appendChild(editImageInput);
noteContent.appendChild(editImagePreviewContainer); noteContent.appendChild(editImagePreviewContainer);
noteContent.appendChild(saveButtonContainer); noteContent.appendChild(saveButtonContainer);
@ -1853,6 +1955,46 @@ function addNoteEventListeners() {
} else if (button.tag === "color") { } else if (button.tag === "color") {
// Для кнопки цвета открываем диалог выбора цвета // Для кнопки цвета открываем диалог выбора цвета
insertColorTagForEdit(textarea); insertColorTagForEdit(textarea);
} else if (button.tag === "preview") {
// Для кнопки предпросмотра переключаем режим
isEditPreviewMode = !isEditPreviewMode;
if (isEditPreviewMode) {
// Показываем предпросмотр
textarea.style.display = "none";
editPreviewContainer.style.display = "block";
// Получаем содержимое и рендерим его
const content = textarea.value;
if (content.trim()) {
// Парсим markdown и делаем теги кликабельными
const htmlContent = marked.parse(content);
const contentWithTags = makeTagsClickable(htmlContent);
editPreviewContent.innerHTML = contentWithTags;
// Инициализируем lazy loading для изображений в превью
setTimeout(() => {
initLazyLoading();
}, 0);
} else {
editPreviewContent.innerHTML =
'<p style="color: #999; font-style: italic;">Нет содержимого для предпросмотра</p>';
}
// Меняем иконку кнопки
btn.innerHTML =
'<span class="iconify" data-icon="mdi:eye-off"></span>';
btn.title = "Закрыть предпросмотр";
} else {
// Возвращаемся к редактированию
textarea.style.display = "block";
editPreviewContainer.style.display = "none";
// Меняем иконку обратно
btn.innerHTML =
'<span class="iconify" data-icon="mdi:eye"></span>';
btn.title = "Предпросмотр";
}
} else { } else {
insertMarkdownForEdit(textarea, button.tag); insertMarkdownForEdit(textarea, button.tag);
} }
@ -2163,6 +2305,17 @@ async function saveNote() {
selectedImages = []; selectedImages = [];
updateImagePreview(); updateImagePreview();
imageInput.value = ""; imageInput.value = "";
// Сбрасываем режим предпросмотра, если он был активен
if (isPreviewMode) {
isPreviewMode = false;
noteInput.style.display = "block";
notePreviewContainer.style.display = "none";
previewBtn.innerHTML =
'<span class="iconify" data-icon="mdi:eye"></span>';
previewBtn.title = "Предпросмотр";
}
await loadNotes(true); await loadNotes(true);
// Показываем уведомление об успешном сохранении // Показываем уведомление об успешном сохранении
@ -2407,8 +2560,12 @@ async function loadUserInfo() {
if (response.ok) { if (response.ok) {
const user = await response.json(); const user = await response.json();
const userAvatar = document.getElementById("user-avatar"); const userAvatar = document.getElementById("user-avatar");
const userAvatarContainer = document.getElementById("user-avatar-container"); const userAvatarContainer = document.getElementById(
const userAvatarPlaceholder = document.getElementById("user-avatar-placeholder"); "user-avatar-container"
);
const userAvatarPlaceholder = document.getElementById(
"user-avatar-placeholder"
);
// Показываем аватарку или плейсхолдер // Показываем аватарку или плейсхолдер
if (user.avatar) { if (user.avatar) {

View File

@ -222,41 +222,36 @@
style="display: none" style="display: none"
></div> ></div>
</div> </div>
<div class="user-info"> <div class="user-info">
<div <div
id="user-avatar-container" id="user-avatar-container"
class="user-avatar-mini" class="user-avatar-mini"
style="display: none" style="display: none"
title="Перейти в профиль" title="Перейти в профиль"
> >
<img <img id="user-avatar" src="" alt="Аватар" loading="lazy" />
id="user-avatar" </div>
src="" <div
alt="Аватар" id="user-avatar-placeholder"
loading="lazy" class="user-avatar-mini user-avatar-placeholder-mini"
/> style="display: none"
</div> title="Перейти в профиль"
<div >
id="user-avatar-placeholder" <span class="iconify" data-icon="mdi:account"></span>
class="user-avatar-mini user-avatar-placeholder-mini" </div>
style="display: none" <button
title="Перейти в профиль" id="settings-btn"
> class="settings-icon-btn"
<span class="iconify" data-icon="mdi:account"></span> title="Настройки"
</div> >
<button <span class="iconify" data-icon="mdi:cog"></span>
id="settings-btn"
class="settings-icon-btn"
title="Настройки"
>
<span class="iconify" data-icon="mdi:cog"></span>
</button>
<form action="/logout" method="POST" style="display: inline">
<button type="submit" class="logout-btn">
<span class="iconify" data-icon="mdi:logout"></span> Выйти
</button> </button>
</form> <form action="/logout" method="POST" style="display: inline">
</div> <button type="submit" class="logout-btn" title="Выйти">
<span class="iconify" data-icon="mdi:logout"></span>
</button>
</form>
</div>
</header> </header>
<div class="main"> <div class="main">
<div class="markdown-buttons"> <div class="markdown-buttons">
@ -266,7 +261,11 @@
<button class="btnMarkdown" id="italicBtn" title="Курсив"> <button class="btnMarkdown" id="italicBtn" title="Курсив">
<span class="iconify" data-icon="mdi:format-italic"></span> <span class="iconify" data-icon="mdi:format-italic"></span>
</button> </button>
<button class="btnMarkdown" id="strikethroughBtn" title="Перечеркнутый"> <button
class="btnMarkdown"
id="strikethroughBtn"
title="Перечеркнутый"
>
<span class="iconify" data-icon="mdi:format-strikethrough"></span> <span class="iconify" data-icon="mdi:format-strikethrough"></span>
</button> </button>
<button class="btnMarkdown" id="colorBtn" title="Цвет текста"> <button class="btnMarkdown" id="colorBtn" title="Цвет текста">
@ -274,8 +273,15 @@
</button> </button>
<div class="header-dropdown"> <div class="header-dropdown">
<button class="btnMarkdown" id="headerBtn" title="Заголовок"> <button class="btnMarkdown" id="headerBtn" title="Заголовок">
<span class="iconify" data-icon="mdi:format-header-pound"></span> <span
<span class="iconify" data-icon="mdi:menu-down" style="font-size: 10px; margin-left: -2px;"></span> class="iconify"
data-icon="mdi:format-header-pound"
></span>
<span
class="iconify"
data-icon="mdi:menu-down"
style="font-size: 10px; margin-left: -2px"
></span>
</button> </button>
<div class="header-dropdown-menu" id="headerDropdown"> <div class="header-dropdown-menu" id="headerDropdown">
<button data-level="1">H1</button> <button data-level="1">H1</button>
@ -318,6 +324,9 @@
> >
<span class="iconify" data-icon="mdi:image-plus"></span> <span class="iconify" data-icon="mdi:image-plus"></span>
</button> </button>
<button class="btnMarkdown" id="previewBtn" title="Предпросмотр">
<span class="iconify" data-icon="mdi:eye"></span>
</button>
</div> </div>
<textarea <textarea
@ -326,6 +335,18 @@
placeholder="Ваша заметка..." placeholder="Ваша заметка..."
></textarea> ></textarea>
<!-- Контейнер для предпросмотра заметки -->
<div
id="notePreviewContainer"
class="note-preview-container"
style="display: none"
>
<div class="note-preview-header">
<span>Предпросмотр:</span>
</div>
<div id="notePreviewContent" class="note-preview-content"></div>
</div>
<!-- Скрытый input для загрузки изображений --> <!-- Скрытый input для загрузки изображений -->
<input <input
type="file" type="file"

View File

@ -58,12 +58,12 @@
кабинет</span кабинет</span
> >
<div class="user-info"> <div class="user-info">
<a href="/notes" class="back-btn">← Назад к заметкам</a> <a href="/notes" class="back-btn">
<form action="/logout" method="POST" style="display: inline"> <span class="iconify" data-icon="mdi:note-text"></span> К заметкам
<button type="submit" class="logout-btn"> </a>
<span class="iconify" data-icon="mdi:logout"></span> Выйти <a href="/settings" class="back-btn">
</button> <span class="iconify" data-icon="mdi:cog"></span> Настройки
</form> </a>
</div> </div>
</header> </header>
@ -126,54 +126,6 @@
<input type="email" id="email" placeholder="example@example.com" /> <input type="email" id="email" placeholder="example@example.com" />
</div> </div>
<div class="form-group">
<label for="accentColor">Цветовой акцент</label>
<div class="accent-color-picker">
<div
class="color-option"
data-color="#007bff"
style="background-color: #007bff"
title="Синий"
></div>
<div
class="color-option"
data-color="#28a745"
style="background-color: #28a745"
title="Зеленый"
></div>
<div
class="color-option"
data-color="#dc3545"
style="background-color: #dc3545"
title="Красный"
></div>
<div
class="color-option"
data-color="#fd7e14"
style="background-color: #fd7e14"
title="Оранжевый"
></div>
<div
class="color-option"
data-color="#6f42c1"
style="background-color: #6f42c1"
title="Фиолетовый"
></div>
<div
class="color-option"
data-color="#e83e8c"
style="background-color: #e83e8c"
title="Розовый"
></div>
</div>
<input
type="color"
id="accentColor"
value="#007bff"
style="margin-top: 10px"
/>
</div>
<button id="updateProfileBtn" class="btnSave"> <button id="updateProfileBtn" class="btnSave">
Сохранить изменения Сохранить изменения
</button> </button>

View File

@ -11,7 +11,6 @@ const newPasswordInput = document.getElementById("newPassword");
const confirmPasswordInput = document.getElementById("confirmPassword"); const confirmPasswordInput = document.getElementById("confirmPassword");
const changePasswordBtn = document.getElementById("changePasswordBtn"); const changePasswordBtn = document.getElementById("changePasswordBtn");
const messageContainer = document.getElementById("messageContainer"); const messageContainer = document.getElementById("messageContainer");
const accentColorInput = document.getElementById("accentColor");
// Lazy loading для изображений // Lazy loading для изображений
function initLazyLoading() { function initLazyLoading() {
@ -78,12 +77,8 @@ async function loadProfile() {
// Заполняем поля // Заполняем поля
usernameInput.value = user.username || ""; usernameInput.value = user.username || "";
emailInput.value = user.email || ""; emailInput.value = user.email || "";
accentColorInput.value = user.accent_color || "#007bff";
// Обновляем выбранный цвет в цветовых опциях // Применяем цветовой акцент пользователя (для отображения)
updateColorPickerSelection(user.accent_color || "#007bff");
// Применяем цветовой акцент пользователя
const accentColor = user.accent_color || "#007bff"; const accentColor = user.accent_color || "#007bff";
document.documentElement.style.setProperty("--accent-color", accentColor); document.documentElement.style.setProperty("--accent-color", accentColor);
@ -104,17 +99,6 @@ async function loadProfile() {
} }
} }
// Функция для обновления выбора цвета в цветовой палитре
function updateColorPickerSelection(selectedColor) {
const colorOptions = document.querySelectorAll(".color-option");
colorOptions.forEach((option) => {
option.classList.remove("selected");
if (option.dataset.color === selectedColor) {
option.classList.add("selected");
}
});
}
// Обработчик загрузки аватарки // Обработчик загрузки аватарки
avatarInput.addEventListener("change", async function (event) { avatarInput.addEventListener("change", async function (event) {
const file = event.target.files[0]; const file = event.target.files[0];
@ -200,7 +184,6 @@ deleteAvatarBtn.addEventListener("click", async function () {
updateProfileBtn.addEventListener("click", async function () { updateProfileBtn.addEventListener("click", async function () {
const username = usernameInput.value.trim(); const username = usernameInput.value.trim();
const email = emailInput.value.trim(); const email = emailInput.value.trim();
const accentColor = accentColorInput.value;
// Валидация // Валидация
if (!username) { if (!username) {
@ -227,7 +210,6 @@ updateProfileBtn.addEventListener("click", async function () {
body: JSON.stringify({ body: JSON.stringify({
username, username,
email: email || null, email: email || null,
accent_color: accentColor,
}), }),
}); });
@ -238,9 +220,6 @@ updateProfileBtn.addEventListener("click", async function () {
const result = await response.json(); const result = await response.json();
// Применяем новый цветовой акцент
document.documentElement.style.setProperty("--accent-color", accentColor);
showMessage(result.message || "Профиль успешно обновлен", "success"); showMessage(result.message || "Профиль успешно обновлен", "success");
} catch (error) { } catch (error) {
console.error("Ошибка обновления профиля:", error); console.error("Ошибка обновления профиля:", error);
@ -361,23 +340,6 @@ function setupLogoutHandler() {
}); });
} }
// Обработчики для цветовой палитры
function setupColorPicker() {
const colorOptions = document.querySelectorAll(".color-option");
colorOptions.forEach((option) => {
option.addEventListener("click", function () {
const selectedColor = this.dataset.color;
accentColorInput.value = selectedColor;
updateColorPickerSelection(selectedColor);
});
});
// Обработчик для input color
accentColorInput.addEventListener("input", function () {
updateColorPickerSelection(this.value);
});
}
// Загружаем профиль при загрузке страницы // Загружаем профиль при загрузке страницы
document.addEventListener("DOMContentLoaded", function () { document.addEventListener("DOMContentLoaded", function () {
// Проверяем аутентификацию при загрузке страницы // Проверяем аутентификацию при загрузке страницы
@ -387,9 +349,6 @@ document.addEventListener("DOMContentLoaded", function () {
// Инициализируем lazy loading для изображений // Инициализируем lazy loading для изображений
initLazyLoading(); initLazyLoading();
// Настраиваем цветовую палитру
setupColorPicker();
// Добавляем обработчик для кнопки выхода // Добавляем обработчик для кнопки выхода
setupLogoutHandler(); setupLogoutHandler();
}); });

View File

@ -54,22 +54,23 @@
<body> <body>
<div class="container"> <div class="container">
<header class="notes-header"> <header class="notes-header">
<span <span><span class="iconify" data-icon="mdi:cog"></span> Настройки</span>
><span class="iconify" data-icon="mdi:cog"></span> Настройки</span
>
<div class="user-info"> <div class="user-info">
<a href="/profile" class="back-btn">
<span class="iconify" data-icon="mdi:account"></span> Профиль
</a>
<a href="/notes" class="back-btn"> <a href="/notes" class="back-btn">
<span class="iconify" data-icon="mdi:note-text"></span> К заметкам <span class="iconify" data-icon="mdi:note-text"></span> К заметкам
</a> </a>
<a href="/profile" class="back-btn">
<span class="iconify" data-icon="mdi:account"></span> Профиль
</a>
</div> </div>
</header> </header>
<!-- Табы навигации --> <!-- Табы навигации -->
<div class="settings-tabs"> <div class="settings-tabs">
<button class="settings-tab active" data-tab="archive"> <button class="settings-tab active" data-tab="appearance">
<span class="iconify" data-icon="mdi:palette"></span> Внешний вид
</button>
<button class="settings-tab" data-tab="archive">
<span class="iconify" data-icon="mdi:archive"></span> Архив заметок <span class="iconify" data-icon="mdi:archive"></span> Архив заметок
</button> </button>
<button class="settings-tab" data-tab="logs"> <button class="settings-tab" data-tab="logs">
@ -79,8 +80,65 @@
<!-- Контент табов --> <!-- Контент табов -->
<div class="settings-content"> <div class="settings-content">
<!-- Внешний вид -->
<div class="tab-content active" id="appearance-tab">
<h3>Внешний вид</h3>
<div class="form-group">
<label for="settings-accentColor">Цветовой акцент</label>
<div class="accent-color-picker">
<div
class="color-option"
data-color="#007bff"
style="background-color: #007bff"
title="Синий"
></div>
<div
class="color-option"
data-color="#28a745"
style="background-color: #28a745"
title="Зеленый"
></div>
<div
class="color-option"
data-color="#dc3545"
style="background-color: #dc3545"
title="Красный"
></div>
<div
class="color-option"
data-color="#fd7e14"
style="background-color: #fd7e14"
title="Оранжевый"
></div>
<div
class="color-option"
data-color="#6f42c1"
style="background-color: #6f42c1"
title="Фиолетовый"
></div>
<div
class="color-option"
data-color="#e83e8c"
style="background-color: #e83e8c"
title="Розовый"
></div>
</div>
<input
type="color"
id="settings-accentColor"
value="#007bff"
style="margin-top: 10px"
/>
</div>
<button id="updateAppearanceBtn" class="btnSave">
Сохранить изменения
</button>
</div>
<!-- Архив заметок --> <!-- Архив заметок -->
<div class="tab-content active" id="archive-tab"> <div class="tab-content" id="archive-tab">
<h3>Архивные заметки</h3> <h3>Архивные заметки</h3>
<p style="color: #666; font-size: 14px; margin-bottom: 20px"> <p style="color: #666; font-size: 14px; margin-bottom: 20px">
Архивированные заметки можно восстановить или удалить окончательно Архивированные заметки можно восстановить или удалить окончательно
@ -93,7 +151,7 @@
<!-- История действий --> <!-- История действий -->
<div class="tab-content" id="logs-tab"> <div class="tab-content" id="logs-tab">
<h3>История действий</h3> <h3>История действий</h3>
<!-- Фильтры --> <!-- Фильтры -->
<div class="logs-filters"> <div class="logs-filters">
<select id="logTypeFilter" class="log-filter-select"> <select id="logTypeFilter" class="log-filter-select">
@ -107,7 +165,9 @@
<option value="note_pin">Закрепление</option> <option value="note_pin">Закрепление</option>
<option value="note_archive">Архивирование</option> <option value="note_archive">Архивирование</option>
<option value="note_unarchive">Восстановление</option> <option value="note_unarchive">Восстановление</option>
<option value="note_delete_permanent">Окончательное удаление</option> <option value="note_delete_permanent">
Окончательное удаление
</option>
<option value="profile_update">Обновление профиля</option> <option value="profile_update">Обновление профиля</option>
</select> </select>
<button id="refreshLogs" class="btnSave"> <button id="refreshLogs" class="btnSave">
@ -131,14 +191,20 @@
</tbody> </tbody>
</table> </table>
</div> </div>
<div id="logsLoadMore" class="load-more-container" style="display: none"> <div
id="logsLoadMore"
class="load-more-container"
style="display: none"
>
<button id="loadMoreLogsBtn" class="btnSave">Загрузить еще</button> <button id="loadMoreLogsBtn" class="btnSave">Загрузить еще</button>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div id="settings-message-container" class="message-container"></div>
<div class="footer"> <div class="footer">
<p>Создатель: <span>Fovway</span></p> <p>Создатель: <span>Fovway</span></p>
</div> </div>
@ -153,5 +219,3 @@
<script src="/pwa.js"></script> <script src="/pwa.js"></script>
</body> </body>
</html> </html>

View File

@ -3,6 +3,12 @@ let logsOffset = 0;
const logsLimit = 50; const logsLimit = 50;
let hasMoreLogs = true; let hasMoreLogs = true;
// DOM элементы для внешнего вида
const settingsAccentColorInput = document.getElementById(
"settings-accentColor"
);
const updateAppearanceBtn = document.getElementById("updateAppearanceBtn");
// Проверка аутентификации // Проверка аутентификации
async function checkAuthentication() { async function checkAuthentication() {
try { try {
@ -27,19 +33,30 @@ async function checkAuthentication() {
} }
} }
// Загрузка информации о пользователе для применения accent color // Загрузка информации о пользователе для применения accent color и заполнения формы
async function loadUserInfo() { async function loadUserInfo() {
try { try {
const response = await fetch("/api/user"); const response = await fetch("/api/user");
if (response.ok) { if (response.ok) {
const user = await response.json(); const user = await response.json();
const accentColor = user.accent_color || "#007bff"; const accentColor = user.accent_color || "#007bff";
// Применяем цветовой акцент
if ( if (
getComputedStyle(document.documentElement) getComputedStyle(document.documentElement)
.getPropertyValue("--accent-color") .getPropertyValue("--accent-color")
.trim() !== accentColor .trim() !== accentColor
) { ) {
document.documentElement.style.setProperty("--accent-color", accentColor); document.documentElement.style.setProperty(
"--accent-color",
accentColor
);
}
// Заполняем поле цветового акцента в настройках
if (settingsAccentColorInput) {
settingsAccentColorInput.value = accentColor;
updateColorPickerSelection(accentColor);
} }
} }
} catch (error) { } catch (error) {
@ -47,6 +64,33 @@ async function loadUserInfo() {
} }
} }
// Функция для обновления выбора цвета в цветовой палитре
function updateColorPickerSelection(selectedColor) {
const colorOptions = document.querySelectorAll(
"#appearance-tab .color-option"
);
colorOptions.forEach((option) => {
option.classList.remove("selected");
if (option.dataset.color === selectedColor) {
option.classList.add("selected");
}
});
}
// Функция для показа сообщений
function showSettingsMessage(message, type = "success") {
const container = document.getElementById("settings-message-container");
if (container) {
container.textContent = message;
container.className = `message-container ${type}`;
container.style.display = "block";
setTimeout(() => {
container.style.display = "none";
}, 5000);
}
}
// Переключение табов // Переключение табов
function initTabs() { function initTabs() {
const tabs = document.querySelectorAll(".settings-tab"); const tabs = document.querySelectorAll(".settings-tab");
@ -69,6 +113,8 @@ function initTabs() {
loadArchivedNotes(); loadArchivedNotes();
} else if (tabName === "logs") { } else if (tabName === "logs") {
loadLogs(true); loadLogs(true);
} else if (tabName === "appearance") {
// Данные внешнего вида уже загружены в loadUserInfo()
} }
}); });
}); });
@ -77,7 +123,8 @@ function initTabs() {
// Загрузка архивных заметок // Загрузка архивных заметок
async function loadArchivedNotes() { async function loadArchivedNotes() {
const container = document.getElementById("archived-notes-container"); const container = document.getElementById("archived-notes-container");
container.innerHTML = '<p style="text-align: center; color: #999;">Загрузка...</p>'; container.innerHTML =
'<p style="text-align: center; color: #999;">Загрузка...</p>';
try { try {
const response = await fetch("/api/notes/archived"); const response = await fetch("/api/notes/archived");
@ -112,7 +159,8 @@ async function loadArchivedNotes() {
// Преобразуем markdown в HTML для предпросмотра // Преобразуем markdown в HTML для предпросмотра
const htmlContent = marked.parse(note.content); const htmlContent = marked.parse(note.content);
const preview = htmlContent.substring(0, 200) + (htmlContent.length > 200 ? "..." : ""); const preview =
htmlContent.substring(0, 200) + (htmlContent.length > 200 ? "..." : "");
// Изображения // Изображения
let imagesHtml = ""; let imagesHtml = "";
@ -165,9 +213,7 @@ function addArchivedNotesEventListeners() {
} }
// Удаляем элемент из списка // Удаляем элемент из списка
document document.querySelector(`[data-note-id="${noteId}"]`)?.remove();
.querySelector(`[data-note-id="${noteId}"]`)
?.remove();
// Проверяем, остались ли заметки // Проверяем, остались ли заметки
const container = document.getElementById("archived-notes-container"); const container = document.getElementById("archived-notes-container");
@ -190,9 +236,7 @@ function addArchivedNotesEventListeners() {
btn.addEventListener("click", async (e) => { btn.addEventListener("click", async (e) => {
const noteId = e.target.closest("button").dataset.id; const noteId = e.target.closest("button").dataset.id;
if ( if (
confirm( confirm("Удалить эту заметку НАВСЕГДА? Это действие нельзя отменить!")
"Удалить эту заметку НАВСЕГДА? Это действие нельзя отменить!"
)
) { ) {
try { try {
const response = await fetch(`/api/notes/archived/${noteId}`, { const response = await fetch(`/api/notes/archived/${noteId}`, {
@ -204,9 +248,7 @@ function addArchivedNotesEventListeners() {
} }
// Удаляем элемент из списка // Удаляем элемент из списка
document document.querySelector(`[data-note-id="${noteId}"]`)?.remove();
.querySelector(`[data-note-id="${noteId}"]`)
?.remove();
// Проверяем, остались ли заметки // Проверяем, остались ли заметки
const container = document.getElementById("archived-notes-container"); const container = document.getElementById("archived-notes-container");
@ -237,7 +279,8 @@ async function loadLogs(reset = false) {
const filterValue = document.getElementById("logTypeFilter").value; const filterValue = document.getElementById("logTypeFilter").value;
if (reset) { if (reset) {
tbody.innerHTML = '<tr><td colspan="4" style="text-align: center;">Загрузка...</td></tr>'; tbody.innerHTML =
'<tr><td colspan="4" style="text-align: center;">Загрузка...</td></tr>';
} }
try { try {
@ -270,7 +313,7 @@ async function loadLogs(reset = false) {
logs.forEach((log) => { logs.forEach((log) => {
const row = document.createElement("tr"); const row = document.createElement("tr");
// Форматируем дату // Форматируем дату
const created = new Date(log.created_at.replace(" ", "T") + "Z"); const created = new Date(log.created_at.replace(" ", "T") + "Z");
const dateStr = new Intl.DateTimeFormat("ru-RU", { const dateStr = new Intl.DateTimeFormat("ru-RU", {
@ -301,7 +344,9 @@ async function loadLogs(reset = false) {
row.innerHTML = ` row.innerHTML = `
<td>${dateStr}</td> <td>${dateStr}</td>
<td><span class="log-action-badge log-action-${log.action_type}">${actionText}</span></td> <td><span class="log-action-badge log-action-${
log.action_type
}">${actionText}</span></td>
<td>${log.details || "-"}</td> <td>${log.details || "-"}</td>
<td>${log.ip_address || "-"}</td> <td>${log.ip_address || "-"}</td>
`; `;
@ -337,9 +382,6 @@ document.addEventListener("DOMContentLoaded", async function () {
// Инициализируем табы // Инициализируем табы
initTabs(); initTabs();
// Загружаем архивные заметки по умолчанию
loadArchivedNotes();
// Обработчик фильтра логов // Обработчик фильтра логов
document.getElementById("logTypeFilter").addEventListener("change", () => { document.getElementById("logTypeFilter").addEventListener("change", () => {
loadLogs(true); loadLogs(true);
@ -354,6 +396,67 @@ document.addEventListener("DOMContentLoaded", async function () {
document.getElementById("loadMoreLogsBtn").addEventListener("click", () => { document.getElementById("loadMoreLogsBtn").addEventListener("click", () => {
loadLogs(false); loadLogs(false);
}); });
// Обработчик кнопки сохранения внешнего вида
if (updateAppearanceBtn) {
updateAppearanceBtn.addEventListener("click", async function () {
const accentColor = settingsAccentColorInput.value;
try {
const response = await fetch("/api/user/profile", {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
accent_color: accentColor,
}),
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || "Ошибка обновления цветового акцента");
}
const result = await response.json();
// Применяем новый цветовой акцент
document.documentElement.style.setProperty(
"--accent-color",
accentColor
);
showSettingsMessage(
result.message || "Цветовой акцент успешно обновлен",
"success"
);
} catch (error) {
console.error("Ошибка обновления цветового акцента:", error);
showSettingsMessage(error.message, "error");
}
});
}
// Обработчики для цветовой палитры
function setupAppearanceColorPicker() {
const colorOptions = document.querySelectorAll(
"#appearance-tab .color-option"
);
colorOptions.forEach((option) => {
option.addEventListener("click", function () {
const selectedColor = this.dataset.color;
settingsAccentColorInput.value = selectedColor;
updateColorPickerSelection(selectedColor);
});
});
// Обработчик для input color
if (settingsAccentColorInput) {
settingsAccentColorInput.addEventListener("input", function () {
updateColorPickerSelection(this.value);
});
}
}
setupAppearanceColorPicker();
}); });

View File

@ -47,11 +47,7 @@ header .iconify {
margin-right: 8px; margin-right: 8px;
} }
/* Стили для иконок в кнопках */ /* Стили для иконок в кнопках удалены, так как теперь кнопки имеют свои стили */
.logout-btn .iconify {
font-size: 14px;
margin-right: 6px;
}
/* Стили для иконок в секциях */ /* Стили для иконок в секциях */
.search-title .iconify, .search-title .iconify,
@ -82,10 +78,7 @@ header .iconify[data-icon="mdi:account"],
color: #9c27b0; color: #9c27b0;
} }
/* Иконка выхода - красный */ /* Цвет иконки выхода задается в стилях .logout-btn .iconify */
.logout-btn .iconify[data-icon="mdi:logout"] {
color: #f44336;
}
/* Иконка входа - синий */ /* Иконка входа - синий */
header .iconify[data-icon="mdi:login"] { header .iconify[data-icon="mdi:login"] {
@ -279,12 +272,19 @@ header {
font-size: 24px; font-size: 24px;
} }
.user-avatar-placeholder-mini .iconify {
margin: 0;
padding: 0;
line-height: 1;
vertical-align: baseline;
}
/* Иконка настроек */ /* Иконка настроек */
.settings-icon-btn { .settings-icon-btn {
width: 40px; width: 40px;
height: 40px; height: 40px;
border-radius: 50%; border-radius: 50%;
border: 2px solid #ddd; border: none;
background: white; background: white;
cursor: pointer; cursor: pointer;
display: flex; display: flex;
@ -296,7 +296,6 @@ header {
.settings-icon-btn:hover { .settings-icon-btn:hover {
background: var(--accent-color, #007bff); background: var(--accent-color, #007bff);
border-color: var(--accent-color, #007bff);
color: white; color: white;
transform: rotate(45deg); transform: rotate(45deg);
} }
@ -305,6 +304,10 @@ header {
font-size: 20px; font-size: 20px;
color: #666; color: #666;
transition: color 0.3s ease; transition: color 0.3s ease;
margin: 0;
padding: 0;
line-height: 1;
vertical-align: baseline;
} }
.settings-icon-btn:hover .iconify { .settings-icon-btn:hover .iconify {
@ -312,18 +315,34 @@ header {
} }
.logout-btn { .logout-btn {
padding: 8px 16px; width: 40px;
height: 40px;
border-radius: 50%;
border: none;
background: white;
cursor: pointer; cursor: pointer;
border: 1px solid #ddd; display: flex;
background-color: #f8f9fa; align-items: center;
border-radius: 5px; justify-content: center;
font-size: 14px;
color: #dc3545;
transition: all 0.3s ease; transition: all 0.3s ease;
flex-shrink: 0;
} }
.logout-btn:hover { .logout-btn:hover {
background-color: #dc3545; background-color: #dc3545;
}
.logout-btn .iconify {
font-size: 20px;
color: #dc3545;
transition: color 0.3s ease;
margin: 0;
padding: 0;
line-height: 1;
vertical-align: baseline;
}
.logout-btn:hover .iconify {
color: white; color: white;
} }
@ -929,26 +948,42 @@ textarea:focus {
} }
.message-container { .message-container {
margin-top: 20px; position: fixed;
margin-bottom: 80px; /* Отступ снизу, чтобы контент не обрезался футером */ top: 20px;
padding: 10px; left: 50%;
border-radius: 5px; transform: translateX(-50%);
padding: 12px 20px;
border-radius: 8px;
text-align: center; text-align: center;
display: none; display: none;
z-index: 10000;
min-width: 300px;
max-width: 500px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
font-weight: 500;
}
/* Адаптивность для мобильных устройств */
@media (max-width: 768px) {
.message-container {
min-width: 90vw;
max-width: 90vw;
left: 5vw;
transform: none;
padding: 10px 15px;
}
} }
.message-container.success { .message-container.success {
background-color: #d4edda; background-color: #d4edda;
color: #155724; color: #155724;
border: 1px solid #c3e6cb; border: 1px solid #c3e6cb;
display: block;
} }
.message-container.error { .message-container.error {
background-color: #f8d7da; background-color: #f8d7da;
color: #721c24; color: #721c24;
border: 1px solid #f5c6cb; border: 1px solid #f5c6cb;
display: block;
} }
.back-btn { .back-btn {
@ -1508,20 +1543,21 @@ textarea:focus {
/* Адаптируем заголовок заметок */ /* Адаптируем заголовок заметок */
.notes-header { .notes-header {
flex-direction: column; flex-direction: row;
align-items: flex-start; align-items: center;
gap: 10px; gap: 10px;
width: 100%; width: 100%;
} }
.notes-header-left { .notes-header-left {
width: 100%; flex: 1;
min-width: 0;
} }
.user-info { .user-info {
width: 100%; flex-shrink: 0;
justify-content: space-between; justify-content: flex-end;
flex-wrap: wrap; flex-wrap: nowrap;
gap: 10px; gap: 10px;
} }
@ -1585,6 +1621,85 @@ textarea:focus {
color: #495057; color: #495057;
} }
/* Стили для контейнера предпросмотра заметки */
.note-preview-container {
margin: 15px 0;
padding: 15px;
background: #f8f9fa;
border: 2px solid #007bff;
border-radius: 8px;
min-height: 200px;
max-height: 600px;
overflow-y: auto;
}
.note-preview-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 2px solid #dee2e6;
font-weight: 600;
color: #007bff;
font-size: 16px;
}
.note-preview-content {
background: white;
padding: 15px;
border-radius: 6px;
min-height: 150px;
}
.note-preview-content h1,
.note-preview-content h2,
.note-preview-content h3,
.note-preview-content h4,
.note-preview-content h5,
.note-preview-content h6 {
margin-top: 0.5em;
margin-bottom: 0.5em;
}
.note-preview-content p {
margin: 0.5em 0;
}
.note-preview-content ul,
.note-preview-content ol {
margin: 0.5em 0;
padding-left: 2em;
}
.note-preview-content code {
background: #f8f9fa;
padding: 2px 6px;
border-radius: 4px;
font-family: "Courier New", monospace;
}
.note-preview-content pre {
background: #f8f9fa;
padding: 10px;
border-radius: 6px;
overflow-x: auto;
}
.note-preview-content blockquote {
border-left: 4px solid #007bff;
padding-left: 15px;
margin: 10px 0;
color: #495057;
}
.note-preview-content img {
max-width: 100%;
height: auto;
border-radius: 6px;
margin: 10px 0;
}
.clear-images-btn { .clear-images-btn {
background: #dc3545; background: #dc3545;
color: white; color: white;