From e53c107d838454444911d71167aceecd0c75e736 Mon Sep 17 00:00:00 2001 From: Maxim Dolgolyov Date: Wed, 24 Jun 2026 15:35:11 +0300 Subject: [PATCH] =?UTF-8?q?feat(materials):=20=D1=82=D0=B5=D0=B3=D0=B8=20?= =?UTF-8?q?=D0=B8=20=D1=84=D0=B8=D0=BB=D1=8C=D1=82=D1=80=20=D0=BF=D0=BE=20?= =?UTF-8?q?=D1=82=D0=B5=D0=B3=D0=B0=D0=BC=20=D0=B2=20=C2=AB=D0=9C=D0=BE?= =?UTF-8?q?=D0=B8=20=D0=BC=D0=B0=D1=82=D0=B5=D1=80=D0=B8=D0=B0=D0=BB=D1=8B?= =?UTF-8?q?=C2=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - теги показываются чипами на карточках (клик по тегу — фильтр) - панель тегов над сеткой: «Все теги» + все теги пользователя, активный подсвечен - теги редактируются в модалках «Изменить» и «Новая заметка» (через запятую, normTags: тримминг, дедуп без учёта регистра, лимит 12) - фильтр по тегу + существующий текстовый поиск (поиск уже включал tags в haystack) Только фронт: колонка tags и приём в create/update/list уже были на бэкенде. Co-Authored-By: Claude Opus 4.8 (1M context) --- frontend/my-materials.html | 47 +++++++++++++++++++++++++++++++++++--- 1 file changed, 44 insertions(+), 3 deletions(-) diff --git a/frontend/my-materials.html b/frontend/my-materials.html index ceb3c4c..2e98719 100644 --- a/frontend/my-materials.html +++ b/frontend/my-materials.html @@ -48,6 +48,14 @@ .mm-card-body { padding: 12px 14px; border-top: 1px solid var(--border); } .mm-card-title { font-weight: 700; font-size: 0.86rem; color: var(--text); margin-bottom: 3px; } .mm-card-meta { font-size: 0.74rem; color: var(--text-3); } + .mm-tags { display: flex; flex-wrap: wrap; gap: 5px; margin-top: 8px; } + .mm-tag { font-size: 0.7rem; font-weight: 600; color: var(--violet); background: rgba(155,93,229,0.10); border: 1px solid rgba(155,93,229,0.22); border-radius: 99px; padding: 2px 9px; cursor: pointer; } + .mm-tag:hover { background: rgba(155,93,229,0.18); } + .mm-tagbar { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 12px; align-items: center; } + .mm-tagbar:empty { display: none; } + .mm-tagf { font-size: 0.76rem; font-weight: 600; color: var(--text-2); background: var(--surface); border: 1px solid var(--border); border-radius: 99px; padding: 4px 11px; cursor: pointer; } + .mm-tagf:hover { border-color: rgba(155,93,229,0.4); } + .mm-tagf.active { background: var(--violet); color: #fff; border-color: var(--violet); } .mm-card-actions { display: flex; gap: 6px; margin-top: 10px; flex-wrap: wrap; align-items: center; } .mm-btn { display: inline-flex; align-items: center; gap: 5px; padding: 5px 10px; border: 1px solid var(--border); border-radius: 8px; background: var(--surface); cursor: pointer; font-size: 0.76rem; font-weight: 600; color: var(--text-2); text-decoration: none; transition: border-color .12s, color .12s; } .mm-btn:hover { border-color: var(--violet); color: var(--violet); } @@ -110,6 +118,7 @@ +
Загрузка…
@@ -172,7 +181,30 @@ } let _mats = []; let _cols = []; - const _filter = { col: 'all', kind: 'all', q: '' }; + const _filter = { col: 'all', kind: 'all', q: '', tag: null }; + + /* ── Теги (хранятся строкой через запятую в m.tags) ── */ + function tagsOf(m) { return String((m && m.tags) || '').split(',').map(t => t.trim()).filter(Boolean); } + function normTags(str) { const seen = new Set(), out = []; String(str || '').split(',').forEach(t => { t = t.trim().slice(0, 40); const k = t.toLowerCase(); if (t && !seen.has(k)) { seen.add(k); out.push(t); } }); return out.slice(0, 12).join(', '); } + function tagsHtml(m) { + const ts = tagsOf(m); + if (!ts.length) return ''; + return `
${ts.map(t => `#${esc(t)}`).join('')}
`; + } + function allTags() { + const set = new Map(); + _mats.forEach(m => tagsOf(m).forEach(t => set.set(t.toLowerCase(), t))); + return Array.from(set.values()).sort((a, b) => a.localeCompare(b, 'ru')); + } + function renderTags() { + const bar = document.getElementById('mm-tags'); + if (!bar) return; + const tags = allTags(); + if (!tags.length) { bar.innerHTML = ''; return; } + let html = `Все теги`; + html += tags.map(t => `#${esc(t)}`).join(''); + bar.innerHTML = html; + } /* ── Move-to-collection select ── */ function moveSelect(m) { @@ -202,6 +234,7 @@ ${chip}
${esc(m.title || kind)}
${meta}
+ ${tagsHtml(m)}
${mv} @@ -225,6 +258,7 @@ ${chip}
${esc(m.title || kind)}
${meta}
+ ${tagsHtml(m)}
${mv} Открыть @@ -304,6 +338,7 @@ if (_filter.col === 'none' && m.collection_id) return false; if (_filter.col !== 'all' && _filter.col !== 'none' && String(m.collection_id) !== _filter.col) return false; if (_filter.kind !== 'all' && m.kind !== _filter.kind) return false; + if (_filter.tag && !tagsOf(m).some(t => t.toLowerCase() === _filter.tag)) return false; if (_filter.q) { const hay = ((m.title || '') + ' ' + (m.body || '') + ' ' + (m.source_title || '') + ' ' + (m.url || '') + ' ' + (m.tags || '')).toLowerCase(); if (!hay.includes(_filter.q)) return false; @@ -326,6 +361,7 @@ grid.innerHTML = rows.length ? rows.map(card).join('') : `

Ничего не найдено

`; + renderTags(); lucide.createIcons(); } @@ -345,7 +381,8 @@ function setCol(key) { _filter.col = key; renderCols(); renderGrid(); } function onKind(v) { _filter.kind = v; renderGrid(); } function onSearch(v) { _filter.q = (v || '').trim().toLowerCase(); renderGrid(); } - window.setCol = setCol; window.onKind = onKind; window.onSearch = onSearch; + function setTag(t) { const lt = t ? String(t).toLowerCase() : null; _filter.tag = (_filter.tag === lt) ? null : lt; renderGrid(); } + window.setCol = setCol; window.onKind = onKind; window.onSearch = onSearch; window.setTag = setTag; /* ── Material actions ── */ async function moveMaterial(id, cid) { @@ -366,6 +403,7 @@ const content = `
+
`; const m = LS.modal({ title: 'Новая заметка', content, size: 'sm', actions: [ { label: 'Отмена', onClick: () => m.close() }, @@ -374,7 +412,8 @@ const text = m.body.querySelector('#mm-nt-body').value.trim(); if (!text && !title) { LS.toast('Введите текст заметки', 'warn'); return; } const col = _filter.col !== 'all' && _filter.col !== 'none' ? Number(_filter.col) : null; - try { await LS.saveMaterial({ kind: 'note', title, body: text, collection_id: col }); m.close(); load(); } + const tags = normTags(m.body.querySelector('#mm-nt-tags').value); + try { await LS.saveMaterial({ kind: 'note', title, body: text, collection_id: col, tags }); m.close(); load(); } catch (e) { LS.toast(e.message || 'Ошибка', 'error'); } } }, ] }); @@ -388,12 +427,14 @@ const content = `
${isNote ? `` : ''} +
`; const m = LS.modal({ title: 'Изменить', content, size: 'sm', actions: [ { label: 'Отмена', onClick: () => m.close() }, { label: 'Сохранить', primary: true, onClick: async () => { const data = { title: m.body.querySelector('#mm-ed-title').value.trim() }; if (isNote) data.body = m.body.querySelector('#mm-ed-body').value; + data.tags = normTags(m.body.querySelector('#mm-ed-tags').value); try { await LS.updateMaterial(id, data); m.close(); load(); } catch (e) { LS.toast(e.message || 'Ошибка', 'error'); } } },