Restyle toasts and centralize friendly error handling.
Replace the default react-toastify look with project-styled toast cards (icon chip, rounded shell, RTL-aware container, type-colored progress bar) via a new notify helper and globals.css overrides. Add a central error layer (src/lib/errors.ts): classify any caught error by HTTP status / network condition, log the full technical detail (including the raw backend message) to the console only, and surface a friendly, localized message to the user. Raw backend messages are no longer shown. All ~190 toast call sites across 22 files move to notify, routing backend errors through notify.error(err, fallback); dead apiErrorMessage/formatApiError helpers removed. Adds an `errors` section to the fa/en dictionaries. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import api from '@/lib/api';
|
||||
import { Loader2, Upload, Hammer, Rocket, CheckCircle, XCircle, X, Minimize2 } from 'lucide-react';
|
||||
import { toast } from 'react-toastify';
|
||||
import { notify } from '@/lib/notify';
|
||||
import { useT } from '@/i18n/I18nProvider';
|
||||
import type { Dictionary } from '@/i18n/dictionaries/fa';
|
||||
|
||||
@@ -54,13 +54,13 @@ export function BuildProgressModal({
|
||||
const cancelMutation = useMutation({
|
||||
mutationFn: () => api.post(`/deployments/applications/${appId}/cancel`),
|
||||
onSuccess: () => {
|
||||
toast.success(c.deploymentCancelled);
|
||||
notify.success(c.deploymentCancelled);
|
||||
queryClient.invalidateQueries({ queryKey: ['application', appId] });
|
||||
queryClient.invalidateQueries({ queryKey: ['deployments', appId] });
|
||||
queryClient.invalidateQueries({ queryKey: ['build-progress', appId] });
|
||||
queryClient.invalidateQueries({ queryKey: ['applications'] });
|
||||
},
|
||||
onError: () => toast.error(c.cancelDeploymentFailed),
|
||||
onError: () => notify.error(c.cancelDeploymentFailed),
|
||||
});
|
||||
|
||||
const cfg = phaseConfig[progress.phase];
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import api from '@/lib/api';
|
||||
import { toast } from 'react-toastify';
|
||||
import { notify } from '@/lib/notify';
|
||||
import { useT, useLocale } from '@/i18n/I18nProvider';
|
||||
import type { AppSnapshot } from '@/types';
|
||||
import { formatBytes } from '@/lib/format-utils';
|
||||
@@ -87,10 +87,10 @@ export function DatabaseSnapshotsPanel({
|
||||
snap.status === 'completed' &&
|
||||
snap.dbDumpPath
|
||||
) {
|
||||
toast.success(sn.backupReady);
|
||||
notify.success(sn.backupReady);
|
||||
}
|
||||
if (prevInProgressRef.current.has(snap.id) && snap.status === 'failed') {
|
||||
toast.error(snap.errorMessage || sn.backupFailed);
|
||||
notify.error(sn.backupFailed);
|
||||
}
|
||||
}
|
||||
prevInProgressRef.current = inProgressIds;
|
||||
@@ -104,13 +104,10 @@ export function DatabaseSnapshotsPanel({
|
||||
onSuccess: () => {
|
||||
setShowPanel(true);
|
||||
queryClient.invalidateQueries({ queryKey: ['snapshots', serviceId] });
|
||||
toast.info(sn.backupStarted);
|
||||
notify.info(sn.backupStarted);
|
||||
},
|
||||
onError: (err: unknown) => {
|
||||
const msg = (err as { response?: { data?: { message?: string | string[] } } })?.response?.data
|
||||
?.message;
|
||||
const text = Array.isArray(msg) ? msg.join(', ') : msg;
|
||||
toast.error(text || sn.createFailed);
|
||||
notify.error(err, sn.createFailed);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -125,9 +122,9 @@ export function DatabaseSnapshotsPanel({
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['snapshots', serviceId] });
|
||||
toast.success(sn.backupDeleted);
|
||||
notify.success(sn.backupDeleted);
|
||||
},
|
||||
onError: () => toast.error(sn.deleteFailed),
|
||||
onError: () => notify.error(sn.deleteFailed),
|
||||
});
|
||||
|
||||
const downloadSnapshotDb = (snapshotId: string) => {
|
||||
@@ -144,9 +141,9 @@ export function DatabaseSnapshotsPanel({
|
||||
link.download = `database-backup-${snapshotId.slice(0, 8)}.sql`;
|
||||
link.click();
|
||||
URL.revokeObjectURL(link.href);
|
||||
toast.success(sn.downloadStarted);
|
||||
notify.success(sn.downloadStarted);
|
||||
})
|
||||
.catch(() => toast.error(sn.downloadFailed));
|
||||
.catch(() => notify.error(sn.downloadFailed));
|
||||
};
|
||||
|
||||
const handleDelete = async (snap: AppSnapshot) => {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { useRef, useState } from 'react';
|
||||
import { Database, Eye, EyeOff, RefreshCw, AlertCircle } from 'lucide-react';
|
||||
import { toast } from 'react-toastify';
|
||||
import { notify } from '@/lib/notify';
|
||||
import { useT } from '@/i18n/I18nProvider';
|
||||
import { Select } from '@/components/ui/select';
|
||||
import { ONE_GIB, minGiToFitFileBytes } from '@/lib/storage-utils';
|
||||
@@ -57,11 +57,11 @@ export function ManagedDatabaseConfig({
|
||||
|
||||
const acceptDump = (f: File) => {
|
||||
if (!f.name.endsWith('.sql') && !f.name.endsWith('.gz') && !f.name.endsWith('.dump')) {
|
||||
toast.error(c.allowedFormats);
|
||||
notify.error(c.allowedFormats);
|
||||
return;
|
||||
}
|
||||
if (f.size > 500 * 1024 * 1024) {
|
||||
toast.error(c.maxSize);
|
||||
notify.error(c.maxSize);
|
||||
return;
|
||||
}
|
||||
onDbDumpFileChange(f);
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { useState, useEffect, useRef, useCallback } from 'react';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import api from '@/lib/api';
|
||||
import { toast } from 'react-toastify';
|
||||
import { notify } from '@/lib/notify';
|
||||
import { useT } from '@/i18n/I18nProvider';
|
||||
import { useLocalizedRouter } from '@/i18n/navigation';
|
||||
import type { Application, Invoice, OptionalServiceResourcesMap, ResourceUsage } from '@/types';
|
||||
@@ -167,11 +167,10 @@ export function ManagedServiceResourcesPanel({
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['application', serviceId] });
|
||||
queryClient.invalidateQueries({ queryKey: ['resources', serviceId] });
|
||||
toast.success(sr.resourcesUpdated);
|
||||
notify.success(sr.resourcesUpdated);
|
||||
},
|
||||
onError: (err: unknown) => {
|
||||
const msg = (err as { response?: { data?: { message?: string } } })?.response?.data?.message;
|
||||
toast.error(msg || sr.updateFailed);
|
||||
notify.error(err, sr.updateFailed);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -188,14 +187,13 @@ export function ManagedServiceResourcesPanel({
|
||||
setPendingUpgradePayload(null);
|
||||
const paidAmount = res.data.paidAmount || 0;
|
||||
if (paidAmount > 0) {
|
||||
toast.success(sr.upgradedPaid.replace('{amount}', paidAmount.toLocaleString('en-US')));
|
||||
notify.success(sr.upgradedPaid.replace('{amount}', paidAmount.toLocaleString('en-US')));
|
||||
} else {
|
||||
toast.success(sr.updatedSuccess);
|
||||
notify.success(sr.updatedSuccess);
|
||||
}
|
||||
},
|
||||
onError: (err: unknown) => {
|
||||
const msg = (err as { response?: { data?: { message?: string } } })?.response?.data?.message;
|
||||
toast.error(msg || sr.upgradeFailed);
|
||||
notify.error(err, sr.upgradeFailed);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -207,8 +205,7 @@ export function ManagedServiceResourcesPanel({
|
||||
setShowUpgradeConfirm(true);
|
||||
},
|
||||
onError: (err: unknown) => {
|
||||
const msg = (err as { response?: { data?: { message?: string } } })?.response?.data?.message;
|
||||
toast.error(msg || sr.calcFailed);
|
||||
notify.error(err, sr.calcFailed);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -216,7 +213,7 @@ export function ManagedServiceResourcesPanel({
|
||||
mutationFn: (data: UpgradePayload) =>
|
||||
api.post<Invoice>(`/billing/applications/${serviceId}/upgrade/invoice`, data).then((r) => r.data),
|
||||
onSuccess: (invoice) => {
|
||||
toast.success(sr.invoiceCreated);
|
||||
notify.success(sr.invoiceCreated);
|
||||
queryClient.invalidateQueries({ queryKey: ['invoices'] });
|
||||
setShowUpgradeConfirm(false);
|
||||
setUpgradeCostData(null);
|
||||
@@ -224,8 +221,7 @@ export function ManagedServiceResourcesPanel({
|
||||
router.push(`/dashboard/invoices?invoice=${invoice.id}`);
|
||||
},
|
||||
onError: (err: unknown) => {
|
||||
const msg = (err as { response?: { data?: { message?: string } } })?.response?.data?.message;
|
||||
toast.error(msg || sr.invoiceFailed);
|
||||
notify.error(err, sr.invoiceFailed);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -240,12 +236,11 @@ export function ManagedServiceResourcesPanel({
|
||||
onSuccess: (res) => {
|
||||
const data = res.data;
|
||||
setDbRestoreLogs(data.logs || null);
|
||||
if (data.success) toast.success(sr.restoredSuccess);
|
||||
else toast.error(data.message || sr.restoreFailed);
|
||||
if (data.success) notify.success(sr.restoredSuccess);
|
||||
else notify.error(sr.restoreFailed);
|
||||
},
|
||||
onError: (err: unknown) => {
|
||||
const msg = (err as { response?: { data?: { message?: string } } })?.response?.data?.message;
|
||||
toast.error(msg || sr.uploadFailed);
|
||||
notify.error(err, sr.uploadFailed);
|
||||
setDbRestoreLogs(null);
|
||||
},
|
||||
});
|
||||
@@ -253,11 +248,11 @@ export function ManagedServiceResourcesPanel({
|
||||
const handleDbFileUpload = useCallback(
|
||||
(file: File) => {
|
||||
if (!file.name.endsWith('.sql') && !file.name.endsWith('.gz') && !file.name.endsWith('.dump')) {
|
||||
toast.error(sr.invalidFile);
|
||||
notify.error(sr.invalidFile);
|
||||
return;
|
||||
}
|
||||
if (file.size > 500 * 1024 * 1024) {
|
||||
toast.error(sr.fileTooLarge);
|
||||
notify.error(sr.fileTooLarge);
|
||||
return;
|
||||
}
|
||||
setDbRestoreLogs(null);
|
||||
@@ -300,7 +295,7 @@ export function ManagedServiceResourcesPanel({
|
||||
|
||||
const applyResources = () => {
|
||||
if (needsRenewal) {
|
||||
toast.warn(sr.renewFirst);
|
||||
notify.warning(sr.renewFirst);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@ import { ToastContainer } from 'react-toastify';
|
||||
import 'react-toastify/dist/ReactToastify.css';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useAuthStore } from '@/lib/store';
|
||||
import { useLocale } from '@/i18n/I18nProvider';
|
||||
import { dirFor } from '@/i18n/config';
|
||||
import { ModalProvider } from './confirm-modal';
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
@@ -18,6 +20,8 @@ const queryClient = new QueryClient({
|
||||
|
||||
export function Providers({ children }: { children: React.ReactNode }) {
|
||||
const loadUser = useAuthStore((s) => s.loadUser);
|
||||
const locale = useLocale();
|
||||
const isRtl = dirFor(locale) === 'rtl';
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -32,7 +36,19 @@ export function Providers({ children }: { children: React.ReactNode }) {
|
||||
<ModalProvider>
|
||||
{children}
|
||||
</ModalProvider>
|
||||
<ToastContainer position="top-right" autoClose={3000} hideProgressBar={false} closeOnClick pauseOnHover theme="light" />
|
||||
<ToastContainer
|
||||
position={isRtl ? 'top-left' : 'top-right'}
|
||||
rtl={isRtl}
|
||||
autoClose={4000}
|
||||
newestOnTop
|
||||
closeOnClick
|
||||
pauseOnHover
|
||||
draggable
|
||||
icon={false}
|
||||
theme="light"
|
||||
toastClassName="abrban-toast"
|
||||
className="abrban-toast-container"
|
||||
/>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import api from '@/lib/api';
|
||||
import { toast } from 'react-toastify';
|
||||
import { notify } from '@/lib/notify';
|
||||
import { useT } from '@/i18n/I18nProvider';
|
||||
import { Select } from '@/components/ui/select';
|
||||
import type { Application, ServiceAccessGrant, ServiceAccessTarget } from '@/types';
|
||||
@@ -70,11 +70,10 @@ export function ServiceExternalAccessPanel({
|
||||
onSuccess: () => {
|
||||
refetchAccessGrants();
|
||||
queryClient.invalidateQueries({ queryKey: ['access-grants', appId] });
|
||||
toast.success(accessPersistent ? ea.permanentEnabled : ea.temporaryEnabled);
|
||||
notify.success(accessPersistent ? ea.permanentEnabled : ea.temporaryEnabled);
|
||||
},
|
||||
onError: (err: unknown) => {
|
||||
const msg = (err as { response?: { data?: { message?: string } } })?.response?.data?.message;
|
||||
toast.error(msg || ea.enableFailed);
|
||||
notify.error(err, ea.enableFailed);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -83,9 +82,9 @@ export function ServiceExternalAccessPanel({
|
||||
onSuccess: () => {
|
||||
refetchAccessGrants();
|
||||
queryClient.invalidateQueries({ queryKey: ['access-grants', appId] });
|
||||
toast.success(ea.revoked);
|
||||
notify.success(ea.revoked);
|
||||
},
|
||||
onError: () => toast.error(ea.revokeFailed),
|
||||
onError: () => notify.error(ea.revokeFailed),
|
||||
});
|
||||
|
||||
const copyToClipboard = (text: string, field: string) => {
|
||||
|
||||
Reference in New Issue
Block a user