Files

263 lines
8.5 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// =======================================================================
// Notes Application - Feedback & Dev Bug Tracker Widget (Native ES Module)
// ChipDip-style floating chat for instant bug reporting & feature requests
// =======================================================================
import { api } from './api.js';
import { escapeHtml, showToast } from './ui.js';
let activeCategory = 'bug';
let isWidgetOpen = false;
function getCurrentContext() {
const hash = window.location.hash || '#dictionary';
const width = window.innerWidth;
const height = window.innerHeight;
const isMobile = width <= 768;
let friendlyName = 'Главная';
if (hash.startsWith('#reader/book/')) {
const titleEl = document.getElementById('reader-book-title');
const bookTitle = titleEl ? titleEl.textContent : 'Книга';
friendlyName = `Читалка: ${bookTitle}`;
} else if (hash.startsWith('#reader')) {
friendlyName = 'Полка книг';
} else if (hash.startsWith('#srs')) {
friendlyName = 'SRS Тренировка';
} else if (hash.startsWith('#grammar')) {
friendlyName = 'Грамматика';
} else if (hash.startsWith('#cefr')) {
friendlyName = 'CEFR Тестирование';
} else if (hash.startsWith('#lesson')) {
friendlyName = 'Урок';
} else if (hash.startsWith('#dictionary')) {
friendlyName = 'Словарь & Поиск';
}
return {
url: hash,
friendlyName: friendlyName,
data: {
screen: `${width}×${height}`,
device: isMobile ? 'Mobile' : 'Desktop',
userAgent: navigator.userAgent.slice(0, 150),
timestamp: new Date().toISOString()
}
};
}
function updateContextDisplay() {
const indicator = document.getElementById('feedback-context-text');
if (!indicator) return;
const ctx = getCurrentContext();
indicator.textContent = `${ctx.friendlyName} • ${ctx.data.device} (${ctx.data.screen})`;
}
function formatRelativeTime(isoString) {
if (!isoString) return '';
try {
const date = new Date(isoString);
return date.toLocaleDateString('ru-RU', {
day: 'numeric',
month: 'short',
hour: '2-digit',
minute: '2-digit'
});
} catch {
return isoString;
}
}
function createReportCard(r) {
const card = document.createElement('div');
card.className = 'feedback-msg-card';
card.dataset.reportId = r.id;
const catLabels = {
bug: { label: '🐛 Баг', cls: 'bug' },
feature: { label: '💡 Фича', cls: 'feature' },
note: { label: '📝 Заметка', cls: 'note' }
};
const cat = catLabels[r.category] || catLabels.bug;
const statusLabels = {
new: { label: '🆕 Новый', cls: 'new' },
in_progress: { label: '⏳ В работе', cls: 'in_progress' },
resolved: { label: '✅ Исправлено', cls: 'resolved' },
closed: { label: '✕ Закрыт', cls: 'resolved' }
};
const st = statusLabels[r.status] || statusLabels.new;
const timeStr = formatRelativeTime(r.created_at);
const contextStr = r.context_url ? escapeHtml(r.context_url) : '';
card.innerHTML = `
<div class="feedback-msg-header">
<span class="feedback-msg-badge ${cat.cls}">${cat.label}</span>
<span class="feedback-msg-status ${st.cls}">${st.label}</span>
</div>
<div class="feedback-msg-text">${escapeHtml(r.message)}</div>
<div class="feedback-msg-meta">
<span>${timeStr}</span>
${contextStr ? `<span>${contextStr}</span>` : ''}
</div>
`;
return card;
}
export async function loadFeedbackList() {
const listEl = document.getElementById('feedback-messages-list');
const badgeEl = document.getElementById('feedback-badge');
if (!listEl) return;
try {
const reports = await api.feedback.getAll();
const activeCount = reports.filter(r => r.status === 'new' || r.status === 'in_progress').length;
if (badgeEl) {
if (activeCount > 0) {
badgeEl.textContent = activeCount;
badgeEl.classList.remove('hidden');
} else {
badgeEl.classList.add('hidden');
}
}
listEl.innerHTML = '';
if (reports.length === 0) {
listEl.innerHTML = `
<div class="feedback-empty-state">
<span class="empty-icon">📝</span>
<p>Нашли баг или есть идея для фичи?</p>
<small>Напишите сообщение ниже — оно сразу синхронизируется с AI-разработчиком.</small>
</div>
`;
return;
}
reports.forEach(r => {
listEl.appendChild(createReportCard(r));
});
// Scroll to bottom
listEl.scrollTop = listEl.scrollHeight;
} catch (err) {
console.error('Failed to load feedback reports:', err);
}
}
export function openFeedbackWidget() {
const widget = document.getElementById('feedback-widget');
const input = document.getElementById('feedback-input');
if (!widget) return;
isWidgetOpen = true;
widget.classList.remove('hidden');
updateContextDisplay();
loadFeedbackList();
setTimeout(() => {
if (input) input.focus();
}, 100);
}
export function closeFeedbackWidget() {
const widget = document.getElementById('feedback-widget');
if (!widget) return;
isWidgetOpen = false;
widget.classList.add('hidden');
}
export function toggleFeedbackWidget() {
if (isWidgetOpen) {
closeFeedbackWidget();
} else {
openFeedbackWidget();
}
}
export function initFeedbackWidget() {
const triggerBtn = document.getElementById('feedback-trigger-btn');
const closeBtn = document.getElementById('feedback-close-btn');
const widget = document.getElementById('feedback-widget');
const form = document.getElementById('feedback-form');
const input = document.getElementById('feedback-input');
const sendBtn = document.getElementById('feedback-send-btn');
const catChips = document.querySelectorAll('.feedback-cat-chip');
if (triggerBtn) {
triggerBtn.addEventListener('click', (e) => {
e.stopPropagation();
toggleFeedbackWidget();
});
}
if (closeBtn) {
closeBtn.addEventListener('click', (e) => {
e.stopPropagation();
closeFeedbackWidget();
});
}
// Category chips selection
catChips.forEach(chip => {
chip.addEventListener('click', () => {
catChips.forEach(c => c.classList.remove('active'));
chip.classList.add('active');
activeCategory = chip.dataset.category || 'bug';
if (input) input.focus();
});
});
// Close on Escape
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && isWidgetOpen) {
closeFeedbackWidget();
}
});
// Enter to send, Shift+Enter for new line
if (input) {
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
if (form) form.requestSubmit();
}
});
}
// Form submit
if (form) {
form.addEventListener('submit', async (e) => {
e.preventDefault();
const text = input ? input.value.trim() : '';
if (!text) return;
const ctx = getCurrentContext();
if (sendBtn) sendBtn.disabled = true;
try {
await api.feedback.create(activeCategory, text, ctx.url, ctx.data);
showToast('✅ Репорт отправлен в бэклог AI-разработчику!', 'success');
if (input) input.value = '';
await loadFeedbackList();
} catch (err) {
console.error('Feedback submit error:', err);
showToast(err.message || 'Ошибка отправки репорта', 'error');
} finally {
if (sendBtn) sendBtn.disabled = false;
if (input) input.focus();
}
});
}
// Hash change updates context indicator if open
window.addEventListener('hashchange', () => {
if (isWidgetOpen) updateContextDisplay();
});
// Initial check of badge count
loadFeedbackList();
}