Files
ai-qpid-pvz/game/js/systems/feedback.js
T
Antigravity 6457a8904e feat: Initial commit Plants vs Zombies Web Edition v2.4
- 5 Biomes: Day, Night, Pool, Fog, Roof (25 Campaign levels)
- Minigames: Wall-nut Bowling, I Zombie, Roguelike, Boss Rush & Dr. Zomboss
- Systems: Zen Garden, Workshop Crafting, Almanac, Tree of Wisdom, Achievements
- Backend: REST API + SQLite Accounts & Save Synchronization
- Infrastructure: Nginx HTTPS & Docker deployment
2026-09-17 09:30:52 +00:00

209 lines
7.7 KiB
JavaScript
Raw 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.
// Bug Reporting & Community Feedback Chat System
window.PvZFeedback = (function() {
const STORAGE_KEY = 'pvz_cached_bugs';
let cachedReports = [];
let currentFilter = 'all';
function getLocalFallback() {
try {
const saved = localStorage.getItem(STORAGE_KEY);
if (saved) return JSON.parse(saved);
} catch (e) {}
return [
{
id: 1,
author: "Player_Kopri",
category: "bug",
text: "Зомби после поедания растения не шел дальше, а оставался стоять на месте.",
status: "исправлено",
timestamp: "2026-09-16 09:30",
adminReply: "Исправлено в v1.6: сброшен флаг isEating и таймер поедания."
},
{
id: 2,
author: "DaveFan",
category: "bug",
text: "Режим 'Я, Зомби' не работал: сразу после старта выводилось сообщение 'победа'.",
status: "исправлено",
timestamp: "2026-09-16 09:45",
adminReply: "Исправлено в v1.6: устранено состояние гонки при инициализации газона."
},
{
id: 3,
author: "ZombossHater",
category: "visual",
text: "Элементы главного меню и кнопки вываливаются за границы экрана на небольших дисплеях.",
status: "исправлено",
timestamp: "2026-09-16 10:50",
adminReply: "Исправлено в v2.2: адаптивная панель навигации, компактные карточки и вертикальный скролл."
}
];
}
return {
init: function() {
cachedReports = getLocalFallback();
this.fetchReports();
},
getReports: function() {
return cachedReports;
},
fetchReports: async function(onDone) {
try {
const resp = await fetch('/api/bugs', { cache: 'no-cache' });
if (resp.ok) {
const data = await resp.json();
if (Array.isArray(data)) {
cachedReports = data;
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
} catch (e) {}
}
}
} catch (err) {
console.warn('Could not fetch online bugs, using local cache:', err);
if (cachedReports.length === 0) {
cachedReports = getLocalFallback();
}
}
if (typeof onDone === 'function') onDone(cachedReports);
this.renderFeed(currentFilter);
},
sendReport: async function(author, category, text, onResult) {
const payload = {
author: (author || 'Гость').trim(),
category: category || 'bug',
text: (text || '').trim(),
timestamp: new Date().toISOString()
};
if (!payload.text) {
if (onResult) onResult({ success: false, message: 'Введите описание проблемы' });
return;
}
try {
const resp = await fetch('/api/bugs', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
if (resp.ok) {
const res = await resp.json();
await this.fetchReports();
if (onResult) onResult({ success: true, report: res.report });
return;
}
} catch (err) {
console.warn('Failed to POST to server, storing locally:', err);
}
// Offline fallback: save into local list
const newId = (cachedReports.reduce((m, r) => Math.max(m, r.id || 0), 0) || 0) + 1;
const localReport = {
id: newId,
author: payload.author,
category: payload.category,
text: payload.text,
status: 'новый',
timestamp: new Date().toLocaleString('ru-RU', { hour: '2-digit', minute: '2-digit', day: '2-digit', month: '2-digit' }),
adminReply: null
};
cachedReports.push(localReport);
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(cachedReports));
} catch (e) {}
this.renderFeed(currentFilter);
if (onResult) onResult({ success: true, report: localReport, offline: true });
},
setFilter: function(filter) {
currentFilter = filter;
this.renderFeed(filter);
},
renderFeed: function(filter = currentFilter) {
currentFilter = filter;
const feedEl = document.getElementById('bugreport-feed');
const countBadge = document.getElementById('bug-count-badge');
if (countBadge) {
const total = cachedReports.length;
const word = total % 10 === 1 && total % 100 !== 11 ? 'репорт' : (total % 10 >= 2 && total % 10 <= 4 && (total % 100 < 10 || total % 100 >= 20) ? 'репорта' : 'репортов');
countBadge.textContent = `${total} ${word}`;
}
if (!feedEl) return;
let list = [...cachedReports];
if (filter === 'bug') list = list.filter(r => r.category === 'bug');
else if (filter === 'feature') list = list.filter(r => r.category === 'feature');
else if (filter === 'visual') list = list.filter(r => r.category === 'visual');
else if (filter === 'balance') list = list.filter(r => r.category === 'balance');
else if (filter === 'resolved') list = list.filter(r => r.status === 'исправлено');
if (list.length === 0) {
feedEl.innerHTML = '<div class="bug-empty">Сообщений в этой категории пока нет. Напишите первое! 💬</div>';
return;
}
// Show newest first
list.sort((a, b) => (b.id || 0) - (a.id || 0));
const categoryLabels = {
bug: { text: '🔴 Баг', cls: 'cat-bug' },
feature: { text: '💡 Идея', cls: 'cat-feature' },
visual: { text: '🎨 Графика/UI', cls: 'cat-visual' },
balance: { text: '⚖️ Баланс', cls: 'cat-balance' }
};
const statusLabels = {
'новый': { text: '🔴 Новый', cls: 'status-new' },
'в работе': { text: '🟡 В работе', cls: 'status-progress' },
'исправлено': { text: '✅ Исправлено', cls: 'status-done' }
};
let html = '';
list.forEach(r => {
const cat = categoryLabels[r.category] || categoryLabels.bug;
const st = statusLabels[r.status] || statusLabels['новый'];
const replyHtml = r.adminReply
? `<div class="bug-reply">
<span class="reply-tag">🛠️ Ответ разработчика:</span>
<div class="reply-text">${this.escapeHtml(r.adminReply)}</div>
</div>`
: '';
html += `
<div class="bug-card ${r.status === 'исправлено' ? 'is-fixed' : ''}">
<div class="bug-card-top">
<div class="bug-author">👤 <strong>${this.escapeHtml(r.author || 'Гость')}</strong></div>
<div class="bug-meta">
<span class="bug-category ${cat.cls}">${cat.text}</span>
<span class="bug-status ${st.cls}">${st.text}</span>
<span class="bug-time">🕒 ${this.escapeHtml(r.timestamp || '')}</span>
</div>
</div>
<div class="bug-text">${this.escapeHtml(r.text)}</div>
${replyHtml}
</div>
`;
});
feedEl.innerHTML = html;
},
escapeHtml: function(str) {
if (!str) return '';
return String(str)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
}
};
})();