feat: add error handling with toast notifications across various tools and admin page

This commit is contained in:
Your Name
2026-03-22 17:24:33 +02:00
parent ce610f5c6e
commit 2f60043ed8
12 changed files with 73 additions and 16 deletions

View File

@@ -2,6 +2,7 @@ import { useEffect, useRef, useState } from 'react';
import { useLocation } from 'react-router-dom'; import { useLocation } from 'react-router-dom';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Bot, SendHorizontal, Sparkles, X } from 'lucide-react'; import { Bot, SendHorizontal, Sparkles, X } from 'lucide-react';
import { toast } from 'sonner';
import { getToolSEO } from '@/config/seoData'; import { getToolSEO } from '@/config/seoData';
import { streamAssistantChat, type AssistantHistoryMessage } from '@/services/api'; import { streamAssistantChat, type AssistantHistoryMessage } from '@/services/api';
import { trackEvent } from '@/services/analytics'; import { trackEvent } from '@/services/analytics';
@@ -181,6 +182,7 @@ export default function SiteAssistant() {
? requestError.message ? requestError.message
: t('assistant.unavailable'); : t('assistant.unavailable');
setError(message); setError(message);
toast.error(message);
setMessages((currentMessages) => currentMessages.map((currentMessage) => ( setMessages((currentMessages) => currentMessages.map((currentMessage) => (
currentMessage.id === assistantMessageId currentMessage.id === assistantMessageId

View File

@@ -6,6 +6,7 @@ import ProgressBar from '@/components/shared/ProgressBar';
import AdSlot from '@/components/layout/AdSlot'; import AdSlot from '@/components/layout/AdSlot';
import { useTaskPolling } from '@/hooks/useTaskPolling'; import { useTaskPolling } from '@/hooks/useTaskPolling';
import { generateToolSchema } from '@/utils/seo'; import { generateToolSchema } from '@/utils/seo';
import { toast } from 'sonner';
import api, { type TaskResponse } from '@/services/api'; import api, { type TaskResponse } from '@/services/api';
const BARCODE_TYPES = ['code128', 'code39', 'ean13', 'ean8', 'upca', 'isbn13', 'isbn10', 'issn', 'pzn'] as const; 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); setTaskId(res.data.task_id);
} catch (err) { } 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'); setPhase('done');
} }
}; };

View File

@@ -9,6 +9,7 @@ import AdSlot from '@/components/layout/AdSlot';
import { useTaskPolling } from '@/hooks/useTaskPolling'; import { useTaskPolling } from '@/hooks/useTaskPolling';
import { generateToolSchema } from '@/utils/seo'; import { generateToolSchema } from '@/utils/seo';
import { useFileStore } from '@/stores/fileStore'; import { useFileStore } from '@/stores/fileStore';
import { toast } from 'sonner';
import api, { type TaskResponse } from '@/services/api'; import api, { type TaskResponse } from '@/services/api';
export default function CropPdf() { export default function CropPdf() {
@@ -40,7 +41,9 @@ export default function CropPdf() {
const res = await api.post<TaskResponse>('/pdf-tools/crop', fd); const res = await api.post<TaskResponse>('/pdf-tools/crop', fd);
setTaskId(res.data.task_id); setTaskId(res.data.task_id);
} catch (err) { } 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'); setPhase('done');
} }
}; };

View File

@@ -9,6 +9,7 @@ import AdSlot from '@/components/layout/AdSlot';
import { useTaskPolling } from '@/hooks/useTaskPolling'; import { useTaskPolling } from '@/hooks/useTaskPolling';
import { generateToolSchema } from '@/utils/seo'; import { generateToolSchema } from '@/utils/seo';
import { useFileStore } from '@/stores/fileStore'; import { useFileStore } from '@/stores/fileStore';
import { toast } from 'sonner';
import api, { type TaskResponse } from '@/services/api'; import api, { type TaskResponse } from '@/services/api';
export default function ImageCrop() { export default function ImageCrop() {
@@ -40,7 +41,9 @@ export default function ImageCrop() {
const res = await api.post<TaskResponse>('/image/crop', fd); const res = await api.post<TaskResponse>('/image/crop', fd);
setTaskId(res.data.task_id); setTaskId(res.data.task_id);
} catch (err) { } 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'); setPhase('done');
} }
}; };

View File

@@ -9,6 +9,7 @@ import AdSlot from '@/components/layout/AdSlot';
import { useTaskPolling } from '@/hooks/useTaskPolling'; import { useTaskPolling } from '@/hooks/useTaskPolling';
import { generateToolSchema } from '@/utils/seo'; import { generateToolSchema } from '@/utils/seo';
import { useFileStore } from '@/stores/fileStore'; import { useFileStore } from '@/stores/fileStore';
import { toast } from 'sonner';
import api, { type TaskResponse } from '@/services/api'; import api, { type TaskResponse } from '@/services/api';
export default function ImageRotateFlip() { export default function ImageRotateFlip() {
@@ -41,7 +42,9 @@ export default function ImageRotateFlip() {
const res = await api.post<TaskResponse>('/image/rotate-flip', fd); const res = await api.post<TaskResponse>('/image/rotate-flip', fd);
setTaskId(res.data.task_id); setTaskId(res.data.task_id);
} catch (err) { } 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'); setPhase('done');
} }
}; };

View File

@@ -6,6 +6,7 @@ import AdSlot from '@/components/layout/AdSlot';
import ProgressBar from '@/components/shared/ProgressBar'; import ProgressBar from '@/components/shared/ProgressBar';
import DownloadButton from '@/components/shared/DownloadButton'; import DownloadButton from '@/components/shared/DownloadButton';
import { useTaskPolling } from '@/hooks/useTaskPolling'; import { useTaskPolling } from '@/hooks/useTaskPolling';
import { toast } from 'sonner';
import { uploadFiles } from '@/services/api'; import { uploadFiles } from '@/services/api';
import { generateToolSchema } from '@/utils/seo'; import { generateToolSchema } from '@/utils/seo';
import { useFileStore } from '@/stores/fileStore'; import { useFileStore } from '@/stores/fileStore';
@@ -66,7 +67,9 @@ export default function ImagesToPdf() {
setTaskId(data.task_id); setTaskId(data.task_id);
setPhase('processing'); setPhase('processing');
} catch (err) { } 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 { } finally {
setIsUploading(false); setIsUploading(false);
} }

View File

@@ -7,6 +7,7 @@ import ProgressBar from '@/components/shared/ProgressBar';
import DownloadButton from '@/components/shared/DownloadButton'; import DownloadButton from '@/components/shared/DownloadButton';
import AdSlot from '@/components/layout/AdSlot'; import AdSlot from '@/components/layout/AdSlot';
import { useTaskPolling } from '@/hooks/useTaskPolling'; import { useTaskPolling } from '@/hooks/useTaskPolling';
import { toast } from 'sonner';
import { uploadFiles } from '@/services/api'; import { uploadFiles } from '@/services/api';
import { generateToolSchema } from '@/utils/seo'; import { generateToolSchema } from '@/utils/seo';
import { useFileStore } from '@/stores/fileStore'; import { useFileStore } from '@/stores/fileStore';
@@ -66,7 +67,9 @@ export default function MergePdf() {
setTaskId(data.task_id); setTaskId(data.task_id);
setPhase('processing'); setPhase('processing');
} catch (err) { } 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 { } finally {
setIsUploading(false); setIsUploading(false);
} }

View File

@@ -4,6 +4,7 @@ import { Helmet } from 'react-helmet-async';
import { GitBranch } from 'lucide-react'; import { GitBranch } from 'lucide-react';
import AdSlot from '@/components/layout/AdSlot'; import AdSlot from '@/components/layout/AdSlot';
import { useTaskPolling } from '@/hooks/useTaskPolling'; import { useTaskPolling } from '@/hooks/useTaskPolling';
import { toast } from 'sonner';
import { startTask, uploadFile } from '@/services/api'; import { startTask, uploadFile } from '@/services/api';
import { generateToolSchema } from '@/utils/seo'; import { generateToolSchema } from '@/utils/seo';
import { useFileStore } from '@/stores/fileStore'; import { useFileStore } from '@/stores/fileStore';
@@ -91,7 +92,9 @@ export default function PdfFlowchart() {
const data = await uploadFile('/flowchart/extract', file); const data = await uploadFile('/flowchart/extract', file);
setTaskId(data.task_id); setTaskId(data.task_id);
} catch (err) { } 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); setUploading(false);
} }
}; };
@@ -104,7 +107,9 @@ export default function PdfFlowchart() {
const data = await startTask('/flowchart/extract-sample'); const data = await startTask('/flowchart/extract-sample');
setTaskId(data.task_id); setTaskId(data.task_id);
} catch (err) { } 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); setUploading(false);
} }
}; };

View File

@@ -9,6 +9,7 @@ import AdSlot from '@/components/layout/AdSlot';
import { useTaskPolling } from '@/hooks/useTaskPolling'; import { useTaskPolling } from '@/hooks/useTaskPolling';
import { generateToolSchema } from '@/utils/seo'; import { generateToolSchema } from '@/utils/seo';
import { useFileStore } from '@/stores/fileStore'; import { useFileStore } from '@/stores/fileStore';
import { toast } from 'sonner';
import api, { type TaskResponse } from '@/services/api'; import api, { type TaskResponse } from '@/services/api';
export default function PdfMetadata() { export default function PdfMetadata() {
@@ -37,7 +38,9 @@ export default function PdfMetadata() {
const res = await api.post<TaskResponse>('/pdf-tools/metadata', fd); const res = await api.post<TaskResponse>('/pdf-tools/metadata', fd);
setTaskId(res.data.task_id); setTaskId(res.data.task_id);
} catch (err) { } 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'); setPhase('done');
} }
}; };

View File

@@ -6,6 +6,7 @@ import ProgressBar from '@/components/shared/ProgressBar';
import AdSlot from '@/components/layout/AdSlot'; import AdSlot from '@/components/layout/AdSlot';
import { useTaskPolling } from '@/hooks/useTaskPolling'; import { useTaskPolling } from '@/hooks/useTaskPolling';
import { generateToolSchema } from '@/utils/seo'; import { generateToolSchema } from '@/utils/seo';
import { toast } from 'sonner';
import api, { type TaskResponse, type TaskResult } from '@/services/api'; import api, { type TaskResponse, type TaskResult } from '@/services/api';
import { dispatchRatingPrompt } from '@/utils/ratingPrompt'; import { dispatchRatingPrompt } from '@/utils/ratingPrompt';
@@ -31,7 +32,9 @@ export default function QrCodeGenerator() {
const res = await api.post<TaskResponse>('/qrcode/generate', { data: data.trim(), size }); const res = await api.post<TaskResponse>('/qrcode/generate', { data: data.trim(), size });
setTaskId(res.data.task_id); setTaskId(res.data.task_id);
} catch (err) { } 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'); setPhase('done');
} }
}; };

View File

@@ -9,6 +9,7 @@ import AdSlot from '@/components/layout/AdSlot';
import { useTaskPolling } from '@/hooks/useTaskPolling'; import { useTaskPolling } from '@/hooks/useTaskPolling';
import { generateToolSchema } from '@/utils/seo'; import { generateToolSchema } from '@/utils/seo';
import { useFileStore } from '@/stores/fileStore'; import { useFileStore } from '@/stores/fileStore';
import { toast } from 'sonner';
import api, { type TaskResponse } from '@/services/api'; import api, { type TaskResponse } from '@/services/api';
export default function SignPdf() { export default function SignPdf() {
@@ -40,7 +41,9 @@ export default function SignPdf() {
const res = await api.post<TaskResponse>('/pdf-tools/sign', fd); const res = await api.post<TaskResponse>('/pdf-tools/sign', fd);
setTaskId(res.data.task_id); setTaskId(res.data.task_id);
} catch (err) { } 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'); setPhase('done');
} }
}; };

View File

@@ -1,4 +1,5 @@
import { useEffect, useMemo, useState, type FormEvent } from 'react'; import { useEffect, useMemo, useState, type FormEvent } from 'react';
import { toast } from 'sonner';
import { Helmet } from 'react-helmet-async'; import { Helmet } from 'react-helmet-async';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { import {
@@ -182,6 +183,12 @@ const TRANSLATIONS: Record<Lang, Record<string, string>> = {
btnPro: 'Pro', btnPro: 'Pro',
btnUser: 'User', btnUser: 'User',
btnAdmin: 'Admin', 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: { ar: {
// Page & header // Page & header
@@ -317,6 +324,12 @@ const TRANSLATIONS: Record<Lang, Record<string, string>> = {
btnPro: 'Pro', btnPro: 'Pro',
btnUser: 'مستخدم', btnUser: 'مستخدم',
btnAdmin: 'مشرف', btnAdmin: 'مشرف',
// Errors
loadError: 'فشل تحميل بيانات لوحة التحكم.',
loginError: 'تعذّر تسجيل الدخول.',
updatePlanError: 'تعذّر تحديث الخطة.',
updateRoleError: 'تعذّر تحديث الدور.',
updateContactError: 'تعذّر تحديث رسالة التواصل.',
}, },
}; };
@@ -475,7 +488,9 @@ export default function InternalAdminPage() {
} }
} }
} catch (e) { } 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 { } finally {
setLoading(false); setLoading(false);
} }
@@ -492,7 +507,9 @@ export default function InternalAdminPage() {
} }
setPassword(''); setPassword('');
} catch (e) { } 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 updateInternalAdminUserPlan(userId, plan);
await loadTab('users'); await loadTab('users');
} catch (e) { } 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 { } finally {
setUpdatingUserId(null); setUpdatingUserId(null);
} }
@@ -518,7 +537,9 @@ export default function InternalAdminPage() {
await updateInternalAdminUserRole(userId, role); await updateInternalAdminUserRole(userId, role);
await loadTab('users'); await loadTab('users');
} catch (e) { } 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 { } finally {
setUpdatingRoleUserId(null); setUpdatingRoleUserId(null);
} }
@@ -532,7 +553,9 @@ export default function InternalAdminPage() {
await markInternalAdminContactRead(messageId); await markInternalAdminContactRead(messageId);
await loadTab('contacts'); await loadTab('contacts');
} catch (e) { } 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 { } finally {
setMarkingMessageId(null); setMarkingMessageId(null);
} }