Files
plm-lottery/app/static/report-bug.html
T

170 lines
7.3 KiB
HTML
Raw Normal View History

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title data-i18n="bugReport.pageTitle">Report a bug</title>
<link rel="icon" type="image/svg+xml" href="/logo.svg">
<link rel="stylesheet" href="/style.css">
</head>
<body>
<div class="app-shell app-shell-bugreport">
<div class="bugreport-topbar">
<a class="brand" href="/">
<img class="brand-mark" src="/logo.svg" alt="">
PLM Lottery
</a>
<div class="bugreport-topbar-right">
<a class="back-home-btn" href="/">
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="19" y1="12" x2="5" y2="12"/><polyline points="12 19 5 12 12 5"/></svg>
<span data-i18n="bugReport.backLink">Back to home</span>
</a>
<select id="lang-switcher" class="lang-switcher" onchange="setLanguage(this.value)" aria-label="Language">
<option value="en">English</option>
<option value="it">Italiano</option>
<option value="es">Español</option>
<option value="fr">Français</option>
<option value="de">Deutsch</option>
<option value="ru">Русский</option>
<option value="zh">中文</option>
</select>
</div>
</div>
<div class="bugreport-hero">
<div class="bugreport-hero-icon">
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 8v5"/><path d="M12 16h.01"/></svg>
</div>
<div>
<h1 data-i18n="bugReport.heading">Report a bug</h1>
<p data-i18n="bugReport.intro">Found a problem? Describe it below — your report goes straight to the admin panel.</p>
</div>
</div>
<div class="card" id="report-form">
<div class="field-note" id="english-notice">
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>
<span data-i18n="bugReport.englishNotice">Please write your bug report in English, regardless of the language you're browsing in — this helps us handle it faster.</span>
</div>
<label for="bug-description" data-i18n="bugReport.descriptionLabel">What happened?</label>
<textarea id="bug-description" rows="6" maxlength="2000" data-i18n-placeholder="bugReport.descriptionPlaceholder" oninput="updateCharCount()"></textarea>
<div class="char-count" id="char-count">0 / 2000</div>
<label for="bug-contact" data-i18n="bugReport.contactLabel">Contact (optional)</label>
<input id="bug-contact" type="text" maxlength="256" data-i18n-placeholder="bugReport.contactPlaceholder">
<button onclick="submitBugReport()" id="bug-submit-btn" data-i18n="bugReport.submitBtn">Send report</button>
</div>
<div class="hidden" id="my-reports-section">
<p class="section-label" data-i18n="bugReport.myReportsTitle">Your reports</p>
<div class="card">
<p class="hint" data-i18n="bugReport.myReportsHint">Only reports sent from this account, with the status set by the admin team.</p>
<div id="my-reports-list"></div>
</div>
</div>
</div>
<div id="toast-container" aria-live="polite"></div>
<script src="/i18n.js"></script>
<script>
function toast(message, type) {
const container = document.getElementById('toast-container');
const el = document.createElement('div');
el.className = 'toast ' + type;
el.textContent = message;
container.appendChild(el);
setTimeout(() => el.remove(), 4000);
}
function escapeHtml(s) {
return String(s).replace(/[&<>"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]));
}
function updateCharCount() {
const field = document.getElementById('bug-description');
document.getElementById('char-count').textContent = field.value.length + ' / ' + field.maxLength;
}
async function submitBugReport() {
const btn = document.getElementById('bug-submit-btn');
const description = document.getElementById('bug-description').value.trim();
const contact = document.getElementById('bug-contact').value.trim();
if (!description) {
toast(t('bugReport.blankError'), 'error');
return;
}
const headers = { 'Content-Type': 'application/json' };
const token = localStorage.getItem('plm_token');
if (token) headers['Authorization'] = 'Bearer ' + token;
btn.disabled = true;
const original = btn.textContent;
btn.textContent = t('bugReport.submitting');
try {
const res = await fetch('/bug-reports', {
method: 'POST',
headers,
body: JSON.stringify({ description, contact: contact || null }),
});
const data = await res.json().catch(() => ({}));
if (!res.ok) throw new Error(data.detail?.message || data.detail || res.statusText);
document.getElementById('bug-description').value = '';
document.getElementById('bug-contact').value = '';
updateCharCount();
toast(t('bugReport.successToast'), 'success');
loadMyBugReports();
} catch (e) {
toast(t('bugReport.errorPrefix') + e.message, 'error');
} finally {
btn.disabled = false;
btn.textContent = original;
applyStaticTranslations(btn);
}
}
async function loadMyBugReports() {
const token = localStorage.getItem('plm_token');
const section = document.getElementById('my-reports-section');
if (!token) {
section.classList.add('hidden');
return;
}
try {
const res = await fetch('/bug-reports/mine', { headers: { Authorization: 'Bearer ' + token } });
if (!res.ok) {
section.classList.add('hidden');
return;
}
const reports = await res.json();
section.classList.remove('hidden');
const list = document.getElementById('my-reports-list');
list.innerHTML = reports.map((r) => `
<div class="report-row">
<span class="badge bug-status-${escapeHtml(r.status)}">${escapeHtml(t('bugReport.status' + r.status.charAt(0).toUpperCase() + r.status.slice(1)))}</span>
<span class="report-row-desc" title="${escapeHtml(r.description)}">${escapeHtml(r.description)}</span>
<span class="report-row-date">${new Date(r.created_at).toLocaleDateString(currentDateLocale(), { day: 'numeric', month: 'short', year: 'numeric' })}</span>
</div>
`).join('') || `<p class="hint report-empty">${escapeHtml(t('bugReport.myReportsEmpty'))}</p>`;
} catch (e) {
section.classList.add('hidden');
}
}
// Re-renders server-rendered content (my reports list) on a language switch,
// the same split app.js uses between data-i18n (static markup) and t() (data).
function onLanguageChange() {
loadMyBugReports();
}
updateCharCount();
loadMyBugReports();
</script>
</body>
</html>