From efba722977fad7038c19ad73de6017766d94264a Mon Sep 17 00:00:00 2001 From: Maxim Dolgolyov Date: Tue, 23 Jun 2026 16:26:32 +0300 Subject: [PATCH] =?UTF-8?q?feat(wishes):=20=D1=80=D0=B5=D0=B4=D0=B8=D0=B7?= =?UTF-8?q?=D0=B0=D0=B9=D0=BD=20=D1=81=D1=82=D1=80=D0=B0=D0=BD=D0=B8=D1=86?= =?UTF-8?q?=D1=8B=20=E2=80=94=20=D1=83=D0=B4=D0=BE=D0=B1=D0=BD=D0=B5=D0=B5?= =?UTF-8?q?=20=D0=B8=20=D0=BA=D1=80=D0=B0=D1=81=D0=B8=D0=B2=D0=B5=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Полный фронт-редизайн /wishes (бэкенд не тронут): - Hero с градиентной иконкой; «Поделиться идеей» — сворачиваемая форма (по умолчанию свёрнута, если пожелания уже есть; список сразу виден). - Визуальный выбор категории чипами с иконками/цветом вместо select; счётчик символов. - Статус-пилюли вверху с counts — кликабельный фильтр (для всех ролей, не только админ). - Подбар: фильтр по категориям + живой поиск (по заголовку/тексту/автору); адаптивно скрывается, когда мало данных. - Карточки: цветная иконка категории, статус-бейдж с иконкой, ответ админа в выделенном блоке, анимация появления, hover. Дружелюбные empty-состояния (нет идей / ничего не найдено) и скелетоны при загрузке. - Клиентская фильтрация (один fetch, мгновенно) + точечные обновления списка без перезагрузки после создания/сохранения/удаления. Verified: рендер-смоук 13/13 (карточки, иконки категорий, статусы, ответ, фильтры status/cat/поиск с тогглом, empty); node --check инлайна; эмодзи нет (иконки — lucide). Co-Authored-By: Claude Opus 4.8 (1M context) --- frontend/wishes.html | 424 +++++++++++++++++++++++++++++-------------- 1 file changed, 283 insertions(+), 141 deletions(-) diff --git a/frontend/wishes.html b/frontend/wishes.html index 6e2ec23..aaabf94 100644 --- a/frontend/wishes.html +++ b/frontend/wishes.html @@ -10,62 +10,120 @@ @@ -74,35 +132,41 @@
-
Пожелания по улучшению
-
Предложите, что улучшить в системе — мы это увидим и ответим.
+ +
+
+
+
Пожелания по улучшению
+
Есть идея, как сделать систему лучше? Расскажите — мы прочитаем и ответим.
+
+ +
-
-
- - -
-
- -
-
+ - - +
+ -
Загрузка…
+
+
+
@@ -116,81 +180,53 @@ LS.showBoardIfAllowed(); LS.notif.init(); - const CAT_LABEL = { ui: 'Интерфейс', content: 'Контент', feature: 'Новая функция', bug: 'Баг', other: 'Другое' }; - const ST_LABEL = { new: 'Новое', planned: 'Запланировано', in_progress: 'В работе', done: 'Готово', declined: 'Отклонено' }; - const ST_ORDER = ['new', 'planned', 'in_progress', 'done', 'declined']; - let _statusFilter = null; - let _wishes = []; + const CAT = { + feature: { label: 'Новая функция', icon: 'sparkles', color: '#9B5DE5' }, + ui: { label: 'Интерфейс', icon: 'layout-panel-top', color: '#06B6D4' }, + content: { label: 'Контент', icon: 'book-open', color: '#2563EB' }, + bug: { label: 'Баг / ошибка', icon: 'bug', color: '#EF476F' }, + other: { label: 'Другое', icon: 'message-circle', color: '#64748B' }, + }; + const CAT_ORDER = ['feature', 'ui', 'content', 'bug', 'other']; + const ST = { + new: { label: 'Новое', icon: 'sparkle', color: '#06aab3' }, + planned: { label: 'Запланировано', icon: 'calendar-clock', color: '#9B5DE5' }, + in_progress: { label: 'В работе', icon: 'loader', color: '#d97706' }, + done: { label: 'Готово', icon: 'check-circle-2', color: '#059652' }, + declined: { label: 'Отклонено', icon: 'x-circle', color: '#64748B' }, + }; + const ST_ORDER = ['new', 'planned', 'in_progress', 'done', 'declined']; + + let _wishes = [], _statusFilter = null, _catFilter = null, _q = '', _formCat = 'feature', _formOpen = false; function fmtDate(s) { if (!s) return ''; const d = new Date(s.includes('T') ? s : s.replace(' ', 'T') + 'Z'); return d.toLocaleDateString('ru', { day: 'numeric', month: 'short', year: 'numeric' }); } + function icons() { if (window.lucide) lucide.createIcons(); } - async function load() { - try { - const params = _statusFilter ? { status: _statusFilter } : {}; - const data = await LS.wishesList(params); - _wishes = data.wishes || []; - if (data.isAdmin) renderFilters(data.counts || {}); - render(); - } catch (e) { - document.getElementById('w-list').innerHTML = `
Не удалось загрузить: ${esc(e.message || '')}
`; - } + /* ── form ── */ + function renderCatPick() { + document.getElementById('wq-cat-pick').innerHTML = CAT_ORDER.map(k => + ``).join(''); + icons(); } - - function renderFilters(counts) { - const el = document.getElementById('w-filters'); - el.style.display = ''; - const total = Object.values(counts).reduce((a, b) => a + b, 0); - let html = ``; - html += ST_ORDER.map(s => counts[s] - ? `` - : '').join(''); - el.innerHTML = html; + function pickCat(k) { _formCat = k; renderCatPick(); } + function updCounter() { + const n = document.getElementById('wf-title').value.length; + document.getElementById('wf-counter').textContent = n + ' / 200'; } - - function setFilter(s) { _statusFilter = s; load(); } - - function render() { - const el = document.getElementById('w-list'); - if (!_wishes.length) { - el.innerHTML = `
${isAdmin ? 'Пожеланий пока нет.' : 'Вы ещё не оставляли пожеланий. Поделитесь идеей выше!'}
`; - if (window.lucide) lucide.createIcons(); - return; - } - el.innerHTML = _wishes.map(cardHtml).join(''); - if (window.lucide) lucide.createIcons(); - } - - const ST_COLOR = { new: '#06aab3', planned: '#9B5DE5', in_progress: '#d97706', done: '#059652', declined: '#94A3B8' }; - - function cardHtml(w) { - const author = (isAdmin && w.author_name) ? `${esc(w.author_name)} · ` : ''; - const noteHtml = w.admin_note ? `
Ответ: ${esc(w.admin_note)}
` : ''; - let manage = ''; - if (isAdmin) { - const opts = ST_ORDER.map(s => ``).join(''); - manage = `
- - - - -
`; - } else if (w.status === 'new') { - manage = `
`; - } - return `
-
- ${esc(w.title)} - ${ST_LABEL[w.status] || w.status} -
-
${author}${CAT_LABEL[w.category] || w.category} · ${fmtDate(w.created_at)}
- ${w.body ? `
${esc(w.body)}
` : ''} - ${noteHtml} - ${manage} -
`; + function toggleForm(forceOpen) { + _formOpen = forceOpen === undefined ? !_formOpen : forceOpen; + document.getElementById('wq-form').classList.toggle('collapsed', !_formOpen); + const btn = document.getElementById('wq-new-btn'); + btn.classList.toggle('open', _formOpen); + document.getElementById('wq-new-lbl').textContent = _formOpen ? 'Свернуть' : 'Поделиться идеей'; + btn.querySelector('i').setAttribute('data-lucide', _formOpen ? 'chevron-up' : 'plus'); + icons(); + if (_formOpen) setTimeout(() => document.getElementById('wf-title').focus(), 80); } async function submitWish() { @@ -199,28 +235,133 @@ const btn = document.getElementById('wf-submit'); btn.disabled = true; try { - await LS.wishCreate({ - title, - category: document.getElementById('wf-cat').value, - body: document.getElementById('wf-body').value.trim(), - }); + const row = await LS.wishCreate({ title, category: _formCat, body: document.getElementById('wf-body').value.trim() }); + if (isAdmin && user) { row.author_name = user.name; } + _wishes.unshift(row); document.getElementById('wf-title').value = ''; document.getElementById('wf-body').value = ''; + _formCat = 'feature'; renderCatPick(); updCounter(); + toggleForm(false); LS.toast('Пожелание отправлено — спасибо!', 'success'); - _statusFilter = null; - await load(); + _statusFilter = null; _catFilter = null; + renderAll(); } catch (e) { LS.toast(e.message || 'Ошибка', 'error'); } finally { btn.disabled = false; } } + /* ── load + render ── */ + async function load() { + try { + const data = await LS.wishesList(); + _wishes = data.wishes || []; + renderAll(); + } catch (e) { + document.getElementById('w-list').innerHTML = `
Не удалось загрузить
${esc(e.message || '')}
`; + } + } + + function counts() { + const c = {}; ST_ORDER.forEach(s => c[s] = 0); + _wishes.forEach(w => { c[w.status] = (c[w.status] || 0) + 1; }); + return c; + } + + function renderAll() { renderStats(); renderSubbar(); renderList(); } + + function renderStats() { + const c = counts(); + const total = _wishes.length; + let html = ``; + html += ST_ORDER.filter(s => c[s] > 0).map(s => + ``).join(''); + document.getElementById('wq-stats').innerHTML = html; + } + + function renderSubbar() { + const cats = [...new Set(_wishes.map(w => w.category))]; + const bar = document.getElementById('wq-subbar'); + // показываем подбар только если есть смысл (несколько категорий или много пожеланий) + if (cats.length < 2 && _wishes.length < 4) { bar.style.display = 'none'; return; } + bar.style.display = ''; + document.getElementById('wq-cats').innerHTML = CAT_ORDER.filter(k => cats.includes(k)).map(k => + ``).join(''); + document.getElementById('wq-search').style.display = _wishes.length >= 4 ? '' : 'none'; + icons(); + } + + function setStatus(s) { _statusFilter = (_statusFilter === s) ? null : s; renderAll(); } + function setCat(k) { _catFilter = (_catFilter === k) ? null : k; renderAll(); } + function onSearch(v) { _q = v.trim().toLowerCase(); renderList(); } + + function renderList() { + const el = document.getElementById('w-list'); + let list = _wishes; + if (_statusFilter) list = list.filter(w => w.status === _statusFilter); + if (_catFilter) list = list.filter(w => w.category === _catFilter); + if (_q) list = list.filter(w => + (w.title || '').toLowerCase().includes(_q) || + (w.body || '').toLowerCase().includes(_q) || + (w.author_name || '').toLowerCase().includes(_q)); + + if (!list.length) { + const fresh = !_wishes.length; + el.innerHTML = `
+
+
${fresh ? (isAdmin ? 'Пожеланий пока нет' : 'У вас пока нет пожеланий') : 'Ничего не найдено'}
+
${fresh ? (isAdmin ? 'Они появятся здесь, когда пользователи их оставят.' : 'Поделитесь идеей — нажмите «Поделиться идеей» выше.') : 'Попробуйте изменить фильтр или запрос.'}
+
`; + icons(); + return; + } + el.innerHTML = list.map(cardHtml).join(''); + icons(); + } + + function cardHtml(w) { + const cat = CAT[w.category] || CAT.other; + const st = ST[w.status] || ST.new; + const author = (isAdmin && w.author_name) ? `${esc(w.author_name)}·` : ''; + const note = w.admin_note ? `
Ответ: ${esc(w.admin_note)}
` : ''; + let manage = ''; + if (isAdmin) { + const opts = ST_ORDER.map(s => ``).join(''); + manage = `
+ + + + +
`; + } else if (w.status === 'new') { + manage = `
`; + } + return `
+
+
+
+ ${esc(w.title)} + ${st.label} +
+
${author}${cat.label}·${fmtDate(w.created_at)}
+ ${w.body ? `
${esc(w.body)}
` : ''} + ${note} + ${manage} +
+
`; + } + async function saveWish(id) { try { - await LS.wishUpdate(id, { + const upd = await LS.wishUpdate(id, { status: document.getElementById('st-' + id).value, admin_note: document.getElementById('note-' + id).value.trim(), }); + const i = _wishes.findIndex(w => w.id === id); + if (i >= 0) { _wishes[i] = { ..._wishes[i], ...upd }; } LS.toast('Сохранено', 'success'); - await load(); + renderAll(); } catch (e) { LS.toast(e.message || 'Ошибка', 'error'); } } @@ -229,12 +370,13 @@ try { await LS.wishDelete(id); _wishes = _wishes.filter(w => w.id !== id); - render(); + renderAll(); } catch (e) { LS.toast(e.message || 'Ошибка', 'error'); } } + renderCatPick(); load(); - if (window.lucide) lucide.createIcons(); + icons();