From 2f60043ed85040779fd1838361b3e801bf738208 Mon Sep 17 00:00:00 2001 From: Your Name <119736744+aborayan2022@users.noreply.github.com> Date: Sun, 22 Mar 2026 17:24:33 +0200 Subject: [PATCH] feat: add error handling with toast notifications across various tools and admin page --- .../src/components/layout/SiteAssistant.tsx | 2 ++ .../src/components/tools/BarcodeGenerator.tsx | 5 ++- frontend/src/components/tools/CropPdf.tsx | 5 ++- frontend/src/components/tools/ImageCrop.tsx | 5 ++- .../src/components/tools/ImageRotateFlip.tsx | 5 ++- frontend/src/components/tools/ImagesToPdf.tsx | 5 ++- frontend/src/components/tools/MergePdf.tsx | 5 ++- .../src/components/tools/PdfFlowchart.tsx | 9 +++-- frontend/src/components/tools/PdfMetadata.tsx | 5 ++- .../src/components/tools/QrCodeGenerator.tsx | 5 ++- frontend/src/components/tools/SignPdf.tsx | 5 ++- frontend/src/pages/InternalAdminPage.tsx | 33 ++++++++++++++++--- 12 files changed, 73 insertions(+), 16 deletions(-) diff --git a/frontend/src/components/layout/SiteAssistant.tsx b/frontend/src/components/layout/SiteAssistant.tsx index 464ccc2..ffa1ba7 100644 --- a/frontend/src/components/layout/SiteAssistant.tsx +++ b/frontend/src/components/layout/SiteAssistant.tsx @@ -2,6 +2,7 @@ import { useEffect, useRef, useState } from 'react'; import { useLocation } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { Bot, SendHorizontal, Sparkles, X } from 'lucide-react'; +import { toast } from 'sonner'; import { getToolSEO } from '@/config/seoData'; import { streamAssistantChat, type AssistantHistoryMessage } from '@/services/api'; import { trackEvent } from '@/services/analytics'; @@ -181,6 +182,7 @@ export default function SiteAssistant() { ? requestError.message : t('assistant.unavailable'); setError(message); + toast.error(message); setMessages((currentMessages) => currentMessages.map((currentMessage) => ( currentMessage.id === assistantMessageId diff --git a/frontend/src/components/tools/BarcodeGenerator.tsx b/frontend/src/components/tools/BarcodeGenerator.tsx index 43f017c..1b54a4a 100644 --- a/frontend/src/components/tools/BarcodeGenerator.tsx +++ b/frontend/src/components/tools/BarcodeGenerator.tsx @@ -6,6 +6,7 @@ import ProgressBar from '@/components/shared/ProgressBar'; import AdSlot from '@/components/layout/AdSlot'; import { useTaskPolling } from '@/hooks/useTaskPolling'; import { generateToolSchema } from '@/utils/seo'; +import { toast } from 'sonner'; import api, { type TaskResponse } from '@/services/api'; const BARCODE_TYPES = ['code128', 'code39', 'ean13', 'ean8', 'upca', 'isbn13', 'isbn10', 'issn', 'pzn'] as const; @@ -32,7 +33,9 @@ export default function BarcodeGenerator() { }); setTaskId(res.data.task_id); } catch (err) { - setError(err instanceof Error ? err.message : 'Failed to generate barcode.'); + const msg = err instanceof Error ? err.message : t('common.errors.processingFailed'); + setError(msg); + toast.error(msg); setPhase('done'); } }; diff --git a/frontend/src/components/tools/CropPdf.tsx b/frontend/src/components/tools/CropPdf.tsx index 99397b5..7abf430 100644 --- a/frontend/src/components/tools/CropPdf.tsx +++ b/frontend/src/components/tools/CropPdf.tsx @@ -9,6 +9,7 @@ import AdSlot from '@/components/layout/AdSlot'; import { useTaskPolling } from '@/hooks/useTaskPolling'; import { generateToolSchema } from '@/utils/seo'; import { useFileStore } from '@/stores/fileStore'; +import { toast } from 'sonner'; import api, { type TaskResponse } from '@/services/api'; export default function CropPdf() { @@ -40,7 +41,9 @@ export default function CropPdf() { const res = await api.post('/pdf-tools/crop', fd); setTaskId(res.data.task_id); } catch (err) { - setError(err instanceof Error ? err.message : 'Failed to crop PDF.'); + const msg = err instanceof Error ? err.message : t('common.errors.processingFailed'); + setError(msg); + toast.error(msg); setPhase('done'); } }; diff --git a/frontend/src/components/tools/ImageCrop.tsx b/frontend/src/components/tools/ImageCrop.tsx index f3c55d1..4a5a800 100644 --- a/frontend/src/components/tools/ImageCrop.tsx +++ b/frontend/src/components/tools/ImageCrop.tsx @@ -9,6 +9,7 @@ import AdSlot from '@/components/layout/AdSlot'; import { useTaskPolling } from '@/hooks/useTaskPolling'; import { generateToolSchema } from '@/utils/seo'; import { useFileStore } from '@/stores/fileStore'; +import { toast } from 'sonner'; import api, { type TaskResponse } from '@/services/api'; export default function ImageCrop() { @@ -40,7 +41,9 @@ export default function ImageCrop() { const res = await api.post('/image/crop', fd); setTaskId(res.data.task_id); } catch (err) { - setError(err instanceof Error ? err.message : 'Failed to crop image.'); + const msg = err instanceof Error ? err.message : t('common.errors.processingFailed'); + setError(msg); + toast.error(msg); setPhase('done'); } }; diff --git a/frontend/src/components/tools/ImageRotateFlip.tsx b/frontend/src/components/tools/ImageRotateFlip.tsx index 354a626..b323abb 100644 --- a/frontend/src/components/tools/ImageRotateFlip.tsx +++ b/frontend/src/components/tools/ImageRotateFlip.tsx @@ -9,6 +9,7 @@ import AdSlot from '@/components/layout/AdSlot'; import { useTaskPolling } from '@/hooks/useTaskPolling'; import { generateToolSchema } from '@/utils/seo'; import { useFileStore } from '@/stores/fileStore'; +import { toast } from 'sonner'; import api, { type TaskResponse } from '@/services/api'; export default function ImageRotateFlip() { @@ -41,7 +42,9 @@ export default function ImageRotateFlip() { const res = await api.post('/image/rotate-flip', fd); setTaskId(res.data.task_id); } catch (err) { - setError(err instanceof Error ? err.message : 'Failed to transform image.'); + const msg = err instanceof Error ? err.message : t('common.errors.processingFailed'); + setError(msg); + toast.error(msg); setPhase('done'); } }; diff --git a/frontend/src/components/tools/ImagesToPdf.tsx b/frontend/src/components/tools/ImagesToPdf.tsx index 5e85992..cd45889 100644 --- a/frontend/src/components/tools/ImagesToPdf.tsx +++ b/frontend/src/components/tools/ImagesToPdf.tsx @@ -6,6 +6,7 @@ import AdSlot from '@/components/layout/AdSlot'; import ProgressBar from '@/components/shared/ProgressBar'; import DownloadButton from '@/components/shared/DownloadButton'; import { useTaskPolling } from '@/hooks/useTaskPolling'; +import { toast } from 'sonner'; import { uploadFiles } from '@/services/api'; import { generateToolSchema } from '@/utils/seo'; import { useFileStore } from '@/stores/fileStore'; @@ -66,7 +67,9 @@ export default function ImagesToPdf() { setTaskId(data.task_id); setPhase('processing'); } catch (err) { - setError(err instanceof Error ? err.message : 'Upload failed.'); + const msg = err instanceof Error ? err.message : t('common.errors.uploadFailed'); + setError(msg); + toast.error(msg); } finally { setIsUploading(false); } diff --git a/frontend/src/components/tools/MergePdf.tsx b/frontend/src/components/tools/MergePdf.tsx index 4de930b..0692b11 100644 --- a/frontend/src/components/tools/MergePdf.tsx +++ b/frontend/src/components/tools/MergePdf.tsx @@ -7,6 +7,7 @@ import ProgressBar from '@/components/shared/ProgressBar'; import DownloadButton from '@/components/shared/DownloadButton'; import AdSlot from '@/components/layout/AdSlot'; import { useTaskPolling } from '@/hooks/useTaskPolling'; +import { toast } from 'sonner'; import { uploadFiles } from '@/services/api'; import { generateToolSchema } from '@/utils/seo'; import { useFileStore } from '@/stores/fileStore'; @@ -66,7 +67,9 @@ export default function MergePdf() { setTaskId(data.task_id); setPhase('processing'); } catch (err) { - setError(err instanceof Error ? err.message : 'Upload failed.'); + const msg = err instanceof Error ? err.message : t('common.errors.uploadFailed'); + setError(msg); + toast.error(msg); } finally { setIsUploading(false); } diff --git a/frontend/src/components/tools/PdfFlowchart.tsx b/frontend/src/components/tools/PdfFlowchart.tsx index 7d204ba..2b53299 100644 --- a/frontend/src/components/tools/PdfFlowchart.tsx +++ b/frontend/src/components/tools/PdfFlowchart.tsx @@ -4,6 +4,7 @@ import { Helmet } from 'react-helmet-async'; import { GitBranch } from 'lucide-react'; import AdSlot from '@/components/layout/AdSlot'; import { useTaskPolling } from '@/hooks/useTaskPolling'; +import { toast } from 'sonner'; import { startTask, uploadFile } from '@/services/api'; import { generateToolSchema } from '@/utils/seo'; import { useFileStore } from '@/stores/fileStore'; @@ -91,7 +92,9 @@ export default function PdfFlowchart() { const data = await uploadFile('/flowchart/extract', file); setTaskId(data.task_id); } catch (err) { - setError(err instanceof Error ? err.message : 'Upload failed.'); + const msg = err instanceof Error ? err.message : t('common.errors.uploadFailed'); + setError(msg); + toast.error(msg); setUploading(false); } }; @@ -104,7 +107,9 @@ export default function PdfFlowchart() { const data = await startTask('/flowchart/extract-sample'); setTaskId(data.task_id); } catch (err) { - setError(err instanceof Error ? err.message : 'Sample failed.'); + const msg = err instanceof Error ? err.message : t('common.errors.processingFailed'); + setError(msg); + toast.error(msg); setUploading(false); } }; diff --git a/frontend/src/components/tools/PdfMetadata.tsx b/frontend/src/components/tools/PdfMetadata.tsx index 3a64ce6..9cc3a88 100644 --- a/frontend/src/components/tools/PdfMetadata.tsx +++ b/frontend/src/components/tools/PdfMetadata.tsx @@ -9,6 +9,7 @@ import AdSlot from '@/components/layout/AdSlot'; import { useTaskPolling } from '@/hooks/useTaskPolling'; import { generateToolSchema } from '@/utils/seo'; import { useFileStore } from '@/stores/fileStore'; +import { toast } from 'sonner'; import api, { type TaskResponse } from '@/services/api'; export default function PdfMetadata() { @@ -37,7 +38,9 @@ export default function PdfMetadata() { const res = await api.post('/pdf-tools/metadata', fd); setTaskId(res.data.task_id); } catch (err) { - setError(err instanceof Error ? err.message : 'Failed to edit metadata.'); + const msg = err instanceof Error ? err.message : t('common.errors.processingFailed'); + setError(msg); + toast.error(msg); setPhase('done'); } }; diff --git a/frontend/src/components/tools/QrCodeGenerator.tsx b/frontend/src/components/tools/QrCodeGenerator.tsx index 976a142..74d8178 100644 --- a/frontend/src/components/tools/QrCodeGenerator.tsx +++ b/frontend/src/components/tools/QrCodeGenerator.tsx @@ -6,6 +6,7 @@ import ProgressBar from '@/components/shared/ProgressBar'; import AdSlot from '@/components/layout/AdSlot'; import { useTaskPolling } from '@/hooks/useTaskPolling'; import { generateToolSchema } from '@/utils/seo'; +import { toast } from 'sonner'; import api, { type TaskResponse, type TaskResult } from '@/services/api'; import { dispatchRatingPrompt } from '@/utils/ratingPrompt'; @@ -31,7 +32,9 @@ export default function QrCodeGenerator() { const res = await api.post('/qrcode/generate', { data: data.trim(), size }); setTaskId(res.data.task_id); } catch (err) { - setError(err instanceof Error ? err.message : 'Failed to generate QR code.'); + const msg = err instanceof Error ? err.message : t('common.errors.processingFailed'); + setError(msg); + toast.error(msg); setPhase('done'); } }; diff --git a/frontend/src/components/tools/SignPdf.tsx b/frontend/src/components/tools/SignPdf.tsx index 32cc1f3..80296f9 100644 --- a/frontend/src/components/tools/SignPdf.tsx +++ b/frontend/src/components/tools/SignPdf.tsx @@ -9,6 +9,7 @@ import AdSlot from '@/components/layout/AdSlot'; import { useTaskPolling } from '@/hooks/useTaskPolling'; import { generateToolSchema } from '@/utils/seo'; import { useFileStore } from '@/stores/fileStore'; +import { toast } from 'sonner'; import api, { type TaskResponse } from '@/services/api'; export default function SignPdf() { @@ -40,7 +41,9 @@ export default function SignPdf() { const res = await api.post('/pdf-tools/sign', fd); setTaskId(res.data.task_id); } catch (err) { - setError(err instanceof Error ? err.message : 'Failed to sign PDF.'); + const msg = err instanceof Error ? err.message : t('common.errors.processingFailed'); + setError(msg); + toast.error(msg); setPhase('done'); } }; diff --git a/frontend/src/pages/InternalAdminPage.tsx b/frontend/src/pages/InternalAdminPage.tsx index 415d6de..9a68681 100644 --- a/frontend/src/pages/InternalAdminPage.tsx +++ b/frontend/src/pages/InternalAdminPage.tsx @@ -1,4 +1,5 @@ import { useEffect, useMemo, useState, type FormEvent } from 'react'; +import { toast } from 'sonner'; import { Helmet } from 'react-helmet-async'; import { Link } from 'react-router-dom'; import { @@ -182,6 +183,12 @@ const TRANSLATIONS: Record> = { btnPro: 'Pro', btnUser: 'User', btnAdmin: 'Admin', + // Errors + loadError: 'Failed to load dashboard data.', + loginError: 'Unable to sign in.', + updatePlanError: 'Unable to update plan.', + updateRoleError: 'Unable to update role.', + updateContactError: 'Unable to update contact message.', }, ar: { // Page & header @@ -317,6 +324,12 @@ const TRANSLATIONS: Record> = { btnPro: 'Pro', btnUser: 'مستخدم', btnAdmin: 'مشرف', + // Errors + loadError: 'فشل تحميل بيانات لوحة التحكم.', + loginError: 'تعذّر تسجيل الدخول.', + updatePlanError: 'تعذّر تحديث الخطة.', + updateRoleError: 'تعذّر تحديث الدور.', + updateContactError: 'تعذّر تحديث رسالة التواصل.', }, }; @@ -475,7 +488,9 @@ export default function InternalAdminPage() { } } } catch (e) { - setError(e instanceof Error ? e.message : 'Failed to load dashboard data.'); + const msg = e instanceof Error ? e.message : t('loadError'); + setError(msg); + toast.error(msg); } finally { setLoading(false); } @@ -492,7 +507,9 @@ export default function InternalAdminPage() { } setPassword(''); } catch (e) { - setLoginError(e instanceof Error ? e.message : 'Unable to sign in.'); + const msg = e instanceof Error ? e.message : t('loginError'); + setLoginError(msg); + toast.error(msg); } } @@ -504,7 +521,9 @@ export default function InternalAdminPage() { await updateInternalAdminUserPlan(userId, plan); await loadTab('users'); } catch (e) { - setError(e instanceof Error ? e.message : 'Unable to update plan.'); + const msg = e instanceof Error ? e.message : t('updatePlanError'); + setError(msg); + toast.error(msg); } finally { setUpdatingUserId(null); } @@ -518,7 +537,9 @@ export default function InternalAdminPage() { await updateInternalAdminUserRole(userId, role); await loadTab('users'); } catch (e) { - setError(e instanceof Error ? e.message : 'Unable to update role.'); + const msg = e instanceof Error ? e.message : t('updateRoleError'); + setError(msg); + toast.error(msg); } finally { setUpdatingRoleUserId(null); } @@ -532,7 +553,9 @@ export default function InternalAdminPage() { await markInternalAdminContactRead(messageId); await loadTab('contacts'); } catch (e) { - setError(e instanceof Error ? e.message : 'Unable to update contact message.'); + const msg = e instanceof Error ? e.message : t('updateContactError'); + setError(msg); + toast.error(msg); } finally { setMarkingMessageId(null); }