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:
keyhan
2026-06-13 11:45:26 +03:30
parent 97cd5e989a
commit 91a66d5645
29 changed files with 501 additions and 250 deletions
@@ -3,7 +3,7 @@
import { useState, useEffect } 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 type {
PricingCatalog,
@@ -39,15 +39,6 @@ function toPricingCatalogPatch(catalog: PricingCatalog) {
};
}
function formatApiError(err: unknown, fallback: string): string {
if (!err || typeof err !== 'object' || !('response' in err)) return fallback;
const message = (err as { response?: { data?: { message?: string | string[] } } }).response
?.data?.message;
if (Array.isArray(message)) return message.join(', ');
if (typeof message === 'string') return message;
return fallback;
}
function cloneCatalog(catalog: PricingCatalog): PricingCatalog {
const runtimes: PricingCatalog['runtimes'] = {};
for (const key of Object.keys(catalog.runtimes)) {
@@ -377,12 +368,12 @@ export default function AdminBillingPage() {
queryClient.invalidateQueries({ queryKey: ['pricing-catalog'] });
queryClient.invalidateQueries({ queryKey: ['custom-domain-price'] });
queryClient.invalidateQueries({ queryKey: ['optional-services-pricing'] });
toast.success(b.saved);
notify.success(b.saved);
setEditing(false);
setDraft(null);
},
onError: (err: unknown) => {
toast.error(formatApiError(err, b.saveFailed));
notify.error(err, b.saveFailed);
},
});
@@ -743,11 +734,11 @@ function LifecycleSettingsSection() {
mutationFn: (body: Record<string, number>) => api.patch('/lifecycle/settings', body),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['lifecycle-settings'] });
toast.success(b.lifecycleSaved);
notify.success(b.lifecycleSaved);
setEditing(false);
},
onError: (err: unknown) => {
toast.error(formatApiError(err, b.saveFailedShort));
notify.error(err, b.saveFailedShort);
},
});