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 { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { useParams } from 'next/navigation';
|
||||
import api from '@/lib/api';
|
||||
import { toast } from 'react-toastify';
|
||||
import { notify } from '@/lib/notify';
|
||||
import type { Application, Deployment, ResourceUsage, ClusterPublic, ClusterPoolPublic, AppSnapshot, K8sRevisionData, K8sRevision, OptionalServiceCredentials, Invoice } from '@/types';
|
||||
import { useState, useRef, useCallback, useEffect } from 'react';
|
||||
import { Link as NextLink } from '@/i18n/Link';
|
||||
@@ -264,13 +264,13 @@ export default function AppDetailPage() {
|
||||
const renewMutation = useMutation({
|
||||
mutationFn: (cycle: string) => api.post(`/billing/applications/${appId}/renew`, { cycle }),
|
||||
onSuccess: (res) => {
|
||||
toast.success(res.data.message || 'Application renewed successfully!');
|
||||
notify.success(res.data.message || 'Application renewed successfully!');
|
||||
queryClient.invalidateQueries({ queryKey: ['application', appId] });
|
||||
queryClient.invalidateQueries({ queryKey: ['wallet'] });
|
||||
setShowRenewalModal(false);
|
||||
},
|
||||
onError: (err: any) => {
|
||||
toast.error(err.response?.data?.message || 'Failed to renew application');
|
||||
notify.error(err, 'Failed to renew application');
|
||||
},
|
||||
});
|
||||
|
||||
@@ -278,13 +278,13 @@ export default function AppDetailPage() {
|
||||
mutationFn: (cycle: string) =>
|
||||
api.post<Invoice>(`/billing/applications/${appId}/renew/invoice`, { cycle }).then((r) => r.data),
|
||||
onSuccess: (invoice) => {
|
||||
toast.success(ad.invoiceCreated);
|
||||
notify.success(ad.invoiceCreated);
|
||||
queryClient.invalidateQueries({ queryKey: ['invoices'] });
|
||||
setShowRenewalModal(false);
|
||||
router.push(`/dashboard/invoices?invoice=${invoice.id}`);
|
||||
},
|
||||
onError: (err: any) => {
|
||||
toast.error(err.response?.data?.message || 'Failed to create renewal invoice');
|
||||
notify.error(err, 'Failed to create renewal invoice');
|
||||
},
|
||||
});
|
||||
|
||||
@@ -314,36 +314,36 @@ export default function AppDetailPage() {
|
||||
const setDomainMutation = useMutation({
|
||||
mutationFn: (domain: string) => api.post(`/applications/${appId}/domain`, { domain }),
|
||||
onSuccess: () => {
|
||||
toast.success(ad.domainSet);
|
||||
notify.success(ad.domainSet);
|
||||
queryClient.invalidateQueries({ queryKey: ['application', appId] });
|
||||
refetchDomainInfo();
|
||||
setCustomDomainInput('');
|
||||
},
|
||||
onError: (err: any) => toast.error(err.response?.data?.message || 'Failed to set domain'),
|
||||
onError: (err: any) => notify.error(err, 'Failed to set domain'),
|
||||
});
|
||||
|
||||
const verifyDnsMutation = useMutation({
|
||||
mutationFn: () => api.post(`/applications/${appId}/domain/verify`),
|
||||
onSuccess: (res) => {
|
||||
if (res.data.verified) {
|
||||
toast.success(ad.domainVerified);
|
||||
notify.success(ad.domainVerified);
|
||||
} else {
|
||||
toast.warning(res.data.message || 'DNS is not ready yet. Please try again later.');
|
||||
notify.warning(res.data.message || 'DNS is not ready yet. Please try again later.');
|
||||
}
|
||||
queryClient.invalidateQueries({ queryKey: ['application', appId] });
|
||||
refetchDomainInfo();
|
||||
},
|
||||
onError: (err: any) => toast.error(err.response?.data?.message || 'DNS verification failed'),
|
||||
onError: (err: any) => notify.error(err, 'DNS verification failed'),
|
||||
});
|
||||
|
||||
const removeDomainMutation = useMutation({
|
||||
mutationFn: () => api.delete(`/applications/${appId}/domain`),
|
||||
onSuccess: () => {
|
||||
toast.success(ad.customDomainRemoved);
|
||||
notify.success(ad.customDomainRemoved);
|
||||
queryClient.invalidateQueries({ queryKey: ['application', appId] });
|
||||
refetchDomainInfo();
|
||||
},
|
||||
onError: (err: any) => toast.error(err.response?.data?.message || 'Failed to remove domain'),
|
||||
onError: (err: any) => notify.error(err, 'Failed to remove domain'),
|
||||
});
|
||||
|
||||
// ─── Snapshots ──────────────────────────────────────
|
||||
@@ -365,12 +365,12 @@ export default function AppDetailPage() {
|
||||
const revisionRollbackMutation = useMutation({
|
||||
mutationFn: (revision: number) => api.post(`/snapshots/applications/${appId}/revisions/${revision}/rollback`),
|
||||
onSuccess: (res) => {
|
||||
toast.success(res.data.message || 'Rollback completed');
|
||||
notify.success(res.data.message || 'Rollback completed');
|
||||
queryClient.invalidateQueries({ queryKey: ['revisions', appId] });
|
||||
queryClient.invalidateQueries({ queryKey: ['application', appId] });
|
||||
queryClient.invalidateQueries({ queryKey: ['deployments', appId] });
|
||||
},
|
||||
onError: (err: any) => toast.error(err.response?.data?.message || ad.rollbackFailed),
|
||||
onError: (err: any) => notify.error(err, ad.rollbackFailed),
|
||||
});
|
||||
|
||||
const handleRevisionRollback = async (rev: K8sRevision) => {
|
||||
@@ -389,20 +389,20 @@ export default function AppDetailPage() {
|
||||
mutationFn: () => api.post(`/snapshots/applications/${appId}`),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['snapshots', appId] });
|
||||
toast.success(ad.snapshotStarted);
|
||||
notify.success(ad.snapshotStarted);
|
||||
},
|
||||
onError: () => toast.error(ad.snapshotCreateFailed),
|
||||
onError: () => notify.error(ad.snapshotCreateFailed),
|
||||
});
|
||||
|
||||
const rollbackMutation = useMutation({
|
||||
mutationFn: (snapshotId: string) => api.post(`/snapshots/${snapshotId}/rollback`),
|
||||
onSuccess: (res) => {
|
||||
const details = res.data.details || [];
|
||||
toast.success(ad.rollbackCompleted + details.join('\n'));
|
||||
notify.success(ad.rollbackCompleted + details.join('\n'));
|
||||
queryClient.invalidateQueries({ queryKey: ['application', appId] });
|
||||
queryClient.invalidateQueries({ queryKey: ['deployments', appId] });
|
||||
},
|
||||
onError: () => toast.error(ad.rollbackFailed),
|
||||
onError: () => notify.error(ad.rollbackFailed),
|
||||
});
|
||||
|
||||
const [deletingSnapshotId, setDeletingSnapshotId] = useState<string | null>(null);
|
||||
@@ -416,9 +416,9 @@ export default function AppDetailPage() {
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['snapshots', appId] });
|
||||
toast.success(ad.snapshotDeleted);
|
||||
notify.success(ad.snapshotDeleted);
|
||||
},
|
||||
onError: () => toast.error(ad.snapshotDeleteFailed),
|
||||
onError: () => notify.error(ad.snapshotDeleteFailed),
|
||||
});
|
||||
|
||||
const handleRollback = async (snap: AppSnapshot) => {
|
||||
@@ -457,13 +457,13 @@ export default function AppDetailPage() {
|
||||
link.click();
|
||||
URL.revokeObjectURL(link.href);
|
||||
})
|
||||
.catch(() => toast.error(ad.downloadFailed.replace('{name}', artifact)));
|
||||
.catch(() => notify.error(ad.downloadFailed.replace('{name}', artifact)));
|
||||
};
|
||||
|
||||
const downloadCurrentArtifact = (artifact: 'source' | 'wp-content' | 'database') => {
|
||||
// Prevent duplicate downloads
|
||||
if (downloadingArtifact) {
|
||||
toast.warn(ad.downloadInProgress);
|
||||
notify.warning(ad.downloadInProgress);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -475,7 +475,7 @@ export default function AppDetailPage() {
|
||||
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
||||
|
||||
const artifactName = artifact === 'source' ? 'Source Code' : artifact === 'wp-content' ? 'wp-content' : 'Database';
|
||||
toast.info(ad.downloading.replace('{name}', artifactName));
|
||||
notify.info(ad.downloading.replace('{name}', artifactName));
|
||||
|
||||
fetch(url, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
@@ -493,14 +493,14 @@ export default function AppDetailPage() {
|
||||
link.download = `current-${artifact}${ext}`;
|
||||
link.click();
|
||||
URL.revokeObjectURL(link.href);
|
||||
toast.success(ad.downloadedSuccess.replace('{name}', artifactName));
|
||||
notify.success(ad.downloadedSuccess.replace('{name}', artifactName));
|
||||
})
|
||||
.catch((err) => {
|
||||
clearTimeout(timeoutId);
|
||||
if (err.name === 'AbortError') {
|
||||
toast.error(ad.downloadTimeout);
|
||||
notify.error(ad.downloadTimeout);
|
||||
} else {
|
||||
toast.error(ad.downloadCurrentFailed.replace('{name}', artifact));
|
||||
notify.error(ad.downloadCurrentFailed.replace('{name}', artifact));
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
@@ -562,11 +562,11 @@ export default function AppDetailPage() {
|
||||
},
|
||||
onSuccess: () => {
|
||||
invalidateAll();
|
||||
toast.success(ad.deploymentTriggered);
|
||||
notify.success(ad.deploymentTriggered);
|
||||
},
|
||||
onError: () => {
|
||||
useDeployProgressStore.getState().stopTracking(appId);
|
||||
toast.error(ad.deployFailed);
|
||||
notify.error(ad.deployFailed);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -574,27 +574,27 @@ export default function AppDetailPage() {
|
||||
mutationFn: () => api.post(`/deployments/applications/${appId}/stop`),
|
||||
onSuccess: () => {
|
||||
invalidateAll();
|
||||
toast.success(ad.appStopped);
|
||||
notify.success(ad.appStopped);
|
||||
},
|
||||
onError: () => toast.error(ad.stopFailed),
|
||||
onError: () => notify.error(ad.stopFailed),
|
||||
});
|
||||
|
||||
const startMutation = useMutation({
|
||||
mutationFn: () => api.post(`/deployments/applications/${appId}/start`),
|
||||
onSuccess: () => {
|
||||
invalidateAll();
|
||||
toast.success(ad.appStarted);
|
||||
notify.success(ad.appStarted);
|
||||
},
|
||||
onError: () => toast.error(ad.startFailed),
|
||||
onError: () => notify.error(ad.startFailed),
|
||||
});
|
||||
|
||||
const restartMutation = useMutation({
|
||||
mutationFn: () => api.post(`/deployments/applications/${appId}/restart`),
|
||||
onSuccess: () => {
|
||||
invalidateAll();
|
||||
toast.success(ad.appRestarting);
|
||||
notify.success(ad.appRestarting);
|
||||
},
|
||||
onError: () => toast.error(ad.restartFailed),
|
||||
onError: () => notify.error(ad.restartFailed),
|
||||
});
|
||||
|
||||
const redeployMutation = useMutation({
|
||||
@@ -604,11 +604,11 @@ export default function AppDetailPage() {
|
||||
},
|
||||
onSuccess: () => {
|
||||
invalidateAll();
|
||||
toast.success(ad.redeployStarted);
|
||||
notify.success(ad.redeployStarted);
|
||||
},
|
||||
onError: () => {
|
||||
useDeployProgressStore.getState().stopTracking(appId);
|
||||
toast.error(ad.redeployFailed);
|
||||
notify.error(ad.redeployFailed);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -616,13 +616,13 @@ export default function AppDetailPage() {
|
||||
invalidateKeys: [['applications', 'application'], ['applications']],
|
||||
onSuccess: (data) => {
|
||||
if (data?.resourceCredit) {
|
||||
toast.success(ad.appDeletedCredit);
|
||||
notify.success(ad.appDeletedCredit);
|
||||
} else {
|
||||
toast.success(ad.appDeleted);
|
||||
notify.success(ad.appDeleted);
|
||||
}
|
||||
router.push('/dashboard/apps');
|
||||
},
|
||||
onError: () => toast.error(ad.deleteAppFailed),
|
||||
onError: () => notify.error(ad.deleteAppFailed),
|
||||
});
|
||||
|
||||
const scaleMutation = useMutation({
|
||||
@@ -639,12 +639,12 @@ export default function AppDetailPage() {
|
||||
setPendingUpgradePayload(null);
|
||||
const paidAmount = res.data.paidAmount || 0;
|
||||
if (paidAmount > 0) {
|
||||
toast.success(ad.upgradedPaid.replace('{amount}', paidAmount.toLocaleString('en-US')));
|
||||
notify.success(ad.upgradedPaid.replace('{amount}', paidAmount.toLocaleString('en-US')));
|
||||
} else {
|
||||
toast.success(ad.resourcesUpdatedSuccess);
|
||||
notify.success(ad.resourcesUpdatedSuccess);
|
||||
}
|
||||
},
|
||||
onError: (err: any) => toast.error(err.response?.data?.message || ad.updateResourcesFailed),
|
||||
onError: (err: any) => notify.error(err, ad.updateResourcesFailed),
|
||||
});
|
||||
|
||||
/** CPU/memory for DB / Redis / RabbitMQ — applies directly in Kubernetes (no billing wizard). */
|
||||
@@ -660,9 +660,9 @@ export default function AppDetailPage() {
|
||||
setResourceFormDirty(false);
|
||||
invalidateAll();
|
||||
queryClient.invalidateQueries({ queryKey: ['resources', appId] });
|
||||
toast.success(ad.resourcesUpdated);
|
||||
notify.success(ad.resourcesUpdated);
|
||||
},
|
||||
onError: (err: any) => toast.error(err.response?.data?.message || ad.updateResourcesFailed),
|
||||
onError: (err: any) => notify.error(err, ad.updateResourcesFailed),
|
||||
});
|
||||
|
||||
// Calculate upgrade cost before applying
|
||||
@@ -672,21 +672,21 @@ export default function AppDetailPage() {
|
||||
setUpgradeCostData(res.data);
|
||||
setShowUpgradeConfirm(true);
|
||||
},
|
||||
onError: (err: any) => toast.error(err.response?.data?.message || ad.calcFailed),
|
||||
onError: (err: any) => notify.error(err, ad.calcFailed),
|
||||
});
|
||||
|
||||
const createUpgradeInvoiceMutation = useMutation({
|
||||
mutationFn: (data: UpgradePayload) =>
|
||||
api.post<Invoice>(`/billing/applications/${appId}/upgrade/invoice`, data).then((r) => r.data),
|
||||
onSuccess: (invoice) => {
|
||||
toast.success(ad.invoiceCreated);
|
||||
notify.success(ad.invoiceCreated);
|
||||
queryClient.invalidateQueries({ queryKey: ['invoices'] });
|
||||
setShowUpgradeConfirm(false);
|
||||
setUpgradeCostData(null);
|
||||
setPendingUpgradePayload(null);
|
||||
router.push(`/dashboard/invoices?invoice=${invoice.id}`);
|
||||
},
|
||||
onError: (err: any) => toast.error(err.response?.data?.message || ad.invoiceFailed),
|
||||
onError: (err: any) => notify.error(err, ad.invoiceFailed),
|
||||
});
|
||||
|
||||
const buildWorkloadUpgradePayload = useCallback((): UpgradePayload => {
|
||||
@@ -774,7 +774,7 @@ export default function AppDetailPage() {
|
||||
patchDatabaseCpuIfNeeded();
|
||||
return;
|
||||
}
|
||||
toast.warn(ad.renewFirst);
|
||||
notify.warning(ad.renewFirst);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -783,7 +783,7 @@ export default function AppDetailPage() {
|
||||
const needsDbCpuPatch = scaleWorkload === 'database';
|
||||
|
||||
if (!hasBillingPayload && !needsDbCpuPatch) {
|
||||
toast.warn(ad.noChanges);
|
||||
notify.warning(ad.noChanges);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -879,12 +879,12 @@ export default function AppDetailPage() {
|
||||
const targetUrl = data.ingressUrl || data.url;
|
||||
window.open(targetUrl, '_blank');
|
||||
if (data.ingressUrl) {
|
||||
toast.success(ad.previewHttps);
|
||||
notify.success(ad.previewHttps);
|
||||
} else {
|
||||
toast.success(ad.previewPort.replace('{port}', String(data.nodePort)));
|
||||
notify.success(ad.previewPort.replace('{port}', String(data.nodePort)));
|
||||
}
|
||||
},
|
||||
onError: () => toast.error(ad.previewFailed),
|
||||
onError: () => notify.error(ad.previewFailed),
|
||||
});
|
||||
|
||||
const uploadMutation = useMutation({
|
||||
@@ -900,11 +900,11 @@ export default function AppDetailPage() {
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['application', appId] });
|
||||
toast.success(ad.sourceUploaded);
|
||||
notify.success(ad.sourceUploaded);
|
||||
setUploadProgress(0);
|
||||
},
|
||||
onError: () => {
|
||||
toast.error(ad.uploadFailed);
|
||||
notify.error(ad.uploadFailed);
|
||||
setUploadProgress(0);
|
||||
},
|
||||
});
|
||||
@@ -921,24 +921,24 @@ export default function AppDetailPage() {
|
||||
const data = res.data;
|
||||
setDbRestoreLogs(data.logs || null);
|
||||
if (data.success) {
|
||||
toast.success(ad.dbRestored);
|
||||
notify.success(ad.dbRestored);
|
||||
} else {
|
||||
toast.error(data.message || 'Database restore failed');
|
||||
notify.error(ad.dbRestoreFailed);
|
||||
}
|
||||
},
|
||||
onError: (err: any) => {
|
||||
toast.error(err.response?.data?.message || ad.dbUploadFailed);
|
||||
notify.error(err, ad.dbUploadFailed);
|
||||
setDbRestoreLogs(null);
|
||||
},
|
||||
});
|
||||
|
||||
const handleFileUpload = useCallback((file: File) => {
|
||||
if (!file.name.endsWith('.zip') && !file.name.endsWith('.tar.gz') && !file.name.endsWith('.tgz')) {
|
||||
toast.error(ad.fileZipOnly);
|
||||
notify.error(ad.fileZipOnly);
|
||||
return;
|
||||
}
|
||||
if (file.size > MAX_SOURCE_ARCHIVE_BYTES) {
|
||||
toast.error(ad.fileMax10);
|
||||
notify.error(ad.fileMax10);
|
||||
return;
|
||||
}
|
||||
uploadMutation.mutate(file);
|
||||
@@ -962,11 +962,11 @@ export default function AppDetailPage() {
|
||||
|
||||
const handleDbFileUpload = useCallback((file: File) => {
|
||||
if (!file.name.endsWith('.sql') && !file.name.endsWith('.gz') && !file.name.endsWith('.dump')) {
|
||||
toast.error(ad.dbInvalidFile);
|
||||
notify.error(ad.dbInvalidFile);
|
||||
return;
|
||||
}
|
||||
if (file.size > 500 * 1024 * 1024) {
|
||||
toast.error(ad.fileMax500);
|
||||
notify.error(ad.fileMax500);
|
||||
return;
|
||||
}
|
||||
setDbRestoreLogs(null);
|
||||
@@ -1698,7 +1698,7 @@ export default function AppDetailPage() {
|
||||
<button
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(domainInfo.fullPlatformUrl);
|
||||
toast.success(ad.copied);
|
||||
notify.success(ad.copied);
|
||||
}}
|
||||
className="text-blue-600 hover:text-blue-800 p-1"
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user