Обновлены функции обработки IP-адресов и улучшен интерфейс редактирования заметок

- Настроена обработка IP-адресов с учетом различных заголовков и удалением порта из IPv6 и IPv4 адресов.
- Добавлены новые кнопки для работы с заголовками в редакторе заметок, включая выпадающее меню для выбора уровня заголовка.
- Реализованы индикаторы для дней с созданными и отредактированными заметками в календаре.
- Обновлены стили для улучшения адаптивности интерфейса и визуального отображения элементов управления.
This commit is contained in:
Fovway 2025-10-25 00:51:29 +07:00
parent dd2a6cfa1a
commit 283e8cad63
3 changed files with 267 additions and 50 deletions

View File

@ -473,13 +473,21 @@ function insertMarkdown(tag) {
return;
}
if (selected.startsWith(tag) && selected.endsWith(tag)) {
// Если теги уже есть, удаляем их
// Определяем, какие теги оборачивают текст (нуждаются в двойных тегах)
const wrappingTags = ["**", "*", "`"];
const isWrappingTag = wrappingTags.some(wrapTag => tag.startsWith(wrapTag));
if (isWrappingTag && selected.startsWith(tag) && selected.endsWith(tag)) {
// Если оборачивающие теги уже есть, удаляем их
noteInput.value = `${before}${selected.slice(
tag.length,
-tag.length
)}${after}`;
noteInput.setSelectionRange(start, end - 2 * tag.length);
} else if (!isWrappingTag && selected.startsWith(tag)) {
// Если одинарные теги (заголовки, списки) уже есть, удаляем их
noteInput.value = `${before}${selected.slice(tag.length)}${after}`;
noteInput.setSelectionRange(start, end - tag.length);
} else if (selected.trim() === "") {
// Если текст не выделен
if (tag === "[Текст ссылки](URL)") {
@ -491,7 +499,7 @@ function insertMarkdown(tag) {
tag === "- " ||
tag === "1. " ||
tag === "> " ||
tag === "# " ||
/^#{1,6} $/.test(tag) ||
tag === "- [ ] "
) {
// Для списка, цитаты, заголовка и чекбокса помещаем курсор после тега
@ -515,7 +523,7 @@ function insertMarkdown(tag) {
tag === "- " ||
tag === "1. " ||
tag === "> " ||
tag === "# " ||
/^#{1,6} $/.test(tag) ||
tag === "- [ ] "
) {
// Для списка, цитаты, заголовка и чекбокса добавляем тег перед выделенным текстом
@ -609,13 +617,21 @@ function insertMarkdownForEdit(textarea, tag) {
return;
}
if (selected.startsWith(tag) && selected.endsWith(tag)) {
// Если теги уже есть, удаляем их
// Определяем, какие теги оборачивают текст (нуждаются в двойных тегах)
const wrappingTags = ["**", "*", "`"];
const isWrappingTag = wrappingTags.some(wrapTag => tag.startsWith(wrapTag));
if (isWrappingTag && selected.startsWith(tag) && selected.endsWith(tag)) {
// Если оборачивающие теги уже есть, удаляем их
textarea.value = `${before}${selected.slice(
tag.length,
-tag.length
)}${after}`;
textarea.setSelectionRange(start, end - 2 * tag.length);
} else if (!isWrappingTag && selected.startsWith(tag)) {
// Если одинарные теги (заголовки, списки) уже есть, удаляем их
textarea.value = `${before}${selected.slice(tag.length)}${after}`;
textarea.setSelectionRange(start, end - tag.length);
} else if (selected.trim() === "") {
// Если текст не выделен
if (tag === "[Текст ссылки](URL)") {
@ -627,7 +643,7 @@ function insertMarkdownForEdit(textarea, tag) {
tag === "- " ||
tag === "1. " ||
tag === "> " ||
tag === "# " ||
/^#{1,6} $/.test(tag) ||
tag === "- [ ] "
) {
// Для списка, цитаты, заголовка и чекбокса помещаем курсор после тега
@ -651,7 +667,7 @@ function insertMarkdownForEdit(textarea, tag) {
tag === "- " ||
tag === "1. " ||
tag === "> " ||
tag === "# " ||
/^#{1,6} $/.test(tag) ||
tag === "- [ ] "
) {
// Для списка, цитаты, заголовка и чекбокса добавляем тег перед выделенным текстом
@ -792,6 +808,20 @@ colorBtn.addEventListener("click", function () {
// Обработчик кнопки заголовка - открываем выпадающее меню
headerBtn.addEventListener("click", function (event) {
event.stopPropagation();
// Проверяем позицию и корректируем если нужно
const rect = headerDropdown.getBoundingClientRect();
const viewportWidth = window.innerWidth;
// Если меню выходит за правую границу, позиционируем его слева
if (rect.right > viewportWidth) {
headerDropdown.style.left = 'auto';
headerDropdown.style.right = '0';
} else {
headerDropdown.style.left = '0';
headerDropdown.style.right = 'auto';
}
headerDropdown.classList.toggle("show");
});
@ -1380,9 +1410,9 @@ async function renderNotes(notes) {
const created = parseSQLiteUtc(note.created_at);
if (note.updated_at && note.created_at !== note.updated_at) {
const updated = parseSQLiteUtc(note.updated_at);
dateDisplay = `Создано: ${formatLocalDateTime(
dateDisplay = `${formatLocalDateTime(
created
)} Изменено: ${formatLocalDateTime(updated)}`;
)} <span class="iconify" data-icon="mdi:pencil" style="font-size: 12px; margin-left: 8px;"></span> ${formatLocalDateTime(updated)}`;
} else {
dateDisplay = formatLocalDateTime(created);
}
@ -1575,7 +1605,7 @@ function addNoteEventListeners() {
// Обработчик редактирования
document.querySelectorAll("#editBtn").forEach((btn) => {
btn.addEventListener("click", function (event) {
const noteId = event.target.dataset.id;
const noteId = event.target.closest("#editBtn").dataset.id;
const noteContainer = event.target.closest("#note");
const noteContent = noteContainer.querySelector(".textNote");
@ -1596,8 +1626,9 @@ function addNoteEventListeners() {
const markdownButtons = [
{ id: "editBoldBtn", icon: "mdi:format-bold", tag: "**" },
{ id: "editItalicBtn", icon: "mdi:format-italic", tag: "*" },
{ id: "editStrikethroughBtn", icon: "mdi:format-strikethrough", tag: "~~" },
{ id: "editColorBtn", icon: "mdi:palette", tag: "color" },
{ id: "editHeaderBtn", icon: "mdi:format-header-1", tag: "# " },
{ id: "editHeaderBtn", icon: "mdi:format-header-pound", tag: "header" },
{ id: "editListBtn", icon: "mdi:format-list-bulleted", tag: "- " },
{
id: "editNumberedListBtn",
@ -1617,11 +1648,66 @@ function addNoteEventListeners() {
];
markdownButtons.forEach((button) => {
const btn = document.createElement("button");
btn.classList.add("btnMarkdown");
btn.id = button.id;
btn.innerHTML = `<span class="iconify" data-icon="${button.icon}"></span>`;
markdownButtonsContainer.appendChild(btn);
if (button.tag === "header") {
// Создаем контейнер для кнопки заголовка с dropdown
const headerContainer = document.createElement("div");
headerContainer.classList.add("header-dropdown");
headerContainer.style.position = "relative";
headerContainer.style.display = "inline-block";
const headerBtn = document.createElement("button");
headerBtn.classList.add("btnMarkdown");
headerBtn.id = button.id;
headerBtn.innerHTML = `
<span class="iconify" data-icon="${button.icon}"></span>
<span class="iconify" data-icon="mdi:menu-down" style="font-size: 10px; margin-left: -2px"></span>
`;
const headerDropdown = document.createElement("div");
headerDropdown.classList.add("header-dropdown-menu");
headerDropdown.style.display = "none";
// Создаем опции для каждого уровня заголовка
for (let i = 1; i <= 6; i++) {
const headerOption = document.createElement("button");
headerOption.type = "button";
headerOption.textContent = `H${i}`;
headerOption.dataset.level = i;
headerOption.addEventListener("click", function (e) {
e.stopPropagation();
const headerTag = "#".repeat(i) + " ";
insertMarkdownForEdit(textarea, headerTag);
headerDropdown.style.display = "none";
headerBtn.classList.remove("active");
});
headerDropdown.appendChild(headerOption);
}
// Обработчик открытия/закрытия dropdown
headerBtn.addEventListener("click", function (e) {
e.stopPropagation();
headerDropdown.style.display = headerDropdown.style.display === "none" ? "block" : "none";
headerBtn.classList.toggle("active");
});
// Закрытие dropdown при клике вне его
document.addEventListener("click", function closeHeaderDropdown(e) {
if (!headerContainer.contains(e.target)) {
headerDropdown.style.display = "none";
headerBtn.classList.remove("active");
}
});
headerContainer.appendChild(headerBtn);
headerContainer.appendChild(headerDropdown);
markdownButtonsContainer.appendChild(headerContainer);
} else {
const btn = document.createElement("button");
btn.classList.add("btnMarkdown");
btn.id = button.id;
btn.innerHTML = `<span class="iconify" data-icon="${button.icon}"></span>`;
markdownButtonsContainer.appendChild(btn);
}
});
// Создаем textarea с уже существующим классом textInput
@ -2016,6 +2102,10 @@ function addNoteEventListeners() {
// Добавляем обработчики для markdown кнопок редактирования
markdownButtons.forEach((button) => {
if (button.tag === "header") {
// Header имеет собственный обработчик в dropdown
return;
}
const btn = document.getElementById(button.id);
btn.addEventListener("click", function () {
if (button.tag === "image") {
@ -2098,6 +2188,7 @@ function addTagClickListeners() {
// Перерисовываем заметки и теги
await renderNotes(allNotes);
renderTags();
renderTagsMobile();
updateFilterIndicator();
});
});
@ -2743,7 +2834,7 @@ function renderCalendar() {
const firstDay = new Date(year, month, 1);
// Получаем последний день месяца
const lastDay = new Date(year, month + 1, 0);
// Получаем день недели первого дня (0 - воскресенье, 1 - понедельник и т.д.)
// Получаем день недели первого дня (0 - воскресенье, 1 - воскресенье, 1 - понедельник и т.д.)
let firstDayOfWeek = firstDay.getDay();
// Преобразуем так, чтобы понедельник был первым днем (0)
firstDayOfWeek = firstDayOfWeek === 0 ? 6 : firstDayOfWeek - 1;
@ -2751,11 +2842,17 @@ function renderCalendar() {
// Очищаем календарь
calendarDays.innerHTML = "";
// Создаём Set дат, когда были созданы заметки (используем created_at)
const noteDates = new Set();
// Создаём Set дат, когда были созданы заметки (зеленые кружки)
const createdNoteDates = new Set();
// Создаём Set дат, когда были отредактированы заметки (желтые кружки)
const editedNoteDates = new Set();
allNotes.forEach((note) => {
if (note.created_at) {
noteDates.add(formatDateFromTimestamp(note.created_at));
createdNoteDates.add(formatDateFromTimestamp(note.created_at));
}
if (note.updated_at && note.created_at !== note.updated_at) {
editedNoteDates.add(formatDateFromTimestamp(note.updated_at));
}
});
@ -2777,9 +2874,12 @@ function renderCalendar() {
dayDiv.dataset.date = dateStr;
// Проверяем, есть ли заметки на этот день
if (noteDates.has(dateStr)) {
if (createdNoteDates.has(dateStr)) {
dayDiv.classList.add("has-notes");
}
if (editedNoteDates.has(dateStr)) {
dayDiv.classList.add("has-edited-notes");
}
// Проверяем, выбран ли этот день
if (selectedDateFilter === dateStr) {
@ -2817,9 +2917,12 @@ function renderCalendar() {
}
// Проверяем, есть ли заметки на этот день
if (noteDates.has(dateStr)) {
if (createdNoteDates.has(dateStr)) {
dayDiv.classList.add("has-notes");
}
if (editedNoteDates.has(dateStr)) {
dayDiv.classList.add("has-edited-notes");
}
// Проверяем, выбран ли этот день
if (selectedDateFilter === dateStr) {
@ -2852,9 +2955,12 @@ function renderCalendar() {
dayDiv.dataset.date = dateStr;
// Проверяем, есть ли заметки на этот день
if (noteDates.has(dateStr)) {
if (createdNoteDates.has(dateStr)) {
dayDiv.classList.add("has-notes");
}
if (editedNoteDates.has(dateStr)) {
dayDiv.classList.add("has-edited-notes");
}
// Проверяем, выбран ли этот день
if (selectedDateFilter === dateStr) {
@ -2945,7 +3051,9 @@ window.clearFilter = async function () {
await renderNotes(allNotes);
renderCalendar();
renderCalendarMobile();
renderTags();
renderTagsMobile();
updateFilterIndicator();
};
@ -3072,11 +3180,19 @@ function renderCalendarMobile() {
// Очищаем календарь
calendarDays.innerHTML = "";
// Создаём Set дат, когда были созданы заметки (используем created_at)
const noteDates = new Set();
// Создаём Set дат, когда были созданы заметки (зеленые кружки)
const createdNoteDates = new Set();
allNotes.forEach((note) => {
if (note.created_at) {
noteDates.add(formatDateFromTimestamp(note.created_at));
createdNoteDates.add(formatDateFromTimestamp(note.created_at));
}
});
// Создаём Set дат, когда были отредактированы заметки (желтые кружки)
const editedNoteDates = new Set();
allNotes.forEach((note) => {
if (note.updated_at && note.created_at !== note.updated_at) {
editedNoteDates.add(formatDateFromTimestamp(note.updated_at));
}
});
@ -3098,9 +3214,12 @@ function renderCalendarMobile() {
dayDiv.dataset.date = dateStr;
// Проверяем, есть ли заметки на этот день
if (noteDates.has(dateStr)) {
if (createdNoteDates.has(dateStr)) {
dayDiv.classList.add("has-notes");
}
if (editedNoteDates.has(dateStr)) {
dayDiv.classList.add("has-edited-notes");
}
// Проверяем, выбран ли этот день
if (selectedDateFilter === dateStr) {
@ -3138,9 +3257,12 @@ function renderCalendarMobile() {
}
// Проверяем, есть ли заметки на этот день
if (noteDates.has(dateStr)) {
if (createdNoteDates.has(dateStr)) {
dayDiv.classList.add("has-notes");
}
if (editedNoteDates.has(dateStr)) {
dayDiv.classList.add("has-edited-notes");
}
// Проверяем, выбран ли этот день
if (selectedDateFilter === dateStr) {
@ -3173,9 +3295,12 @@ function renderCalendarMobile() {
dayDiv.dataset.date = dateStr;
// Проверяем, есть ли заметки на этот день
if (noteDates.has(dateStr)) {
if (createdNoteDates.has(dateStr)) {
dayDiv.classList.add("has-notes");
}
if (editedNoteDates.has(dateStr)) {
dayDiv.classList.add("has-edited-notes");
}
// Проверяем, выбран ли этот день
if (selectedDateFilter === dateStr) {

View File

@ -419,7 +419,7 @@ header {
box-sizing: border-box;
word-wrap: break-word;
overflow-wrap: break-word;
overflow: hidden;
overflow: visible;
}
.login-form {
@ -808,6 +808,8 @@ textarea:focus {
.markdown-buttons {
margin-top: 10px;
margin-bottom: 10px;
overflow: visible;
position: relative;
}
.markdown-buttons .btnMarkdown {
@ -837,20 +839,22 @@ textarea:focus {
.header-dropdown {
position: relative;
display: inline-block;
overflow: visible;
}
.header-dropdown-menu {
display: none;
position: absolute;
top: 100%;
left: 0;
right: 0;
background: white;
border: 1px solid #ddd;
border-radius: 5px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
z-index: 100;
z-index: 1000;
margin-top: 2px;
min-width: 80px;
min-width: 60px;
max-width: 120px;
}
.header-dropdown-menu.show {
@ -883,6 +887,14 @@ textarea:focus {
border-radius: 0 0 5px 5px;
}
/* Адаптивность для выпадающего меню заголовков */
@media (max-width: 768px) {
.header-dropdown-menu {
min-width: 50px;
right: -10px; /* Смещаем чуть левее для лучшего позиционирования на мобильных */
}
}
.footer {
text-align: center;
font-size: 12px;
@ -1168,7 +1180,7 @@ textarea:focus {
font-weight: bold;
}
/* Индикатор для дней с заметками */
/* Индикатор для дней с заметками (зеленый кружок) */
.calendar-day.has-notes::after {
content: "";
position: absolute;
@ -1181,14 +1193,68 @@ textarea:focus {
border-radius: 50%;
}
/* Индикатор для дней с отредактированными заметками (желтый кружок) */
.calendar-day.has-edited-notes::before {
content: "";
position: absolute;
bottom: 2px;
left: 50%;
transform: translateX(-50%);
width: 4px;
height: 4px;
background-color: #ffc107;
border-radius: 50%;
}
/* Когда есть оба типа заметок - сдвигаем кружки в стороны */
.calendar-day.has-notes.has-edited-notes::before {
transform: translateX(-150%);
}
.calendar-day.has-notes.has-edited-notes::after {
transform: translateX(50%);
}
/* Индикатор для выбранного дня с заметками */
.calendar-day.selected.has-notes::after {
background-color: #fff;
background-color: #28a745;
border: 1px solid #fff;
box-shadow: 0 0 3px rgba(0, 0, 0, 0.5);
}
/* Индикатор для выбранного дня с отредактированными заметками */
.calendar-day.selected.has-edited-notes::before {
background-color: #ffc107;
border: 1px solid #fff;
box-shadow: 0 0 3px rgba(0, 0, 0, 0.5);
}
/* Индикатор для сегодняшнего дня с заметками */
.calendar-day.today.has-notes::after {
background-color: #fff;
background-color: #28a745;
border: 1px solid #fff;
box-shadow: 0 0 3px rgba(0, 0, 0, 0.5);
}
/* Индикатор для сегодняшнего дня с отредактированными заметками */
.calendar-day.today.has-edited-notes::before {
background-color: #ffc107;
border: 1px solid #fff;
box-shadow: 0 0 3px rgba(0, 0, 0, 0.5);
}
/* Для дней с обоими типами заметок в выбранном состоянии */
.calendar-day.selected.has-notes.has-edited-notes::before,
.calendar-day.selected.has-notes.has-edited-notes::after {
border: 1px solid #fff;
box-shadow: 0 0 3px rgba(0, 0, 0, 0.5);
}
/* Для дней с обоими типами заметок в сегодняшнем состоянии */
.calendar-day.today.has-notes.has-edited-notes::before,
.calendar-day.today.has-notes.has-edited-notes::after {
border: 1px solid #fff;
box-shadow: 0 0 3px rgba(0, 0, 0, 0.5);
}
/* Стили для секции тегов */

View File

@ -14,6 +14,9 @@ require("dotenv").config();
const app = express();
const PORT = process.env.PORT || 3000;
// Настройка trust proxy для правильного получения IP адресов через прокси
app.set('trust proxy', true);
// Создаем директорию для аватарок, если её нет
const uploadsDir = path.join(__dirname, "public", "uploads");
if (!fs.existsSync(uploadsDir)) {
@ -293,28 +296,51 @@ function logAction(userId, actionType, details, ipAddress) {
// Функция для получения IP-адреса клиента
function getClientIP(req) {
// Проверяем различные заголовки, которые могут содержать внешний IP-адрес
// Приоритет: x-forwarded-for, x-real-ip, cf-connecting-ip (Cloudflare)
let ip =
// Приоритет: x-forwarded-for, x-real-ip, x-client-ip, cf-connecting-ip (Cloudflare)
let ip =
req.headers["x-forwarded-for"]?.split(",")[0].trim() ||
req.headers["x-real-ip"] ||
req.headers["x-client-ip"] ||
req.headers["cf-connecting-ip"] || // Для Cloudflare
req.headers["x-forwarded-for"]?.split(",")[0].trim() || // Повтор для надежности
req.headers["x-cluster-client-ip"] || // Для кластеров
req.connection?.remoteAddress ||
req.socket?.remoteAddress ||
req.connection?.socket?.remoteAddress ||
"unknown";
// Убираем порт из IPv6 адреса, если есть
if (ip.includes(":") && ip.split(":").length > 2) {
// Это IPv6 адрес, убираем порт
ip = ip.split(":").slice(0, -1).join(":").replace(/[[\]]/g, "");
} else if (ip.includes(":")) {
// Это IPv4 адрес с портом, убираем порт
ip = ip.split(":")[0];
// Очищаем IP от скобок IPv6 и портов
if (ip && ip !== "unknown") {
// Убираем скобки IPv6
ip = ip.replace(/[[\]]/g, "");
// Проверяем, является ли это IPv6 адресом
if (ip.includes("::")) {
// Это IPv6 адрес (содержит "::" или несколько ":")
// IPv6 адреса могут быть в формате [::1]:port или ::1
const ipv6Match = ip.match(/^(\[)?([^\]]+)(\])?(:(\d+))?$/);
if (ipv6Match) {
ip = ipv6Match[2]; // Берем IPv6 адрес без скобок и порта
}
} else if (ip.includes(":") && !ip.includes(".")) {
// IPv6 адрес без "::" но с несколькими ":"
// Например, 2001:db8::1
// Оставляем как есть
} else if (ip.includes(":")) {
// IPv4 с портом (например, 192.168.1.1:3000)
const parts = ip.split(":");
if (parts.length === 2 && /^\d+$/.test(parts[1])) {
ip = parts[0];
}
}
// IPv4 без порта оставляем как есть
}
return ip;
// Конвертируем IPv6 localhost в IPv4 для лучшей читаемости
if (ip === "::1") {
ip = "127.0.0.1";
}
return ip || "unknown";
}
// Миграции базы данных