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

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

View File

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

View File

@ -58,12 +58,12 @@
кабинет</span
>
<div class="user-info">
<a href="/notes" class="back-btn">← Назад к заметкам</a>
<form action="/logout" method="POST" style="display: inline">
<button type="submit" class="logout-btn">
<span class="iconify" data-icon="mdi:logout"></span> Выйти
</button>
</form>
<a href="/notes" class="back-btn">
<span class="iconify" data-icon="mdi:note-text"></span> К заметкам
</a>
<a href="/settings" class="back-btn">
<span class="iconify" data-icon="mdi:cog"></span> Настройки
</a>
</div>
</header>
@ -126,54 +126,6 @@
<input type="email" id="email" placeholder="example@example.com" />
</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>

View File

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

View File

@ -54,22 +54,23 @@
<body>
<div class="container">
<header class="notes-header">
<span
><span class="iconify" data-icon="mdi:cog"></span> Настройки</span
>
<span><span class="iconify" data-icon="mdi:cog"></span> Настройки</span>
<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">
<span class="iconify" data-icon="mdi:note-text"></span> К заметкам
</a>
<a href="/profile" class="back-btn">
<span class="iconify" data-icon="mdi:account"></span> Профиль
</a>
</div>
</header>
<!-- Табы навигации -->
<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> Архив заметок
</button>
<button class="settings-tab" data-tab="logs">
@ -79,8 +80,65 @@
<!-- Контент табов -->
<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>
<p style="color: #666; font-size: 14px; margin-bottom: 20px">
Архивированные заметки можно восстановить или удалить окончательно
@ -93,7 +151,7 @@
<!-- История действий -->
<div class="tab-content" id="logs-tab">
<h3>История действий</h3>
<!-- Фильтры -->
<div class="logs-filters">
<select id="logTypeFilter" class="log-filter-select">
@ -107,7 +165,9 @@
<option value="note_pin">Закрепление</option>
<option value="note_archive">Архивирование</option>
<option value="note_unarchive">Восстановление</option>
<option value="note_delete_permanent">Окончательное удаление</option>
<option value="note_delete_permanent">
Окончательное удаление
</option>
<option value="profile_update">Обновление профиля</option>
</select>
<button id="refreshLogs" class="btnSave">
@ -131,14 +191,20 @@
</tbody>
</table>
</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>
</div>
</div>
</div>
</div>
<div id="settings-message-container" class="message-container"></div>
<div class="footer">
<p>Создатель: <span>Fovway</span></p>
</div>
@ -153,5 +219,3 @@
<script src="/pwa.js"></script>
</body>
</html>

View File

@ -3,6 +3,12 @@ let logsOffset = 0;
const logsLimit = 50;
let hasMoreLogs = true;
// DOM элементы для внешнего вида
const settingsAccentColorInput = document.getElementById(
"settings-accentColor"
);
const updateAppearanceBtn = document.getElementById("updateAppearanceBtn");
// Проверка аутентификации
async function checkAuthentication() {
try {
@ -27,19 +33,30 @@ async function checkAuthentication() {
}
}
// Загрузка информации о пользователе для применения accent color
// Загрузка информации о пользователе для применения accent color и заполнения формы
async function loadUserInfo() {
try {
const response = await fetch("/api/user");
if (response.ok) {
const user = await response.json();
const accentColor = user.accent_color || "#007bff";
// Применяем цветовой акцент
if (
getComputedStyle(document.documentElement)
.getPropertyValue("--accent-color")
.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) {
@ -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() {
const tabs = document.querySelectorAll(".settings-tab");
@ -69,6 +113,8 @@ function initTabs() {
loadArchivedNotes();
} else if (tabName === "logs") {
loadLogs(true);
} else if (tabName === "appearance") {
// Данные внешнего вида уже загружены в loadUserInfo()
}
});
});
@ -77,7 +123,8 @@ function initTabs() {
// Загрузка архивных заметок
async function loadArchivedNotes() {
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 {
const response = await fetch("/api/notes/archived");
@ -112,7 +159,8 @@ async function loadArchivedNotes() {
// Преобразуем markdown в HTML для предпросмотра
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 = "";
@ -165,9 +213,7 @@ function addArchivedNotesEventListeners() {
}
// Удаляем элемент из списка
document
.querySelector(`[data-note-id="${noteId}"]`)
?.remove();
document.querySelector(`[data-note-id="${noteId}"]`)?.remove();
// Проверяем, остались ли заметки
const container = document.getElementById("archived-notes-container");
@ -190,9 +236,7 @@ function addArchivedNotesEventListeners() {
btn.addEventListener("click", async (e) => {
const noteId = e.target.closest("button").dataset.id;
if (
confirm(
"Удалить эту заметку НАВСЕГДА? Это действие нельзя отменить!"
)
confirm("Удалить эту заметку НАВСЕГДА? Это действие нельзя отменить!")
) {
try {
const response = await fetch(`/api/notes/archived/${noteId}`, {
@ -204,9 +248,7 @@ function addArchivedNotesEventListeners() {
}
// Удаляем элемент из списка
document
.querySelector(`[data-note-id="${noteId}"]`)
?.remove();
document.querySelector(`[data-note-id="${noteId}"]`)?.remove();
// Проверяем, остались ли заметки
const container = document.getElementById("archived-notes-container");
@ -237,7 +279,8 @@ async function loadLogs(reset = false) {
const filterValue = document.getElementById("logTypeFilter").value;
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 {
@ -270,7 +313,7 @@ async function loadLogs(reset = false) {
logs.forEach((log) => {
const row = document.createElement("tr");
// Форматируем дату
const created = new Date(log.created_at.replace(" ", "T") + "Z");
const dateStr = new Intl.DateTimeFormat("ru-RU", {
@ -301,7 +344,9 @@ async function loadLogs(reset = false) {
row.innerHTML = `
<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.ip_address || "-"}</td>
`;
@ -337,9 +382,6 @@ document.addEventListener("DOMContentLoaded", async function () {
// Инициализируем табы
initTabs();
// Загружаем архивные заметки по умолчанию
loadArchivedNotes();
// Обработчик фильтра логов
document.getElementById("logTypeFilter").addEventListener("change", () => {
loadLogs(true);
@ -354,6 +396,67 @@ document.addEventListener("DOMContentLoaded", async function () {
document.getElementById("loadMoreLogsBtn").addEventListener("click", () => {
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;
}
/* Стили для иконок в кнопках */
.logout-btn .iconify {
font-size: 14px;
margin-right: 6px;
}
/* Стили для иконок в кнопках удалены, так как теперь кнопки имеют свои стили */
/* Стили для иконок в секциях */
.search-title .iconify,
@ -82,10 +78,7 @@ header .iconify[data-icon="mdi:account"],
color: #9c27b0;
}
/* Иконка выхода - красный */
.logout-btn .iconify[data-icon="mdi:logout"] {
color: #f44336;
}
/* Цвет иконки выхода задается в стилях .logout-btn .iconify */
/* Иконка входа - синий */
header .iconify[data-icon="mdi:login"] {
@ -279,12 +272,19 @@ header {
font-size: 24px;
}
.user-avatar-placeholder-mini .iconify {
margin: 0;
padding: 0;
line-height: 1;
vertical-align: baseline;
}
/* Иконка настроек */
.settings-icon-btn {
width: 40px;
height: 40px;
border-radius: 50%;
border: 2px solid #ddd;
border: none;
background: white;
cursor: pointer;
display: flex;
@ -296,7 +296,6 @@ header {
.settings-icon-btn:hover {
background: var(--accent-color, #007bff);
border-color: var(--accent-color, #007bff);
color: white;
transform: rotate(45deg);
}
@ -305,6 +304,10 @@ header {
font-size: 20px;
color: #666;
transition: color 0.3s ease;
margin: 0;
padding: 0;
line-height: 1;
vertical-align: baseline;
}
.settings-icon-btn:hover .iconify {
@ -312,18 +315,34 @@ header {
}
.logout-btn {
padding: 8px 16px;
width: 40px;
height: 40px;
border-radius: 50%;
border: none;
background: white;
cursor: pointer;
border: 1px solid #ddd;
background-color: #f8f9fa;
border-radius: 5px;
font-size: 14px;
color: #dc3545;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
flex-shrink: 0;
}
.logout-btn:hover {
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;
}
@ -929,26 +948,42 @@ textarea:focus {
}
.message-container {
margin-top: 20px;
margin-bottom: 80px; /* Отступ снизу, чтобы контент не обрезался футером */
padding: 10px;
border-radius: 5px;
position: fixed;
top: 20px;
left: 50%;
transform: translateX(-50%);
padding: 12px 20px;
border-radius: 8px;
text-align: center;
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 {
background-color: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
display: block;
}
.message-container.error {
background-color: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
display: block;
}
.back-btn {
@ -1508,20 +1543,21 @@ textarea:focus {
/* Адаптируем заголовок заметок */
.notes-header {
flex-direction: column;
align-items: flex-start;
flex-direction: row;
align-items: center;
gap: 10px;
width: 100%;
}
.notes-header-left {
width: 100%;
flex: 1;
min-width: 0;
}
.user-info {
width: 100%;
justify-content: space-between;
flex-wrap: wrap;
flex-shrink: 0;
justify-content: flex-end;
flex-wrap: nowrap;
gap: 10px;
}
@ -1585,6 +1621,85 @@ textarea:focus {
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 {
background: #dc3545;
color: white;