From 0c98df85263d8b9c20d4bbfe552ef8aa297da3ae Mon Sep 17 00:00:00 2001 From: Hamid Reza Mohammadi Date: Sat, 3 Oct 2026 08:17:31 +0330 Subject: [PATCH] editor service errors are displayed better --- GanjooRazor/Areas/User/Pages/Editor.cshtml | 20 ++--- GanjooRazor/Pages/Shared/_ErrorModal.cshtml | 14 ++++ .../Pages/Shared/_UserPanelLayout.cshtml | 3 +- GanjooRazor/wwwroot/css/user-panel.css | 27 +++++++ GanjooRazor/wwwroot/js/user-panel.js | 75 ++++++++++++++++++- 5 files changed, 125 insertions(+), 14 deletions(-) create mode 100644 GanjooRazor/Pages/Shared/_ErrorModal.cshtml diff --git a/GanjooRazor/Areas/User/Pages/Editor.cshtml b/GanjooRazor/Areas/User/Pages/Editor.cshtml index 4c47da27..c79ff9b7 100644 --- a/GanjooRazor/Areas/User/Pages/Editor.cshtml +++ b/GanjooRazor/Areas/User/Pages/Editor.cshtml @@ -1007,7 +1007,7 @@ location.reload(true); }, error: function (e) { - upToast(e.responseText || String(e), 'error'); + upShowError(e.responseText || String(e)); $('#savecorrections').show(); $('#saving').text(''); } @@ -1024,7 +1024,7 @@ id: poemId }, error: function (e) { - upToast(e.responseText || String(e), 'error'); + upShowError(e.responseText || String(e)); }, success: function (rhyme) { $(elementId).val(rhyme); @@ -1049,7 +1049,7 @@ poemid: id, }, error: function (e) { - upToast(e.responseText || 'خطا رخ داد.', 'error'); + upShowError(e.responseText || 'خطا رخ داد.'); }, success: function () { location.reload(true); @@ -1074,7 +1074,7 @@ }, error: function (e) { - upToast(e.responseText || String(e), 'error'); + upShowError(e.responseText || String(e)); }, success: function () { location.reload(true); @@ -1161,7 +1161,7 @@ location.href = '/?p=' + String(id); }, error: function (e) { - upToast(e.responseText || String(e), 'error'); + upShowError(e.responseText || String(e)); $('#breakpoem-' + String(vOrder)).show(); } @@ -1182,7 +1182,7 @@ upToast('انجام شد.'); }, error: function (e) { - upToast(e.responseText || String(e), 'error'); + upShowError(e.responseText || String(e)); } }); @@ -1210,7 +1210,7 @@ id: id, }, error: function (e) { - upToast(e.responseText || String(e), 'error'); + upShowError(e.responseText || String(e)); }, success: function () { upToast('انجام شد.'); @@ -1235,7 +1235,7 @@ id: id, }, error: function (e) { - upToast(e.responseText || String(e), 'error'); + upShowError(e.responseText || String(e)); }, success: function () { upToast('انجام شد.'); @@ -1268,7 +1268,7 @@ location.reload(); }, error: function (e) { - upToast(e.responseText || String(e), 'error'); + upShowError(e.responseText || String(e)); } }); @@ -1289,7 +1289,7 @@ location.reload(); }, error: function (e) { - upToast(e.responseText || String(e), 'error'); + upShowError(e.responseText || String(e)); } }); diff --git a/GanjooRazor/Pages/Shared/_ErrorModal.cshtml b/GanjooRazor/Pages/Shared/_ErrorModal.cshtml new file mode 100644 index 00000000..a0ad797b --- /dev/null +++ b/GanjooRazor/Pages/Shared/_ErrorModal.cshtml @@ -0,0 +1,14 @@ +@* Styled, non-auto-dismissing display for a service error's full text (these often carry the + backend's exception message and stack trace, which upToast's few-second toast never gave + anyone enough time to actually read before it faded out). Included once by + _UserPanelLayout.cshtml. Trigger with JS: upShowError(message) *@ + diff --git a/GanjooRazor/Pages/Shared/_UserPanelLayout.cshtml b/GanjooRazor/Pages/Shared/_UserPanelLayout.cshtml index c517edb4..1ac47209 100644 --- a/GanjooRazor/Pages/Shared/_UserPanelLayout.cshtml +++ b/GanjooRazor/Pages/Shared/_UserPanelLayout.cshtml @@ -205,10 +205,11 @@ + - + @await RenderSectionAsync("Scripts", required: false) diff --git a/GanjooRazor/wwwroot/css/user-panel.css b/GanjooRazor/wwwroot/css/user-panel.css index 93cdbcc1..4d64377a 100644 --- a/GanjooRazor/wwwroot/css/user-panel.css +++ b/GanjooRazor/wwwroot/css/user-panel.css @@ -475,6 +475,33 @@ body.up-modal-open { overflow: hidden; } +/* ---------- error modal (persistent service-error display, see _ErrorModal.cshtml) ---------- */ + +.up-modal--error { + max-width: 700px; + text-align: start; +} + +.up-modal__details { + max-height: 50vh; + overflow: auto; + white-space: pre-wrap; + word-break: break-word; + /* backend error text/stack traces are English - keep them LTR regardless of the page's own + RTL direction, same reasoning as .linputText elsewhere in this file */ + direction: ltr; + text-align: left; + font-family: 'Consolas', 'Courier New', monospace; + font-size: 0.82rem; + line-height: 1.5; + color: var(--up-text); + background: var(--up-bg); + border: 1px solid var(--up-border); + border-radius: var(--up-radius-sm); + padding: var(--up-space-2); + margin: 0 0 var(--up-space-4); +} + /* ---------- dashboard quick links ---------- */ .up-quick-links { diff --git a/GanjooRazor/wwwroot/js/user-panel.js b/GanjooRazor/wwwroot/js/user-panel.js index 65e2e651..735732d8 100644 --- a/GanjooRazor/wwwroot/js/user-panel.js +++ b/GanjooRazor/wwwroot/js/user-panel.js @@ -1,8 +1,8 @@ // User area (پیشخان کاربر) shared UI helpers. -// Provides upConfirm() and upToast() as drop-in, styled replacements for +// Provides upConfirm(), upToast() and upShowError() as drop-in, styled replacements for // window.confirm()/window.alert(), used across the panel's AJAX actions. -// Requires the markup from Areas/User/Pages/Shared/_ConfirmModal.cshtml and -// _Toasts.cshtml to be present on the page (included once by _UserPanelLayout). +// Requires the markup from Pages/Shared/_ConfirmModal.cshtml, _Toasts.cshtml and +// _ErrorModal.cshtml to be present on the page (included once by _UserPanelLayout). /** * Shows a styled confirmation dialog and resolves to true/false. @@ -88,3 +88,72 @@ function upToast(message, type) { }, 250); }, 4000); } + +/** + * Shows a service error's full text (often a backend exception message plus its stack trace) in + * a modal that stays on screen until closed, with a copy button - unlike upToast(), which fades + * out after 4 seconds and gives no way to get back the text it just showed. Falls back to + * upToast() if the modal markup isn't on the page. + * @param {string} message + */ +function upShowError(message) { + message = message == null ? '' : String(message); + var backdrop = document.getElementById('upErrorBackdrop'); + var detailsEl = document.getElementById('upErrorDetails'); + var closeBtn = document.getElementById('upErrorClose'); + var copyBtn = document.getElementById('upErrorCopy'); + + if (!backdrop || !detailsEl || !closeBtn || !copyBtn) { + upToast(message, 'error'); + return; + } + + detailsEl.textContent = message; + + backdrop.hidden = false; + document.body.classList.add('up-modal-open'); + + function cleanup() { + backdrop.hidden = true; + document.body.classList.remove('up-modal-open'); + closeBtn.removeEventListener('click', onClose); + copyBtn.removeEventListener('click', onCopy); + backdrop.removeEventListener('click', onBackdropClick); + document.removeEventListener('keydown', onKeyDown); + } + + function onClose() { cleanup(); } + function onBackdropClick(e) { if (e.target === backdrop) cleanup(); } + function onKeyDown(e) { if (e.key === 'Escape') cleanup(); } + + function onCopy() { + var restoreText = copyBtn.textContent; + function showCopied(ok) { + copyBtn.textContent = ok ? '✓ کپی شد' : 'کپی ممکن نشد'; + setTimeout(function () { copyBtn.textContent = restoreText; }, 1500); + } + if (navigator.clipboard && navigator.clipboard.writeText) { + navigator.clipboard.writeText(message).then(function () { showCopied(true); }, function () { showCopied(false); }); + } else { + // clipboard API unavailable (e.g. non-HTTPS context) - fall back to a temporary, + // off-screen textarea and the legacy execCommand copy path + var ta = document.createElement('textarea'); + ta.value = message; + ta.style.position = 'fixed'; + ta.style.left = '-9999px'; + document.body.appendChild(ta); + ta.select(); + var ok = false; + try { ok = document.execCommand('copy'); } catch (e) { ok = false; } + document.body.removeChild(ta); + showCopied(ok); + } + } + + closeBtn.addEventListener('click', onClose); + copyBtn.addEventListener('click', onCopy); + backdrop.addEventListener('click', onBackdropClick); + document.addEventListener('keydown', onKeyDown); + + closeBtn.focus(); +}