From e5af15087c12730c0fc0f101fd4ca2610f1456a4 Mon Sep 17 00:00:00 2001 From: Davide Grilli Date: Fri, 31 Jul 2026 16:28:37 +0200 Subject: [PATCH] Polish /report-bug's visual design and refine bug report semantics MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Design pass on the bug report page, staying inside the site's existing design system (tokens, IBM Plex Sans, card/badge/pill components, stroke icon set) rather than introducing a new one: - A slim top bar (brand mark + back-to-home pill button + language switcher) replaces the bare floating heading, so the page reads as part of the product instead of an orphaned form. - The "write in English" notice moves inside the form card, right above the field it applies to, and switches from the amber "needs attention" tone to an accent-tinted info tone, so it doesn't visually collide with the bug-status badges' own use of amber for "not read yet". - "Your reports" is promoted to a proper labeled section with a cleaner row layout (truncated description with a title tooltip, compact date). - A character counter on the description field. - The back-to-home control is now a bordered pill with an arrow icon instead of a bare text link with a hardcoded "←", which also meant dropping that hardcoded arrow from all 7 translations. Also, two content refinements based on feedback: - Max description length dropped from 5000 to 2000 characters, enforced on both the textarea and the API's Pydantic validator. - The "read" status is relabeled from a passive "read"/"letta" to an active "acknowledged"/"presa in carico" (and each other language's own equivalent helpdesk term) — it communicates a team is on it, not just that someone glanced at it. Only the label changed; the underlying "read" status value in the API/DB is untouched. Co-Authored-By: Claude Sonnet 5 --- app/api/routes/bug_reports.py | 2 +- app/static/admin.js | 4 +- app/static/i18n.js | 28 +++++------ app/static/report-bug.html | 92 +++++++++++++++++++++++------------ app/static/style.css | 84 ++++++++++++++++++++++++++++---- 5 files changed, 152 insertions(+), 58 deletions(-) diff --git a/app/api/routes/bug_reports.py b/app/api/routes/bug_reports.py index 6522cef..45e403c 100644 --- a/app/api/routes/bug_reports.py +++ b/app/api/routes/bug_reports.py @@ -12,7 +12,7 @@ router = APIRouter(prefix="/bug-reports", tags=["bug-reports"]) class BugReportCreate(BaseModel): - description: str = Field(min_length=1, max_length=5000) + description: str = Field(min_length=1, max_length=2000) contact: str | None = Field(default=None, max_length=256) @field_validator("description") diff --git a/app/static/admin.js b/app/static/admin.js index 1307746..68b52b5 100644 --- a/app/static/admin.js +++ b/app/static/admin.js @@ -346,7 +346,7 @@ async function loadAuditLog() { } } -const BUG_REPORT_STATUS_LABELS = { open: 'Da leggere', read: 'Letta', resolved: 'Risolta' }; +const BUG_REPORT_STATUS_LABELS = { open: 'Da leggere', read: 'Presa in carico', resolved: 'Risolta' }; function bugReportBadge(status) { return `${escapeHtml(BUG_REPORT_STATUS_LABELS[status] || status)}`; @@ -366,7 +366,7 @@ async function loadBugReports() { ${bugReportBadge(r.status)}
- ${r.status === 'open' ? `` : ''} + ${r.status === 'open' ? `` : ''} ${r.status !== 'resolved' ? `` : ''} ${r.status !== 'open' ? `` : ''}
diff --git a/app/static/i18n.js b/app/static/i18n.js index 0e95c11..5d7d173 100644 --- a/app/static/i18n.js +++ b/app/static/i18n.js @@ -27,9 +27,9 @@ const TRANSLATIONS = { 'bugReport.myReportsHint': 'Only reports sent from this account, with the status set by the admin team.', 'bugReport.myReportsEmpty': "You haven't sent any reports yet.", 'bugReport.statusOpen': 'Not read yet', - 'bugReport.statusRead': 'Read', + 'bugReport.statusRead': 'Acknowledged', 'bugReport.statusResolved': 'Resolved', - 'bugReport.backLink': '← Back to home', + 'bugReport.backLink': 'Back to home', 'nav.logoutTitle': 'Log out', 'nav.logoutAria': 'Log out of your account', 'nav.deposit': 'Deposit', @@ -190,9 +190,9 @@ const TRANSLATIONS = { 'bugReport.myReportsHint': "Solo le segnalazioni inviate da questo account, con lo stato aggiornato dall'amministrazione.", 'bugReport.myReportsEmpty': 'Non hai ancora inviato segnalazioni.', 'bugReport.statusOpen': 'Da leggere', - 'bugReport.statusRead': 'Letta', + 'bugReport.statusRead': 'Presa in carico', 'bugReport.statusResolved': 'Risolta', - 'bugReport.backLink': '← Torna alla home', + 'bugReport.backLink': 'Torna alla home', 'nav.logoutTitle': 'Esci', 'nav.logoutAria': "Esci dall'account", 'nav.deposit': 'Deposito', @@ -350,9 +350,9 @@ const TRANSLATIONS = { 'bugReport.myReportsHint': 'Solo los informes enviados desde esta cuenta, con el estado actualizado por el equipo de administración.', 'bugReport.myReportsEmpty': 'Todavía no has enviado ningún informe.', 'bugReport.statusOpen': 'Sin leer', - 'bugReport.statusRead': 'Leído', + 'bugReport.statusRead': 'En curso', 'bugReport.statusResolved': 'Resuelto', - 'bugReport.backLink': '← Volver al inicio', + 'bugReport.backLink': 'Volver al inicio', 'nav.logoutTitle': 'Salir', 'nav.logoutAria': 'Cerrar sesión', 'nav.deposit': 'Depósito', @@ -510,9 +510,9 @@ const TRANSLATIONS = { 'bugReport.myReportsHint': "Seulement les signalements envoyés depuis ce compte, avec le statut mis à jour par l'équipe d'administration.", 'bugReport.myReportsEmpty': "Vous n'avez encore envoyé aucun signalement.", 'bugReport.statusOpen': 'Non lu', - 'bugReport.statusRead': 'Lu', + 'bugReport.statusRead': 'Prise en charge', 'bugReport.statusResolved': 'Résolu', - 'bugReport.backLink': "← Retour à l'accueil", + 'bugReport.backLink': "Retour à l'accueil", 'nav.logoutTitle': 'Se déconnecter', 'nav.logoutAria': 'Se déconnecter du compte', 'nav.deposit': 'Dépôt', @@ -670,9 +670,9 @@ const TRANSLATIONS = { 'bugReport.myReportsHint': 'Nur Meldungen, die von diesem Konto gesendet wurden, mit dem vom Admin-Team aktualisierten Status.', 'bugReport.myReportsEmpty': 'Du hast noch keine Meldungen gesendet.', 'bugReport.statusOpen': 'Ungelesen', - 'bugReport.statusRead': 'Gelesen', + 'bugReport.statusRead': 'In Bearbeitung', 'bugReport.statusResolved': 'Gelöst', - 'bugReport.backLink': '← Zurück zur Startseite', + 'bugReport.backLink': 'Zurück zur Startseite', 'nav.logoutTitle': 'Abmelden', 'nav.logoutAria': 'Vom Konto abmelden', 'nav.deposit': 'Einzahlung', @@ -830,9 +830,9 @@ const TRANSLATIONS = { 'bugReport.myReportsHint': 'Только сообщения, отправленные с этого аккаунта, со статусом, обновлённым администрацией.', 'bugReport.myReportsEmpty': 'Вы ещё не отправляли сообщений.', 'bugReport.statusOpen': 'Не прочитано', - 'bugReport.statusRead': 'Прочитано', + 'bugReport.statusRead': 'В обработке', 'bugReport.statusResolved': 'Решено', - 'bugReport.backLink': '← Назад на главную', + 'bugReport.backLink': 'Назад на главную', 'nav.logoutTitle': 'Выйти', 'nav.logoutAria': 'Выйти из аккаунта', 'nav.deposit': 'Депозит', @@ -990,9 +990,9 @@ const TRANSLATIONS = { 'bugReport.myReportsHint': '仅显示此账户发送的反馈,状态由管理团队更新。', 'bugReport.myReportsEmpty': '您还没有发送过反馈。', 'bugReport.statusOpen': '待处理', - 'bugReport.statusRead': '已读', + 'bugReport.statusRead': '处理中', 'bugReport.statusResolved': '已解决', - 'bugReport.backLink': '← 返回首页', + 'bugReport.backLink': '返回首页', 'nav.logoutTitle': '退出登录', 'nav.logoutAria': '退出账户', 'nav.deposit': '存款', diff --git a/app/static/report-bug.html b/app/static/report-bug.html index 38b96e7..4b4de4b 100644 --- a/app/static/report-bug.html +++ b/app/static/report-bug.html @@ -4,31 +4,53 @@ Report a bug + -
-
- +
+ +
+ + + PLM Lottery + +
+ + + Back to home + + +
-

Report a bug

-

Found a problem? Describe it below — your report goes straight to the admin panel.

-

- Please write your bug report in English, regardless of the language you're browsing in — this helps us handle it faster. -

+
+
+ +
+
+

Report a bug

+

Found a problem? Describe it below — your report goes straight to the admin panel.

+
+
+
+ + Please write your bug report in English, regardless of the language you're browsing in — this helps us handle it faster. +
+ - + +
0 / 2000
@@ -36,13 +58,14 @@
-
@@ -62,6 +85,11 @@ return String(s).replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[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(); @@ -88,6 +116,7 @@ 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) { @@ -101,29 +130,29 @@ async function loadMyBugReports() { const token = localStorage.getItem('plm_token'); - const card = document.getElementById('my-reports-card'); + const section = document.getElementById('my-reports-section'); if (!token) { - card.classList.add('hidden'); + section.classList.add('hidden'); return; } try { const res = await fetch('/bug-reports/mine', { headers: { Authorization: 'Bearer ' + token } }); if (!res.ok) { - card.classList.add('hidden'); + section.classList.add('hidden'); return; } const reports = await res.json(); - card.classList.remove('hidden'); + section.classList.remove('hidden'); const list = document.getElementById('my-reports-list'); list.innerHTML = reports.map((r) => ` -
+
${escapeHtml(t('bugReport.status' + r.status.charAt(0).toUpperCase() + r.status.slice(1)))} - ${escapeHtml(r.description)} - ${new Date(r.created_at).toLocaleString(currentDateLocale())} + ${escapeHtml(r.description)} + ${new Date(r.created_at).toLocaleDateString(currentDateLocale(), { day: 'numeric', month: 'short', year: 'numeric' })}
- `).join('') || `

${escapeHtml(t('bugReport.myReportsEmpty'))}

`; + `).join('') || `

${escapeHtml(t('bugReport.myReportsEmpty'))}

`; } catch (e) { - card.classList.add('hidden'); + section.classList.add('hidden'); } } @@ -133,6 +162,7 @@ loadMyBugReports(); } + updateCharCount(); loadMyBugReports(); diff --git a/app/static/style.css b/app/static/style.css index 0a7f111..08937c5 100644 --- a/app/static/style.css +++ b/app/static/style.css @@ -254,28 +254,92 @@ button.link:hover, a.link:hover { filter: none; color: var(--color-foreground); .toast.error { background: var(--color-destructive-bg); color: var(--color-destructive); } @keyframes toast-in { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } } -.lang-bar { display: flex; justify-content: flex-end; margin-bottom: 4px; } +/* --- /report-bug: a standalone page (no logged-in navbar), so it gets its + own slim top bar rather than the app's bottom tab bar / sticky header. --- */ +.app-shell-bugreport { padding-bottom: 32px; } -.english-notice { - background: #FEF3C7; color: #92400E; border: 1px solid #F59E0B; - border-radius: var(--radius-sm); padding: 10px 12px; font-size: 0.85rem; font-weight: 500; +.bugreport-topbar { + display: flex; align-items: center; justify-content: space-between; gap: 12px; + padding: 4px 0 20px; margin-bottom: 20px; border-bottom: 1px solid var(--color-border); +} +.bugreport-topbar .brand { + display: flex; align-items: center; gap: 8px; font-weight: 700; font-size: 1rem; + letter-spacing: -0.01em; color: var(--color-foreground); text-decoration: none; +} +.bugreport-topbar .brand-mark { width: 26px; height: 26px; border-radius: 50%; flex-shrink: 0; display: block; } +.bugreport-topbar-right { display: flex; align-items: center; gap: 12px; } + +/* Pill button, same idiom as .trust-pill / .chain-status-pill elsewhere on the + site: a bordered chip rather than a bare text link, so "go back" reads as an + actual control instead of fading into the surrounding copy. */ +.back-home-btn { + display: inline-flex; align-items: center; gap: 6px; + font-size: 0.8rem; font-weight: 500; color: var(--color-muted-foreground); + background: var(--color-surface); border: 1px solid var(--color-border); + padding: 6px 12px 6px 10px; border-radius: 999px; text-decoration: none; + transition: color 150ms, border-color 150ms, background 150ms; +} +.back-home-btn .icon { width: 15px; height: 15px; } +.back-home-btn:hover { + color: var(--color-foreground); background: var(--color-surface-inset); + border-color: color-mix(in srgb, var(--color-ring) 40%, var(--color-border)); +} +.back-home-btn:focus-visible { outline: 2px solid var(--color-ring); outline-offset: 2px; } + +.bugreport-hero { display: flex; align-items: flex-start; gap: 14px; margin-bottom: 20px; } +.bugreport-hero-icon { + width: 44px; height: 44px; flex-shrink: 0; border-radius: 999px; + background: color-mix(in srgb, var(--color-primary) 14%, transparent); + color: var(--color-primary); + display: flex; align-items: center; justify-content: center; +} +.bugreport-hero-icon .icon { width: 22px; height: 22px; } +.bugreport-hero h1 { font-size: 1.3rem; font-weight: 700; margin: 2px 0 4px; text-wrap: balance; } +.bugreport-hero p { color: var(--color-muted-foreground); font-size: 0.9rem; margin: 0; max-width: 46ch; } + +/* Info callout, anchored inside the form card right above the field it + applies to — not a warning (that's what the amber status badges below are + for), so it gets the accent hue instead, keeping the two meanings visually + distinct. */ +.field-note { + display: flex; align-items: flex-start; gap: 10px; + background: color-mix(in srgb, var(--color-accent) 10%, transparent); + border: 1px solid color-mix(in srgb, var(--color-accent) 28%, transparent); + color: color-mix(in srgb, var(--color-accent) 75%, var(--color-foreground)); + border-radius: var(--radius-sm); padding: 10px 12px; font-size: 0.82rem; line-height: 1.4; + margin-bottom: 16px; +} +.field-note .icon { width: 16px; height: 16px; margin-top: 1px; flex-shrink: 0; } + +.char-count { + font-variant-numeric: tabular-nums; text-align: right; + font-size: 0.75rem; color: var(--color-muted-foreground); margin-top: 4px; } .badge { display: inline-block; font-size: 0.72rem; font-weight: 600; padding: 2px 8px; border-radius: 999px; background: var(--color-background); border: 1px solid var(--color-border); + flex-shrink: 0; } -.badge.bug-status-open { background: #FEF3C7; color: #92400E; border-color: #F59E0B; } +.badge.bug-status-open { background: color-mix(in srgb, var(--color-primary) 16%, transparent); color: #92400E; border-color: color-mix(in srgb, var(--color-primary) 55%, transparent); } .badge.bug-status-read { background: var(--color-background); color: var(--color-muted-foreground); } .badge.bug-status-resolved { background: var(--color-success-bg); color: var(--color-success); border-color: var(--color-success); } -.my-report-row { +.report-row { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; - padding: 10px 0; border-bottom: 1px solid var(--color-border); + padding: 12px 0; border-bottom: 1px solid var(--color-border); } -.my-report-row:last-child { border-bottom: none; } -.my-report-desc { flex: 1 1 200px; font-size: 0.9rem; } -.my-report-date { font-size: 0.8rem; color: var(--color-muted-foreground); white-space: nowrap; } +.report-row:first-child { padding-top: 0; } +.report-row:last-child { padding-bottom: 0; border-bottom: none; } +.report-row-desc { + flex: 1 1 200px; font-size: 0.88rem; + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; +} +.report-row-date { + font-size: 0.78rem; color: var(--color-muted-foreground); white-space: nowrap; + font-variant-numeric: tabular-nums; +} +.report-empty { margin: 0; } /* --- landing hero (shown only when logged out) --- */ body {