feat: Добавлена функциональность тегов с фильтрацией

- Добавлена секция тегов под календарем с отображением всех уникальных тегов
- Реализована фильтрация заметок по тегам при клике на тег
- Добавлены кликабельные теги в самих заметках для интуитивной навигации
- Теги автоматически извлекаются из текста заметок в формате #название
- Добавлены счетчики для каждого тега, показывающие количество заметок
- Реализован индикатор активного фильтра с возможностью сброса
- Поддержка комбинированной фильтрации по дате и тегам
- Стилизованные теги с hover-эффектами и анимациями
- Обновление тегов в реальном времени при создании/редактировании заметок

Файлы изменены:
- public/notes.html: добавлена HTML-структура для секции тегов
- public/style.css: стили для тегов в боковой панели и в заметках
- public/app.js: логика извлечения тегов, фильтрации и обработчики кликов
This commit is contained in:
Fovway 2025-10-18 20:16:59 +07:00
parent 493e1a57be
commit 034208fc56
5 changed files with 244 additions and 10 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

View File

@ -15,6 +15,7 @@ const linkBtn = document.getElementById("linkBtn");
// Глобальные переменные для заметок и фильтрации
let allNotes = [];
let selectedDateFilter = null;
let selectedTagFilter = null;
// Функция для получения текущей даты и времени
function getFormattedDateTime() {
@ -37,6 +38,90 @@ function autoExpandTextarea(textarea) {
textarea.style.height = textarea.scrollHeight + "px";
}
// Функция для извлечения тегов из текста заметки
function extractTags(content) {
const tagRegex = /#(\w+)/g;
const tags = [];
let match;
while ((match = tagRegex.exec(content)) !== null) {
const tag = match[1].toLowerCase();
if (!tags.includes(tag)) {
tags.push(tag);
}
}
return tags;
}
// Функция для преобразования тегов в заметках в кликабельные элементы
function makeTagsClickable(content) {
const tagRegex = /#(\w+)/g;
return content.replace(
tagRegex,
'<span class="tag-in-note" data-tag="$1">#$1</span>'
);
}
// Функция для получения всех уникальных тегов из заметок
function getAllTags(notes) {
const tagCounts = {};
notes.forEach((note) => {
const tags = extractTags(note.content);
tags.forEach((tag) => {
tagCounts[tag] = (tagCounts[tag] || 0) + 1;
});
});
return tagCounts;
}
// Функция для отображения тегов
function renderTags() {
const tagsContainer = document.getElementById("tagsContainer");
if (!tagsContainer) return;
const tagCounts = getAllTags(allNotes);
const sortedTags = Object.keys(tagCounts).sort();
if (sortedTags.length === 0) {
tagsContainer.innerHTML =
'<div style="font-size: 10px; color: #999; text-align: center;">Нет тегов</div>';
return;
}
tagsContainer.innerHTML = sortedTags
.map((tag) => {
const count = tagCounts[tag];
const isActive = selectedTagFilter === tag ? "active" : "";
return `<span class="tag ${isActive}" data-tag="${tag}">#${tag}<span class="tag-count">${count}</span></span>`;
})
.join("");
// Добавляем обработчики кликов для тегов
tagsContainer.querySelectorAll(".tag").forEach((tagElement) => {
tagElement.addEventListener("click", handleTagClick);
});
}
// Обработчик клика на тег
function handleTagClick(event) {
const clickedTag = event.target.closest(".tag").dataset.tag;
// Если кликнули на тот же тег, снимаем фильтр
if (selectedTagFilter === clickedTag) {
selectedTagFilter = null;
} else {
selectedTagFilter = clickedTag;
}
// Перерисовываем заметки и теги
renderNotes(allNotes);
renderTags();
updateFilterIndicator();
}
// Привязываем авторасширение к текстовому полю для создания заметки
noteInput.addEventListener("input", function () {
autoExpandTextarea(noteInput);
@ -201,6 +286,7 @@ async function loadNotes() {
allNotes = notes; // Сохраняем все заметки в глобальную переменную
renderNotes(notes);
renderCalendar(); // Обновляем календарь после загрузки заметок
renderTags(); // Обновляем теги после загрузки заметок
} catch (error) {
console.error("Ошибка:", error);
notesList.innerHTML = "<p>Ошибка загрузки заметок</p>";
@ -211,25 +297,44 @@ async function loadNotes() {
function renderNotes(notes) {
notesList.innerHTML = "";
// Фильтруем заметки, если выбрана дата
// Фильтруем заметки по дате и тегам
let notesToDisplay = notes;
if (selectedDateFilter) {
notesToDisplay = notes.filter((note) => note.date === selectedDateFilter);
notesToDisplay = notesToDisplay.filter(
(note) => note.date === selectedDateFilter
);
}
if (selectedTagFilter) {
notesToDisplay = notesToDisplay.filter((note) => {
const tags = extractTags(note.content);
return tags.includes(selectedTagFilter);
});
}
// Если нет заметок для отображения
if (notesToDisplay.length === 0) {
if (selectedDateFilter) {
notesList.innerHTML = `<div class="container"><p style="text-align: center; color: #999;">Нет заметок за выбранную дату (${selectedDateFilter})</p></div>`;
} else {
notesList.innerHTML =
'<div class="container"><p style="text-align: center; color: #999;">Заметок пока нет</p></div>';
let message = "Заметок пока нет";
if (selectedDateFilter && selectedTagFilter) {
message = `Нет заметок за ${selectedDateFilter} с тегом #${selectedTagFilter}`;
} else if (selectedDateFilter) {
message = `Нет заметок за выбранную дату (${selectedDateFilter})`;
} else if (selectedTagFilter) {
message = `Нет заметок с тегом #${selectedTagFilter}`;
}
notesList.innerHTML = `<div class="container"><p style="text-align: center; color: #999;">${message}</p></div>`;
return;
}
// Итерируемся по заметкам в обычном порядке, чтобы новые были сверху
notesToDisplay.forEach(function (note) {
// Преобразуем теги в кликабельные элементы перед парсингом markdown
const contentWithClickableTags = makeTagsClickable(note.content);
const parsedContent = marked.parse(contentWithClickableTags);
const noteHtml = `
<div id="note" class="container">
<div class="date">
@ -244,7 +349,7 @@ function renderNotes(notes) {
<div class="textNote" data-original-content="${note.content.replace(
/"/g,
"&quot;"
)}">${marked.parse(note.content)}</div>
)}">${parsedContent}</div>
</div>
`;
notesList.insertAdjacentHTML("afterbegin", noteHtml);
@ -252,6 +357,9 @@ function renderNotes(notes) {
// Добавляем обработчики событий для кнопок редактирования и удаления
addNoteEventListeners();
// Добавляем обработчики кликов для тегов в заметках
addTagClickListeners();
}
// Функция для добавления обработчиков событий к заметкам
@ -375,6 +483,30 @@ function addNoteEventListeners() {
});
}
// Функция для добавления обработчиков кликов на теги в заметках
function addTagClickListeners() {
document.querySelectorAll(".textNote .tag-in-note").forEach((tagElement) => {
tagElement.addEventListener("click", function (event) {
event.preventDefault();
event.stopPropagation();
const clickedTag = this.dataset.tag.toLowerCase();
// Если кликнули на тот же тег, снимаем фильтр
if (selectedTagFilter === clickedTag) {
selectedTagFilter = null;
} else {
selectedTagFilter = clickedTag;
}
// Перерисовываем заметки и теги
renderNotes(allNotes);
renderTags();
updateFilterIndicator();
});
});
}
// Функция сохранения заметки (вынесена отдельно для повторного использования)
async function saveNote() {
if (noteInput.value.trim() !== "") {
@ -630,9 +762,10 @@ function handleDayClick(event) {
selectedDateFilter = clickedDate;
}
// Перерисовываем заметки и календарь
// Перерисовываем заметки, календарь и теги
renderNotes(allNotes);
renderCalendar();
renderTags();
updateFilterIndicator();
}
@ -641,9 +774,21 @@ function updateFilterIndicator() {
const filterIndicator = document.getElementById("filter-indicator");
if (!filterIndicator) return;
const filters = [];
if (selectedDateFilter) {
filters.push(`Дата: ${selectedDateFilter}`);
}
if (selectedTagFilter) {
filters.push(`Тег: #${selectedTagFilter}`);
}
if (filters.length > 0) {
filterIndicator.style.display = "inline-block";
filterIndicator.innerHTML = `Фильтр: ${selectedDateFilter} <button id="clear-filter-btn">✕</button>`;
filterIndicator.innerHTML = `Фильтр: ${filters.join(
", "
)} <button id="clear-filter-btn"></button>`;
// Добавляем обработчик клика для кнопки сброса
const clearBtn = document.getElementById("clear-filter-btn");
@ -658,8 +803,10 @@ function updateFilterIndicator() {
// Функция для сброса фильтра (глобальная)
window.clearFilter = function () {
selectedDateFilter = null;
selectedTagFilter = null;
renderNotes(allNotes);
renderCalendar();
renderTags();
updateFilterIndicator();
};

View File

@ -29,6 +29,16 @@
</div>
<div class="calendar-days" id="calendarDays"></div>
</div>
<!-- Секция тегов -->
<div class="tags-section">
<div class="tags-header">
<span class="tags-title">🏷️ Теги</span>
</div>
<div class="tags-container" id="tagsContainer">
<!-- Теги будут добавлены динамически -->
</div>
</div>
</div>
<div class="center">
<div class="container">

View File

@ -601,3 +601,80 @@ textarea:focus {
.calendar-day.today.has-notes::after {
background-color: #fff;
}
/* Стили для секции тегов */
.tags-section {
margin-top: 15px;
padding-top: 15px;
border-top: 1px solid #e0e0e0;
}
.tags-header {
margin-bottom: 10px;
}
.tags-title {
font-size: 12px;
font-weight: bold;
color: #333;
}
.tags-container {
display: flex;
flex-wrap: wrap;
gap: 5px;
}
.tag {
display: inline-block;
padding: 4px 8px;
background-color: #e7f3ff;
color: #007bff;
border: 1px solid #007bff;
border-radius: 12px;
font-size: 10px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
user-select: none;
}
.tag:hover {
background-color: #007bff;
color: white;
}
.tag.active {
background-color: #007bff;
color: white;
font-weight: bold;
}
.tag-count {
margin-left: 4px;
font-size: 9px;
opacity: 0.8;
}
/* Стили для тегов в заметках */
.textNote .tag-in-note {
display: inline-block;
padding: 2px 6px;
background-color: #e7f3ff;
color: #007bff;
border: 1px solid #007bff;
border-radius: 8px;
font-size: 12px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
text-decoration: none;
margin: 0 2px;
}
.textNote .tag-in-note:hover {
background-color: #007bff;
color: white;
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(0, 123, 255, 0.3);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB