Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e5af15087c | ||
|
|
a384b08044 |
@@ -210,12 +210,13 @@ Two static SPAs served directly by FastAPI (`main.py` mounts `app/static/` and a
|
|||||||
|
|
||||||
Both talk to the same JSON API; there's no admin/user API split beyond `require_admin`.
|
Both talk to the same JSON API; there's no admin/user API split beyond `require_admin`.
|
||||||
|
|
||||||
## Internationalization (`/` only)
|
## Internationalization (`/` and `/report-bug`)
|
||||||
|
|
||||||
`app/static/i18n.js` holds every user-facing string of `/` in 7 languages (en, it, es, fr, de, ru, zh) as one flat `TRANSLATIONS` table — no build step, no fetch, loaded before `app.js` so `t()` is always available. Language: `localStorage.plm_lang` → `navigator.language` → `en`. The switcher sits in the **chain-bar, not the navbar**, deliberately: the navbar is hidden until login, which would leave the landing page and login form untranslatable for exactly the users who need it.
|
`app/static/i18n.js` holds every user-facing string of `/` and `/report-bug` in 7 languages (en, it, es, fr, de, ru, zh) as one flat `TRANSLATIONS` table — no build step, no fetch. `/` loads it before `app.js`; `/report-bug` loads it before its own inline script — either way `t()` is always available by the time it's called. Language: `localStorage.plm_lang` → `navigator.language` → `en`, shared across both pages since they read/write the same `localStorage` key. On `/` the switcher sits in the **chain-bar, not the navbar**, deliberately: the navbar is hidden until login, which would leave the landing page and login form untranslatable for exactly the users who need it. `/report-bug` has no navbar at all, so its switcher is just a top-right bar of its own.
|
||||||
|
|
||||||
- Static markup is translated by attribute (`data-i18n`, plus `-html`, `-placeholder`, `-title`, `-aria-label`, `-alt`) via `applyStaticTranslations(root?)`; anything rendered from server data uses `t()` in `app.js` and is re-rendered by `onLanguageChange()`. An element belongs to one camp or the other, **never both**, or the two mechanisms overwrite each other — that's why `#bet-btn` has no `data-i18n`: its label carries the configurable bet amount, so `renderBetButton()` owns it.
|
- Static markup is translated by attribute (`data-i18n`, plus `-html`, `-placeholder`, `-title`, `-aria-label`, `-alt`) via `applyStaticTranslations(root?)`; anything rendered from server data uses `t()` directly (`app.js`'s `onLanguageChange()`, `report-bug.html`'s own inline equivalent) and is re-rendered on a language switch. An element belongs to one camp or the other, **never both**, or the two mechanisms overwrite each other — that's why `#bet-btn` has no `data-i18n`: its label carries the configurable bet amount, so `renderBetButton()` owns it.
|
||||||
- **Every language must have exactly the same key set.** There is no fallback beyond `en`; a missing key renders as the raw key string.
|
- **Every language must have exactly the same key set.** There is no fallback beyond `en`; a missing key renders as the raw key string.
|
||||||
|
- `/report-bug`'s `bugReport.englishNotice` string is itself translated into all 7 languages — it just always *says*, in whichever language the visitor reads, to write the actual bug description in English (so the admin panel, which is Italian-operator-facing and untranslated, doesn't end up with reports in 7 different languages).
|
||||||
- `/admin` is intentionally untranslated (operator-facing, Italian), as is `/guida`.
|
- `/admin` is intentionally untranslated (operator-facing, Italian), as is `/guida`.
|
||||||
|
|
||||||
**API error contract** (`app/api/errors.py`) — the API is single-language by design. Failures answer with a structured `detail`: `{"code", "message", "params"}`, where `message` is English for non-dashboard consumers and `code` is what the frontend maps to `error.<code>` in `i18n.js` (falling back to `message` for an unknown code). `BetError`/`WithdrawalError` subclass `ApiError` and carry the code from where the failure happens. Even the catch-all 500 handler answers in that shape (`internal_error`), so clients never special-case unexpected errors, and the exception text stays in `logs/app.log`. Adding a user-facing error: give it a code, add `error.<code>` to all 7 languages, and pass values through `params` (amounts as `*_sats` — the frontend derives a `*_plm` sibling) instead of baking them into English text.
|
**API error contract** (`app/api/errors.py`) — the API is single-language by design. Failures answer with a structured `detail`: `{"code", "message", "params"}`, where `message` is English for non-dashboard consumers and `code` is what the frontend maps to `error.<code>` in `i18n.js` (falling back to `message` for an unknown code). `BetError`/`WithdrawalError` subclass `ApiError` and carry the code from where the failure happens. Even the catch-all 500 handler answers in that shape (`internal_error`), so clients never special-case unexpected errors, and the exception text stays in `logs/app.log`. Adding a user-facing error: give it a code, add `error.<code>` to all 7 languages, and pass values through `params` (amounts as `*_sats` — the frontend derives a `*_plm` sibling) instead of baking them into English text.
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ router = APIRouter(prefix="/bug-reports", tags=["bug-reports"])
|
|||||||
|
|
||||||
|
|
||||||
class BugReportCreate(BaseModel):
|
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)
|
contact: str | None = Field(default=None, max_length=256)
|
||||||
|
|
||||||
@field_validator("description")
|
@field_validator("description")
|
||||||
|
|||||||
+2
-2
@@ -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) {
|
function bugReportBadge(status) {
|
||||||
return `<span class="badge bug-status-${escapeHtml(status)}">${escapeHtml(BUG_REPORT_STATUS_LABELS[status] || status)}</span>`;
|
return `<span class="badge bug-status-${escapeHtml(status)}">${escapeHtml(BUG_REPORT_STATUS_LABELS[status] || status)}</span>`;
|
||||||
@@ -366,7 +366,7 @@ async function loadBugReports() {
|
|||||||
<td>
|
<td>
|
||||||
${bugReportBadge(r.status)}
|
${bugReportBadge(r.status)}
|
||||||
<div class="bug-actions">
|
<div class="bug-actions">
|
||||||
${r.status === 'open' ? `<button class="secondary" onclick="setBugReportStatus(${r.id}, 'read', this)">Segna come letta</button>` : ''}
|
${r.status === 'open' ? `<button class="secondary" onclick="setBugReportStatus(${r.id}, 'read', this)">Segna come presa in carico</button>` : ''}
|
||||||
${r.status !== 'resolved' ? `<button class="secondary" onclick="setBugReportStatus(${r.id}, 'resolved', this)">Segna come risolta</button>` : ''}
|
${r.status !== 'resolved' ? `<button class="secondary" onclick="setBugReportStatus(${r.id}, 'resolved', this)">Segna come risolta</button>` : ''}
|
||||||
${r.status !== 'open' ? `<button class="secondary" onclick="setBugReportStatus(${r.id}, 'open', this)">Riapri</button>` : ''}
|
${r.status !== 'open' ? `<button class="secondary" onclick="setBugReportStatus(${r.id}, 'open', this)">Riapri</button>` : ''}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -10,6 +10,26 @@ const TRANSLATIONS = {
|
|||||||
'nav.guideTitle': 'Guide',
|
'nav.guideTitle': 'Guide',
|
||||||
'nav.guideAria': 'Open the user guide',
|
'nav.guideAria': 'Open the user guide',
|
||||||
'nav.bugReport': 'Report a bug',
|
'nav.bugReport': 'Report a bug',
|
||||||
|
'bugReport.pageTitle': 'Report a bug',
|
||||||
|
'bugReport.heading': 'Report a bug',
|
||||||
|
'bugReport.intro': 'Found a problem? Describe it below — your report goes straight to the admin panel.',
|
||||||
|
'bugReport.englishNotice': "Please write your bug report in English, regardless of the language you're browsing in — this helps us handle it faster.",
|
||||||
|
'bugReport.descriptionLabel': 'What happened?',
|
||||||
|
'bugReport.descriptionPlaceholder': 'Describe the bug: what you were doing, what you expected, and what happened instead.',
|
||||||
|
'bugReport.contactLabel': 'Contact (optional)',
|
||||||
|
'bugReport.contactPlaceholder': "Email or other contact, if you'd like a reply",
|
||||||
|
'bugReport.submitBtn': 'Send report',
|
||||||
|
'bugReport.submitting': 'Sending…',
|
||||||
|
'bugReport.blankError': 'Describe the bug before sending.',
|
||||||
|
'bugReport.successToast': 'Thanks! Report sent.',
|
||||||
|
'bugReport.errorPrefix': 'Error sending: ',
|
||||||
|
'bugReport.myReportsTitle': 'Your reports',
|
||||||
|
'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': 'Acknowledged',
|
||||||
|
'bugReport.statusResolved': 'Resolved',
|
||||||
|
'bugReport.backLink': 'Back to home',
|
||||||
'nav.logoutTitle': 'Log out',
|
'nav.logoutTitle': 'Log out',
|
||||||
'nav.logoutAria': 'Log out of your account',
|
'nav.logoutAria': 'Log out of your account',
|
||||||
'nav.deposit': 'Deposit',
|
'nav.deposit': 'Deposit',
|
||||||
@@ -153,6 +173,26 @@ const TRANSLATIONS = {
|
|||||||
'nav.guideTitle': 'Guida',
|
'nav.guideTitle': 'Guida',
|
||||||
'nav.guideAria': 'Apri la guida utente',
|
'nav.guideAria': 'Apri la guida utente',
|
||||||
'nav.bugReport': 'Segnala un bug',
|
'nav.bugReport': 'Segnala un bug',
|
||||||
|
'bugReport.pageTitle': 'Segnala un bug',
|
||||||
|
'bugReport.heading': 'Segnala un bug',
|
||||||
|
'bugReport.intro': 'Hai trovato un problema? Descrivilo qui sotto: la segnalazione arriva direttamente al pannello di amministrazione.',
|
||||||
|
'bugReport.englishNotice': "Scrivi la segnalazione in inglese, indipendentemente dalla lingua che stai usando per navigare: questo ci aiuta a gestirla più velocemente.",
|
||||||
|
'bugReport.descriptionLabel': 'Cosa è successo?',
|
||||||
|
'bugReport.descriptionPlaceholder': 'Descrivi il bug: cosa stavi facendo, cosa ti aspettavi e cosa è successo invece.',
|
||||||
|
'bugReport.contactLabel': 'Contatto (opzionale)',
|
||||||
|
'bugReport.contactPlaceholder': 'Email o altro recapito, se vuoi essere ricontattato',
|
||||||
|
'bugReport.submitBtn': 'Invia segnalazione',
|
||||||
|
'bugReport.submitting': 'Invio…',
|
||||||
|
'bugReport.blankError': 'Descrivi il bug prima di inviare.',
|
||||||
|
'bugReport.successToast': 'Grazie! Segnalazione inviata.',
|
||||||
|
'bugReport.errorPrefix': "Errore nell'invio: ",
|
||||||
|
'bugReport.myReportsTitle': 'Le tue segnalazioni',
|
||||||
|
'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': 'Presa in carico',
|
||||||
|
'bugReport.statusResolved': 'Risolta',
|
||||||
|
'bugReport.backLink': 'Torna alla home',
|
||||||
'nav.logoutTitle': 'Esci',
|
'nav.logoutTitle': 'Esci',
|
||||||
'nav.logoutAria': "Esci dall'account",
|
'nav.logoutAria': "Esci dall'account",
|
||||||
'nav.deposit': 'Deposito',
|
'nav.deposit': 'Deposito',
|
||||||
@@ -293,6 +333,26 @@ const TRANSLATIONS = {
|
|||||||
'nav.guideTitle': 'Guía',
|
'nav.guideTitle': 'Guía',
|
||||||
'nav.guideAria': 'Abrir la guía del usuario',
|
'nav.guideAria': 'Abrir la guía del usuario',
|
||||||
'nav.bugReport': 'Reportar un error',
|
'nav.bugReport': 'Reportar un error',
|
||||||
|
'bugReport.pageTitle': 'Reportar un error',
|
||||||
|
'bugReport.heading': 'Reportar un error',
|
||||||
|
'bugReport.intro': '¿Encontraste un problema? Descríbelo a continuación: el informe llega directamente al panel de administración.',
|
||||||
|
'bugReport.englishNotice': 'Escribe el informe en inglés, independientemente del idioma que estés usando para navegar: esto nos ayuda a gestionarlo más rápido.',
|
||||||
|
'bugReport.descriptionLabel': '¿Qué pasó?',
|
||||||
|
'bugReport.descriptionPlaceholder': 'Describe el error: qué estabas haciendo, qué esperabas y qué sucedió en su lugar.',
|
||||||
|
'bugReport.contactLabel': 'Contacto (opcional)',
|
||||||
|
'bugReport.contactPlaceholder': 'Correo u otro contacto, si quieres que te respondamos',
|
||||||
|
'bugReport.submitBtn': 'Enviar informe',
|
||||||
|
'bugReport.submitting': 'Enviando…',
|
||||||
|
'bugReport.blankError': 'Describe el error antes de enviarlo.',
|
||||||
|
'bugReport.successToast': '¡Gracias! Informe enviado.',
|
||||||
|
'bugReport.errorPrefix': 'Error al enviar: ',
|
||||||
|
'bugReport.myReportsTitle': 'Tus informes',
|
||||||
|
'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': 'En curso',
|
||||||
|
'bugReport.statusResolved': 'Resuelto',
|
||||||
|
'bugReport.backLink': 'Volver al inicio',
|
||||||
'nav.logoutTitle': 'Salir',
|
'nav.logoutTitle': 'Salir',
|
||||||
'nav.logoutAria': 'Cerrar sesión',
|
'nav.logoutAria': 'Cerrar sesión',
|
||||||
'nav.deposit': 'Depósito',
|
'nav.deposit': 'Depósito',
|
||||||
@@ -433,6 +493,26 @@ const TRANSLATIONS = {
|
|||||||
'nav.guideTitle': 'Guide',
|
'nav.guideTitle': 'Guide',
|
||||||
'nav.guideAria': "Ouvrir le guide de l'utilisateur",
|
'nav.guideAria': "Ouvrir le guide de l'utilisateur",
|
||||||
'nav.bugReport': 'Signaler un bug',
|
'nav.bugReport': 'Signaler un bug',
|
||||||
|
'bugReport.pageTitle': 'Signaler un bug',
|
||||||
|
'bugReport.heading': 'Signaler un bug',
|
||||||
|
'bugReport.intro': "Vous avez trouvé un problème ? Décrivez-le ci-dessous : le signalement arrive directement dans le panneau d'administration.",
|
||||||
|
'bugReport.englishNotice': "Rédigez votre signalement en anglais, quelle que soit la langue que vous utilisez pour naviguer : cela nous aide à le traiter plus rapidement.",
|
||||||
|
'bugReport.descriptionLabel': "Que s'est-il passé ?",
|
||||||
|
'bugReport.descriptionPlaceholder': "Décrivez le bug : ce que vous faisiez, ce que vous attendiez et ce qui s'est passé à la place.",
|
||||||
|
'bugReport.contactLabel': 'Contact (facultatif)',
|
||||||
|
'bugReport.contactPlaceholder': 'Email ou autre contact, si vous souhaitez une réponse',
|
||||||
|
'bugReport.submitBtn': 'Envoyer le signalement',
|
||||||
|
'bugReport.submitting': 'Envoi…',
|
||||||
|
'bugReport.blankError': "Décrivez le bug avant d'envoyer.",
|
||||||
|
'bugReport.successToast': 'Merci ! Signalement envoyé.',
|
||||||
|
'bugReport.errorPrefix': "Erreur lors de l'envoi : ",
|
||||||
|
'bugReport.myReportsTitle': 'Vos signalements',
|
||||||
|
'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': 'Prise en charge',
|
||||||
|
'bugReport.statusResolved': 'Résolu',
|
||||||
|
'bugReport.backLink': "Retour à l'accueil",
|
||||||
'nav.logoutTitle': 'Se déconnecter',
|
'nav.logoutTitle': 'Se déconnecter',
|
||||||
'nav.logoutAria': 'Se déconnecter du compte',
|
'nav.logoutAria': 'Se déconnecter du compte',
|
||||||
'nav.deposit': 'Dépôt',
|
'nav.deposit': 'Dépôt',
|
||||||
@@ -573,6 +653,26 @@ const TRANSLATIONS = {
|
|||||||
'nav.guideTitle': 'Anleitung',
|
'nav.guideTitle': 'Anleitung',
|
||||||
'nav.guideAria': 'Benutzerhandbuch öffnen',
|
'nav.guideAria': 'Benutzerhandbuch öffnen',
|
||||||
'nav.bugReport': 'Fehler melden',
|
'nav.bugReport': 'Fehler melden',
|
||||||
|
'bugReport.pageTitle': 'Fehler melden',
|
||||||
|
'bugReport.heading': 'Fehler melden',
|
||||||
|
'bugReport.intro': 'Ein Problem gefunden? Beschreibe es unten — die Meldung geht direkt an das Admin-Panel.',
|
||||||
|
'bugReport.englishNotice': 'Bitte schreibe die Fehlermeldung auf Englisch, unabhängig von der Sprache, die du gerade verwendest — das hilft uns, sie schneller zu bearbeiten.',
|
||||||
|
'bugReport.descriptionLabel': 'Was ist passiert?',
|
||||||
|
'bugReport.descriptionPlaceholder': 'Beschreibe den Fehler: was du getan hast, was du erwartet hast und was stattdessen passiert ist.',
|
||||||
|
'bugReport.contactLabel': 'Kontakt (optional)',
|
||||||
|
'bugReport.contactPlaceholder': 'E-Mail oder anderer Kontakt, falls du eine Antwort möchtest',
|
||||||
|
'bugReport.submitBtn': 'Meldung senden',
|
||||||
|
'bugReport.submitting': 'Senden…',
|
||||||
|
'bugReport.blankError': 'Beschreibe den Fehler, bevor du sendest.',
|
||||||
|
'bugReport.successToast': 'Danke! Meldung gesendet.',
|
||||||
|
'bugReport.errorPrefix': 'Fehler beim Senden: ',
|
||||||
|
'bugReport.myReportsTitle': 'Deine Meldungen',
|
||||||
|
'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': 'In Bearbeitung',
|
||||||
|
'bugReport.statusResolved': 'Gelöst',
|
||||||
|
'bugReport.backLink': 'Zurück zur Startseite',
|
||||||
'nav.logoutTitle': 'Abmelden',
|
'nav.logoutTitle': 'Abmelden',
|
||||||
'nav.logoutAria': 'Vom Konto abmelden',
|
'nav.logoutAria': 'Vom Konto abmelden',
|
||||||
'nav.deposit': 'Einzahlung',
|
'nav.deposit': 'Einzahlung',
|
||||||
@@ -713,6 +813,26 @@ const TRANSLATIONS = {
|
|||||||
'nav.guideTitle': 'Инструкция',
|
'nav.guideTitle': 'Инструкция',
|
||||||
'nav.guideAria': 'Открыть руководство пользователя',
|
'nav.guideAria': 'Открыть руководство пользователя',
|
||||||
'nav.bugReport': 'Сообщить об ошибке',
|
'nav.bugReport': 'Сообщить об ошибке',
|
||||||
|
'bugReport.pageTitle': 'Сообщить об ошибке',
|
||||||
|
'bugReport.heading': 'Сообщить об ошибке',
|
||||||
|
'bugReport.intro': 'Нашли проблему? Опишите её ниже — сообщение сразу попадёт в панель администратора.',
|
||||||
|
'bugReport.englishNotice': 'Пожалуйста, опишите ошибку на английском языке, независимо от языка интерфейса — это поможет нам обработать её быстрее.',
|
||||||
|
'bugReport.descriptionLabel': 'Что произошло?',
|
||||||
|
'bugReport.descriptionPlaceholder': 'Опишите ошибку: что вы делали, что ожидали и что произошло вместо этого.',
|
||||||
|
'bugReport.contactLabel': 'Контакт (необязательно)',
|
||||||
|
'bugReport.contactPlaceholder': 'Email или другой контакт, если хотите получить ответ',
|
||||||
|
'bugReport.submitBtn': 'Отправить сообщение',
|
||||||
|
'bugReport.submitting': 'Отправка…',
|
||||||
|
'bugReport.blankError': 'Опишите ошибку перед отправкой.',
|
||||||
|
'bugReport.successToast': 'Спасибо! Сообщение отправлено.',
|
||||||
|
'bugReport.errorPrefix': 'Ошибка отправки: ',
|
||||||
|
'bugReport.myReportsTitle': 'Ваши сообщения',
|
||||||
|
'bugReport.myReportsHint': 'Только сообщения, отправленные с этого аккаунта, со статусом, обновлённым администрацией.',
|
||||||
|
'bugReport.myReportsEmpty': 'Вы ещё не отправляли сообщений.',
|
||||||
|
'bugReport.statusOpen': 'Не прочитано',
|
||||||
|
'bugReport.statusRead': 'В обработке',
|
||||||
|
'bugReport.statusResolved': 'Решено',
|
||||||
|
'bugReport.backLink': 'Назад на главную',
|
||||||
'nav.logoutTitle': 'Выйти',
|
'nav.logoutTitle': 'Выйти',
|
||||||
'nav.logoutAria': 'Выйти из аккаунта',
|
'nav.logoutAria': 'Выйти из аккаунта',
|
||||||
'nav.deposit': 'Депозит',
|
'nav.deposit': 'Депозит',
|
||||||
@@ -853,6 +973,26 @@ const TRANSLATIONS = {
|
|||||||
'nav.guideTitle': '指南',
|
'nav.guideTitle': '指南',
|
||||||
'nav.guideAria': '打开用户指南',
|
'nav.guideAria': '打开用户指南',
|
||||||
'nav.bugReport': '报告问题',
|
'nav.bugReport': '报告问题',
|
||||||
|
'bugReport.pageTitle': '报告问题',
|
||||||
|
'bugReport.heading': '报告问题',
|
||||||
|
'bugReport.intro': '发现问题了吗?请在下面描述——您的反馈会直接发送到管理员面板。',
|
||||||
|
'bugReport.englishNotice': '请用英文描述问题,无论您当前使用的是哪种语言界面——这有助于我们更快处理。',
|
||||||
|
'bugReport.descriptionLabel': '发生了什么?',
|
||||||
|
'bugReport.descriptionPlaceholder': '描述问题:您当时在做什么、期望的结果是什么,以及实际发生了什么。',
|
||||||
|
'bugReport.contactLabel': '联系方式(可选)',
|
||||||
|
'bugReport.contactPlaceholder': '如果希望得到回复,请留下邮箱或其他联系方式',
|
||||||
|
'bugReport.submitBtn': '发送反馈',
|
||||||
|
'bugReport.submitting': '发送中…',
|
||||||
|
'bugReport.blankError': '请先描述问题再发送。',
|
||||||
|
'bugReport.successToast': '谢谢!反馈已发送。',
|
||||||
|
'bugReport.errorPrefix': '发送出错:',
|
||||||
|
'bugReport.myReportsTitle': '您的反馈',
|
||||||
|
'bugReport.myReportsHint': '仅显示此账户发送的反馈,状态由管理团队更新。',
|
||||||
|
'bugReport.myReportsEmpty': '您还没有发送过反馈。',
|
||||||
|
'bugReport.statusOpen': '待处理',
|
||||||
|
'bugReport.statusRead': '处理中',
|
||||||
|
'bugReport.statusResolved': '已解决',
|
||||||
|
'bugReport.backLink': '返回首页',
|
||||||
'nav.logoutTitle': '退出登录',
|
'nav.logoutTitle': '退出登录',
|
||||||
'nav.logoutAria': '退出账户',
|
'nav.logoutAria': '退出账户',
|
||||||
'nav.deposit': '存款',
|
'nav.deposit': '存款',
|
||||||
|
|||||||
+81
-30
@@ -1,37 +1,76 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="it">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Segnala un bug — PLM Lottery</title>
|
<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">
|
<link rel="stylesheet" href="/style.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="app-shell">
|
<div class="app-shell app-shell-bugreport">
|
||||||
<h1>Segnala un bug</h1>
|
|
||||||
<p>Hai trovato un problema? Descrivilo qui sotto: la segnalazione arriva direttamente al pannello di amministrazione.</p>
|
<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="card" id="report-form">
|
||||||
<label for="bug-description">Cosa è successo?</label>
|
<div class="field-note" id="english-notice">
|
||||||
<textarea id="bug-description" rows="6" maxlength="5000" placeholder="Descrivi il bug: cosa stavi facendo, cosa ti aspettavi e cosa è successo invece."></textarea>
|
<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-contact">Contatto (opzionale)</label>
|
<label for="bug-description" data-i18n="bugReport.descriptionLabel">What happened?</label>
|
||||||
<input id="bug-contact" type="text" maxlength="256" placeholder="Email o altro recapito, se vuoi essere ricontattato">
|
<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>
|
||||||
|
|
||||||
<button onclick="submitBugReport()" id="bug-submit-btn">Invia segnalazione</button>
|
<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>
|
||||||
|
|
||||||
<div class="card hidden" id="my-reports-card">
|
<div class="hidden" id="my-reports-section">
|
||||||
<h2>Le tue segnalazioni</h2>
|
<p class="section-label" data-i18n="bugReport.myReportsTitle">Your reports</p>
|
||||||
<p class="hint">Solo le segnalazioni inviate da questo account, con lo stato aggiornato dall'amministrazione.</p>
|
<div class="card">
|
||||||
<div id="my-reports-list"></div>
|
<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>
|
||||||
|
|
||||||
<p><a class="link" href="/">← Torna alla home</a></p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="toast-container" aria-live="polite"></div>
|
<div id="toast-container" aria-live="polite"></div>
|
||||||
|
|
||||||
|
<script src="/i18n.js"></script>
|
||||||
<script>
|
<script>
|
||||||
function toast(message, type) {
|
function toast(message, type) {
|
||||||
const container = document.getElementById('toast-container');
|
const container = document.getElementById('toast-container');
|
||||||
@@ -46,14 +85,17 @@
|
|||||||
return String(s).replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
|
return String(s).replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
|
||||||
}
|
}
|
||||||
|
|
||||||
const BUG_REPORT_STATUS_LABELS = { open: 'Da leggere', read: 'Letta', resolved: 'Risolta' };
|
function updateCharCount() {
|
||||||
|
const field = document.getElementById('bug-description');
|
||||||
|
document.getElementById('char-count').textContent = field.value.length + ' / ' + field.maxLength;
|
||||||
|
}
|
||||||
|
|
||||||
async function submitBugReport() {
|
async function submitBugReport() {
|
||||||
const btn = document.getElementById('bug-submit-btn');
|
const btn = document.getElementById('bug-submit-btn');
|
||||||
const description = document.getElementById('bug-description').value.trim();
|
const description = document.getElementById('bug-description').value.trim();
|
||||||
const contact = document.getElementById('bug-contact').value.trim();
|
const contact = document.getElementById('bug-contact').value.trim();
|
||||||
if (!description) {
|
if (!description) {
|
||||||
toast('Descrivi il bug prima di inviare.', 'error');
|
toast(t('bugReport.blankError'), 'error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,7 +105,7 @@
|
|||||||
|
|
||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
const original = btn.textContent;
|
const original = btn.textContent;
|
||||||
btn.textContent = 'Invio…';
|
btn.textContent = t('bugReport.submitting');
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/bug-reports', {
|
const res = await fetch('/bug-reports', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -74,44 +116,53 @@
|
|||||||
if (!res.ok) throw new Error(data.detail?.message || data.detail || res.statusText);
|
if (!res.ok) throw new Error(data.detail?.message || data.detail || res.statusText);
|
||||||
document.getElementById('bug-description').value = '';
|
document.getElementById('bug-description').value = '';
|
||||||
document.getElementById('bug-contact').value = '';
|
document.getElementById('bug-contact').value = '';
|
||||||
toast('Grazie! Segnalazione inviata.', 'success');
|
updateCharCount();
|
||||||
|
toast(t('bugReport.successToast'), 'success');
|
||||||
loadMyBugReports();
|
loadMyBugReports();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toast('Errore nell\'invio: ' + e.message, 'error');
|
toast(t('bugReport.errorPrefix') + e.message, 'error');
|
||||||
} finally {
|
} finally {
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
btn.textContent = original;
|
btn.textContent = original;
|
||||||
|
applyStaticTranslations(btn);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadMyBugReports() {
|
async function loadMyBugReports() {
|
||||||
const token = localStorage.getItem('plm_token');
|
const token = localStorage.getItem('plm_token');
|
||||||
const card = document.getElementById('my-reports-card');
|
const section = document.getElementById('my-reports-section');
|
||||||
if (!token) {
|
if (!token) {
|
||||||
card.classList.add('hidden');
|
section.classList.add('hidden');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/bug-reports/mine', { headers: { Authorization: 'Bearer ' + token } });
|
const res = await fetch('/bug-reports/mine', { headers: { Authorization: 'Bearer ' + token } });
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
card.classList.add('hidden');
|
section.classList.add('hidden');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const reports = await res.json();
|
const reports = await res.json();
|
||||||
card.classList.remove('hidden');
|
section.classList.remove('hidden');
|
||||||
const list = document.getElementById('my-reports-list');
|
const list = document.getElementById('my-reports-list');
|
||||||
list.innerHTML = reports.map((r) => `
|
list.innerHTML = reports.map((r) => `
|
||||||
<div class="my-report-row">
|
<div class="report-row">
|
||||||
<span class="badge bug-status-${escapeHtml(r.status)}">${escapeHtml(BUG_REPORT_STATUS_LABELS[r.status] || r.status)}</span>
|
<span class="badge bug-status-${escapeHtml(r.status)}">${escapeHtml(t('bugReport.status' + r.status.charAt(0).toUpperCase() + r.status.slice(1)))}</span>
|
||||||
<span class="my-report-desc">${escapeHtml(r.description)}</span>
|
<span class="report-row-desc" title="${escapeHtml(r.description)}">${escapeHtml(r.description)}</span>
|
||||||
<span class="my-report-date">${new Date(r.created_at).toLocaleString('it-IT')}</span>
|
<span class="report-row-date">${new Date(r.created_at).toLocaleDateString(currentDateLocale(), { day: 'numeric', month: 'short', year: 'numeric' })}</span>
|
||||||
</div>
|
</div>
|
||||||
`).join('') || '<p class="hint">Non hai ancora inviato segnalazioni.</p>';
|
`).join('') || `<p class="hint report-empty">${escapeHtml(t('bugReport.myReportsEmpty'))}</p>`;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
card.classList.add('hidden');
|
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();
|
loadMyBugReports();
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
+77
-6
@@ -254,21 +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); }
|
.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); } }
|
@keyframes toast-in { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }
|
||||||
|
|
||||||
|
/* --- /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; }
|
||||||
|
|
||||||
|
.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 {
|
.badge {
|
||||||
display: inline-block; font-size: 0.72rem; font-weight: 600; padding: 2px 8px;
|
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);
|
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-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); }
|
.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;
|
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; }
|
.report-row:first-child { padding-top: 0; }
|
||||||
.my-report-desc { flex: 1 1 200px; font-size: 0.9rem; }
|
.report-row:last-child { padding-bottom: 0; border-bottom: none; }
|
||||||
.my-report-date { font-size: 0.8rem; color: var(--color-muted-foreground); white-space: nowrap; }
|
.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) --- */
|
/* --- landing hero (shown only when logged out) --- */
|
||||||
body {
|
body {
|
||||||
|
|||||||
Reference in New Issue
Block a user