2026-07-27 08:35:47 +02:00
<!DOCTYPE html>
2026-07-31 16:03:38 +02:00
< html lang = "en" >
2026-07-27 08:35:47 +02:00
< head >
< meta charset = "UTF-8" >
< meta name = "viewport" content = "width=device-width, initial-scale=1.0" >
2026-07-31 16:03:38 +02:00
< title data-i18n = "bugReport.pageTitle" > Report a bug</ title >
2026-07-31 16:28:37 +02:00
< link rel = "icon" type = "image/svg+xml" href = "/logo.svg" >
2026-07-27 08:35:47 +02:00
< link rel = "stylesheet" href = "/style.css" >
</ head >
< body >
2026-07-31 16:28:37 +02:00
< 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 >
2026-07-31 16:03:38 +02:00
</ div >
2026-07-31 16:28:37 +02:00
< 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 >
2026-07-31 15:50:40 +02:00
< div class = "card" id = "report-form" >
2026-07-31 16:28:37 +02:00
< 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 >
2026-07-31 16:03:38 +02:00
< label for = "bug-description" data-i18n = "bugReport.descriptionLabel" > What happened?</ label >
2026-07-31 16:28:37 +02:00
< 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 >
2026-07-31 15:50:40 +02:00
2026-07-31 16:03:38 +02:00
< label for = "bug-contact" data-i18n = "bugReport.contactLabel" > Contact (optional)</ label >
< input id = "bug-contact" type = "text" maxlength = "256" data-i18n-placeholder = "bugReport.contactPlaceholder" >
2026-07-31 15:50:40 +02:00
2026-07-31 16:03:38 +02:00
< button onclick = "submitBugReport()" id = "bug-submit-btn" data-i18n = "bugReport.submitBtn" > Send report</ button >
2026-07-31 15:50:40 +02:00
</ div >
2026-07-31 16:28:37 +02:00
< 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 >
2026-07-31 15:50:40 +02:00
</ div >
2026-07-27 08:35:47 +02:00
</ div >
2026-07-31 15:50:40 +02:00
< div id = "toast-container" aria-live = "polite" ></ div >
2026-07-31 16:03:38 +02:00
< script src = "/i18n.js" ></ script >
2026-07-31 15:50:40 +02:00
< 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 ) => ({ '&' : '&' , '<' : '<' , '>' : '>' , '"' : '"' , "'" : ''' }[ c ]));
}
2026-07-31 16:28:37 +02:00
function updateCharCount () {
const field = document . getElementById ( 'bug-description' );
document . getElementById ( 'char-count' ). textContent = field . value . length + ' / ' + field . maxLength ;
}
2026-07-31 15:50:40 +02:00
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 ) {
2026-07-31 16:03:38 +02:00
toast ( t ( 'bugReport.blankError' ), 'error' );
2026-07-31 15:50:40 +02:00
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 ;
2026-07-31 16:03:38 +02:00
btn . textContent = t ( 'bugReport.submitting' );
2026-07-31 15:50:40 +02:00
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 = '' ;
2026-07-31 16:28:37 +02:00
updateCharCount ();
2026-07-31 16:03:38 +02:00
toast ( t ( 'bugReport.successToast' ), 'success' );
2026-07-31 15:50:40 +02:00
loadMyBugReports ();
} catch ( e ) {
2026-07-31 16:03:38 +02:00
toast ( t ( 'bugReport.errorPrefix' ) + e . message , 'error' );
2026-07-31 15:50:40 +02:00
} finally {
btn . disabled = false ;
btn . textContent = original ;
2026-07-31 16:03:38 +02:00
applyStaticTranslations ( btn );
2026-07-31 15:50:40 +02:00
}
}
async function loadMyBugReports () {
const token = localStorage . getItem ( 'plm_token' );
2026-07-31 16:28:37 +02:00
const section = document . getElementById ( 'my-reports-section' );
2026-07-31 15:50:40 +02:00
if ( ! token ) {
2026-07-31 16:28:37 +02:00
section . classList . add ( 'hidden' );
2026-07-31 15:50:40 +02:00
return ;
}
try {
const res = await fetch ( '/bug-reports/mine' , { headers : { Authorization : 'Bearer ' + token } });
if ( ! res . ok ) {
2026-07-31 16:28:37 +02:00
section . classList . add ( 'hidden' );
2026-07-31 15:50:40 +02:00
return ;
}
const reports = await res . json ();
2026-07-31 16:28:37 +02:00
section . classList . remove ( 'hidden' );
2026-07-31 15:50:40 +02:00
const list = document . getElementById ( 'my-reports-list' );
list . innerHTML = reports . map (( r ) => `
2026-07-31 16:28:37 +02:00
<div class="report-row">
2026-07-31 16:03:38 +02:00
<span class="badge bug-status- ${ escapeHtml ( r . status ) } "> ${ escapeHtml ( t ( 'bugReport.status' + r . status . charAt ( 0 ). toUpperCase () + r . status . slice ( 1 ))) } </span>
2026-07-31 16:28:37 +02:00
<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>
2026-07-31 15:50:40 +02:00
</div>
2026-07-31 16:28:37 +02:00
` ). join ( '' ) || `<p class="hint report-empty"> ${ escapeHtml ( t ( 'bugReport.myReportsEmpty' )) } </p>` ;
2026-07-31 15:50:40 +02:00
} catch ( e ) {
2026-07-31 16:28:37 +02:00
section . classList . add ( 'hidden' );
2026-07-31 15:50:40 +02:00
}
}
2026-07-31 16:03:38 +02:00
// 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 ();
}
2026-07-31 16:28:37 +02:00
updateCharCount ();
2026-07-31 15:50:40 +02:00
loadMyBugReports ();
</ script >
2026-07-27 08:35:47 +02:00
</ body >
</ html >