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
@@ -6,7 +6,7 @@ import { Link } from '@/i18n/Link';
import { useT, useLocale } from '@/i18n/I18nProvider';
import type { Dictionary } from '@/i18n/dictionaries/fa';
import api from '@/lib/api';
import { toast } from 'react-toastify';
import { notify } from '@/lib/notify';
import type { Application, AppLifecycleStatus, ApplicationMigrationEvent, ApplicationMigrationJob, BillingCycle, Cluster } from '@/types';
import { Search, X, Package, Hexagon, User, Database, Box, AlertTriangle, Clock, ArrowRightLeft, RefreshCw } from 'lucide-react';
import { useConfirm } from '@/components/confirm-modal';
@@ -138,18 +138,18 @@ export default function AdminAppsPage() {
}),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['application-migrations'] });
toast.success(aa.migrationQueued);
notify.success(aa.migrationQueued);
},
onError: (err: any) => toast.error(err?.response?.data?.message || aa.migrationQueueFailed),
onError: (err: any) => notify.error(err, aa.migrationQueueFailed),
});
const retryMigration = useMutation({
mutationFn: (id: string) => api.post(`/application-migrations/${id}/retry`),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['application-migrations'] });
toast.success(aa.migrationRetryQueued);
notify.success(aa.migrationRetryQueued);
},
onError: (err: any) => toast.error(err?.response?.data?.message || aa.migrationRetryFailed),
onError: (err: any) => notify.error(err, aa.migrationRetryFailed),
});
// Compute status counts from apps
@@ -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);
},
});
@@ -3,7 +3,7 @@
import { useState } 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 { Cluster, ClusterResources } from '@/types';
import { Server, CheckCircle, XCircle, BarChart3, Clock, RotateCw, Plug, X, ScrollText } from 'lucide-react';
@@ -123,14 +123,6 @@ function ResourcePanel({ clusterId }: { clusterId: string }) {
);
}
function apiErrorMessage(err: unknown, fallback: string): string {
const e = err as { response?: { data?: { message?: string | string[] } } };
const msg = e?.response?.data?.message;
if (Array.isArray(msg)) return msg.join(', ');
if (typeof msg === 'string' && msg.trim()) return msg;
return fallback;
}
interface ClusterToolField {
key: string;
label: string;
@@ -178,18 +170,18 @@ function ToolRow({ clusterId, tool, tools }: { clusterId: string; tool: ClusterT
invalidate();
setShowForm(false);
setFields({});
toast.success(res.data?.message || cl.installStarted.replace('{name}', tool.name));
notify.success(res.data?.message || cl.installStarted.replace('{name}', tool.name));
},
onError: (err) => toast.error(apiErrorMessage(err, cl.installToolFailed.replace('{name}', tool.name))),
onError: (err) => notify.error(err, cl.installToolFailed.replace('{name}', tool.name)),
});
const uninstallMutation = useMutation({
mutationFn: () => api.delete(`/clusters/${clusterId}/tools/${tool.id}`),
onSuccess: (res) => {
invalidate();
toast.success(res.data?.message || cl.toolRemoved.replace('{name}', tool.name));
notify.success(res.data?.message || cl.toolRemoved.replace('{name}', tool.name));
},
onError: (err) => toast.error(apiErrorMessage(err, cl.removeToolFailed.replace('{name}', tool.name))),
onError: (err) => notify.error(err, cl.removeToolFailed.replace('{name}', tool.name)),
});
const unmetDeps = tool.dependencies.filter(
@@ -212,7 +204,7 @@ function ToolRow({ clusterId, tool, tools }: { clusterId: string; tool: ClusterT
const submitForm = () => {
for (const f of tool.installFields) {
if (f.required && !fields[f.key]?.trim()) {
toast.error(cl.fieldRequired.replace('{field}', f.label));
notify.error(cl.fieldRequired.replace('{field}', f.label));
return;
}
}
@@ -412,13 +404,12 @@ export default function AdminClustersPage() {
}),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['admin-clusters'] });
toast.success(cl.added);
notify.success(cl.added);
setShowForm(false);
setForm({ name: '', description: '', apiServer: '', kubeconfig: '', region: '', provider: '', weight: 1, tags: '', isDefault: false });
},
onError: (err: any) => {
const message = err?.response?.data?.message || cl.addFailed;
toast.error(message);
notify.error(err, cl.addFailed);
},
});
@@ -431,14 +422,14 @@ export default function AdminClustersPage() {
queryClient.invalidateQueries({ queryKey: ['admin-clusters'] });
const data = res.data;
if (data.connected) {
toast.success(cl.connectionOk.replace('{version}', data.version));
notify.success(cl.connectionOk.replace('{version}', data.version));
} else {
toast.error(cl.connectionFailed.replace('{error}', data.error));
notify.error(cl.connectionFailed.replace('{error}', data.error));
}
setTestingId(null);
},
onError: () => {
toast.error(cl.testFailed);
notify.error(cl.testFailed);
setTestingId(null);
},
});
@@ -447,7 +438,7 @@ export default function AdminClustersPage() {
mutationFn: (id: string) => api.delete(`/clusters/${id}`),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['admin-clusters'] });
toast.success(cl.removed);
notify.success(cl.removed);
},
});
@@ -3,7 +3,7 @@
import { useState } from 'react';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { FileText, Search, User, Wallet, CreditCard, XCircle, Download } from 'lucide-react';
import { toast } from 'react-toastify';
import { notify } from '@/lib/notify';
import api from '@/lib/api';
import { useT, useLocale } from '@/i18n/I18nProvider';
import type { Invoice, InvoiceStatus, PaymentMethod } from '@/types';
@@ -51,12 +51,12 @@ export default function AdminInvoicesPage() {
mutationFn: ({ invoiceId, nextStatus, reason }: { invoiceId: string; nextStatus: InvoiceStatus; reason: string }) =>
api.patch(`/billing/admin/invoices/${invoiceId}/status`, { status: nextStatus, reason }).then((r) => r.data),
onSuccess: () => {
toast.success(inv.statusUpdated);
notify.success(inv.statusUpdated);
setStatusReason('');
queryClient.invalidateQueries({ queryKey: ['admin-invoices'] });
queryClient.invalidateQueries({ queryKey: ['admin-invoice', selectedId] });
},
onError: (err: any) => toast.error(err.response?.data?.message || inv.statusUpdateFailed),
onError: (err: any) => notify.error(err, inv.statusUpdateFailed),
});
const downloadPdfMutation = useMutation({
@@ -71,7 +71,7 @@ export default function AdminInvoicesPage() {
link.remove();
window.URL.revokeObjectURL(url);
},
onError: () => toast.error(inv.downloadFailed),
onError: () => notify.error(inv.downloadFailed),
});
const formatPrice = (amount: number) => Number(amount || 0).toLocaleString('en-US');
@@ -80,7 +80,7 @@ export default function AdminInvoicesPage() {
const handleStatusUpdate = (nextStatus: InvoiceStatus) => {
if (!selectedInvoice) return;
if (!statusReason.trim()) {
toast.error(inv.reasonRequired);
notify.error(inv.reasonRequired);
return;
}
updateStatusMutation.mutate({
@@ -3,7 +3,7 @@
import { useState } 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 { Cluster, ClusterPool } from '@/types';
import { Scale, BarChart3, RotateCw, CheckCircle, XCircle, Pencil, Clock, X } from 'lucide-react';
@@ -45,11 +45,11 @@ export default function AdminPoolsPage() {
mutationFn: (data: typeof form) => api.post('/clusters/pools', data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['admin-pools'] });
toast.success(p.poolCreated);
notify.success(p.poolCreated);
resetForm();
},
onError: (err: any) => {
toast.error(err?.response?.data?.message || p.createFailed);
notify.error(err, p.createFailed);
},
});
@@ -58,11 +58,11 @@ export default function AdminPoolsPage() {
api.patch(`/clusters/pools/${id}`, data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['admin-pools'] });
toast.success(p.poolUpdated);
notify.success(p.poolUpdated);
resetForm();
},
onError: (err: any) => {
toast.error(err?.response?.data?.message || p.updateFailed);
notify.error(err, p.updateFailed);
},
});
@@ -70,7 +70,7 @@ export default function AdminPoolsPage() {
mutationFn: (id: string) => api.delete(`/clusters/pools/${id}`),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['admin-pools'] });
toast.success(p.poolDeleted);
notify.success(p.poolDeleted);
},
});
@@ -5,7 +5,7 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import api from '@/lib/api';
import { useAuthStore } from '@/lib/store';
import { useT, useLocale } from '@/i18n/I18nProvider';
import { toast } from 'react-toastify';
import { notify } from '@/lib/notify';
import type { AdminUser } from '@/types';
import { Users, Search, X, Clock, KeyRound } from 'lucide-react';
import { Select } from '@/components/ui/select';
@@ -48,12 +48,12 @@ export default function AdminUsersPage() {
mutationFn: (data: typeof form) => api.post('/users', data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['admin-users'] });
toast.success(u.createdSuccess);
notify.success(u.createdSuccess);
setShowForm(false);
setForm({ email: '', password: '', firstName: '', lastName: '', role: 'user' });
},
onError: (err: any) => {
toast.error(err?.response?.data?.message || u.createFailed);
notify.error(err, u.createFailed);
},
});
@@ -62,7 +62,7 @@ export default function AdminUsersPage() {
api.patch(`/users/${id}/${isActive ? 'deactivate' : 'activate'}`),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['admin-users'] });
toast.success(u.userUpdated);
notify.success(u.userUpdated);
},
});
@@ -71,7 +71,7 @@ export default function AdminUsersPage() {
api.patch(`/users/${id}/role`, { role }),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['admin-users'] });
toast.success(u.roleUpdated);
notify.success(u.roleUpdated);
},
});
@@ -80,16 +80,12 @@ export default function AdminUsersPage() {
api.patch(`/users/${id}/password`, { password }),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['admin-users'] });
toast.success(u.passwordUpdated);
notify.success(u.passwordUpdated);
setPwdModalUser(null);
setPwdModalPassword('');
},
onError: (err: unknown) => {
const msg =
err && typeof err === 'object' && 'response' in err
? (err as { response?: { data?: { message?: string } } }).response?.data?.message
: undefined;
toast.error(typeof msg === 'string' ? msg : u.passwordFailed);
notify.error(err, u.passwordFailed);
},
});
@@ -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"
>
@@ -10,7 +10,7 @@ import { parseDotenv } from '@/lib/parseDotenv';
import { useAuthStore } from '@/lib/store';
import { useDeployProgressStore } from '@/lib/deploy-progress-store';
import { useDeployProgressActions } from '@/lib/use-deploy-progress-actions';
import { toast } from 'react-toastify';
import { notify } from '@/lib/notify';
import type {
CreateApplicationDto,
ClusterPublic,
@@ -352,13 +352,13 @@ export default function DeployPage() {
setDnsCheckResult(data);
if (data.verified) {
setDnsVerified(true);
toast.success(dw.dnsVerifiedProceed);
notify.success(dw.dnsVerifiedProceed);
} else {
toast.warning(data.message);
notify.warning(data.message);
}
},
onError: () => {
toast.error(dw.dnsCheckFailed);
notify.error(dw.dnsCheckFailed);
},
});
@@ -455,14 +455,14 @@ export default function DeployPage() {
},
onSuccess: (res) => {
setDeployStage('deploying');
toast.success(dw.paymentSuccessDeploying);
notify.success(dw.paymentSuccessDeploying);
void triggerAppDeploy(res.data.id, res.data.name);
setDeployStage('done');
router.push(`/dashboard/apps/${res.data.id}`);
},
onError: (err: any) => {
setDeployStage('error');
toast.error(err.response?.data?.message || 'Payment or deployment failed');
notify.error(err, 'Payment or deployment failed');
setUploadProgress(0);
setDbUploadProgress(0);
setTimeout(() => setDeployStage('idle'), 2000);
@@ -537,14 +537,14 @@ export default function DeployPage() {
},
onSuccess: (res) => {
setDeployStage('deploying');
toast.success(dw.paymentSuccessDeploying);
notify.success(dw.paymentSuccessDeploying);
void triggerAppDeploy(res.data.id, res.data.name);
setDeployStage('done');
router.push(`/dashboard/apps/${res.data.id}`);
},
onError: (err: any) => {
setDeployStage('error');
toast.error(err.response?.data?.message || 'Payment failed');
notify.error(err, 'Payment failed');
setUploadProgress(0);
setDbUploadProgress(0);
setTimeout(() => setDeployStage('idle'), 2000);
@@ -592,14 +592,14 @@ export default function DeployPage() {
},
onSuccess: (res) => {
setDeployStage('deploying');
toast.success(dw.appCreatedDeploying);
notify.success(dw.appCreatedDeploying);
void triggerAppDeploy(res.data.id, res.data.name);
setDeployStage('done');
router.push(`/dashboard/apps/${res.data.id}`);
},
onError: (err: any) => {
setDeployStage('error');
toast.error(err.response?.data?.message || 'Failed to create application');
notify.error(err, 'Failed to create application');
setUploadProgress(0);
setDbUploadProgress(0);
setTimeout(() => setDeployStage('idle'), 2000);
@@ -628,18 +628,18 @@ export default function DeployPage() {
const count = Object.keys(vars).length;
if (count === 0) {
toast.error(errors.length > 0 ? errors[0] : dw.noValidEnvVars);
notify.error(errors.length > 0 ? errors[0] : dw.noValidEnvVars);
return;
}
setForm((prev) => ({ ...prev, envVars: { ...prev.envVars, ...vars } }));
setEnvFile(file);
toast.success(`${count} environment variable${count === 1 ? '' : 's'} imported from ${file.name}`);
notify.success(`${count} environment variable${count === 1 ? '' : 's'} imported from ${file.name}`);
if (errors.length > 0) {
toast.warn(`${errors.length} line${errors.length === 1 ? '' : 's'} skipped`);
notify.warning(`${errors.length} line${errors.length === 1 ? '' : 's'} skipped`);
}
};
reader.onerror = () => toast.error(dw.envReadFailed);
reader.onerror = () => notify.error(dw.envReadFailed);
reader.readAsText(file);
};
@@ -670,11 +670,11 @@ export default function DeployPage() {
const hasValidExt = validExtensions.some((ext) => file.name.toLowerCase().endsWith(ext));
if (!hasValidExt && !validTypes.includes(file.type)) {
toast.error(dw.onlyZipAllowed);
notify.error(dw.onlyZipAllowed);
return;
}
if (file.size > MAX_SOURCE_CODE_UPLOAD_BYTES) {
toast.error(dw.fileMax10);
notify.error(dw.fileMax10);
return;
}
setZipFile(file);
@@ -683,7 +683,7 @@ export default function DeployPage() {
const cur = parseInt(prev.appStorageSize || '2', 10) || 2;
return { ...prev, appStorageSize: String(Math.max(cur, minG)) };
});
toast.success(`Selected: ${file.name}`);
notify.success(`Selected: ${file.name}`);
}, []);
const handleDrop = useCallback((e: React.DragEvent) => {
@@ -707,11 +707,11 @@ export default function DeployPage() {
const validExtensions = ['.zip', '.tar.gz', '.tgz'];
const hasValidExt = validExtensions.some((ext) => file.name.toLowerCase().endsWith(ext));
if (!hasValidExt) {
toast.error(dw.onlyZipAllowed);
notify.error(dw.onlyZipAllowed);
return;
}
if (file.size > MAX_SOURCE_CODE_UPLOAD_BYTES) {
toast.error(dw.archiveMax10);
notify.error(dw.archiveMax10);
return;
}
setWpContentFile(file);
@@ -722,7 +722,7 @@ export default function DeployPage() {
const cur = parseInt(prev.appStorageSize || '2', 10) || 2;
return { ...prev, appStorageSize: String(Math.max(cur, target)) };
});
toast.success(`WordPress files selected: ${file.name}`);
notify.success(`WordPress files selected: ${file.name}`);
}, []);
const handleWpDrop = useCallback((e: React.DragEvent) => {
@@ -773,7 +773,7 @@ export default function DeployPage() {
if (data.verified) {
setDnsVerified(true);
setDnsCheckResult(data);
toast.success(dw.dnsVerified);
notify.success(dw.dnsVerified);
setStep(step + 1);
} else {
setDnsCheckResult(data);
@@ -1599,9 +1599,9 @@ export default function DeployPage() {
const f = e.dataTransfer.files[0];
if (f) {
if (!f.name.endsWith('.sql') && !f.name.endsWith('.gz') && !f.name.endsWith('.dump')) {
toast.error(dw.allowedDumpFormats);
notify.error(dw.allowedDumpFormats);
} else if (f.size > 500 * 1024 * 1024) {
toast.error(dw.maxSize500);
notify.error(dw.maxSize500);
} else {
setDbDumpFile(f);
const suggested = Math.max(
@@ -1628,9 +1628,9 @@ export default function DeployPage() {
const f = e.target.files?.[0];
if (f) {
if (!f.name.endsWith('.sql') && !f.name.endsWith('.gz') && !f.name.endsWith('.dump')) {
toast.error(dw.allowedDumpFormats);
notify.error(dw.allowedDumpFormats);
} else if (f.size > 500 * 1024 * 1024) {
toast.error(dw.maxSize500);
notify.error(dw.maxSize500);
} else {
setDbDumpFile(f);
const suggested = Math.max(
@@ -2849,7 +2849,7 @@ export default function DeployPage() {
walletPayMutation.mutate();
} else if (paymentMethod === 'wallet') {
if (!hasEnoughBalance) {
toast.error(dw.insufficientWallet);
notify.error(dw.insufficientWallet);
return;
}
walletPayMutation.mutate();
@@ -4,7 +4,7 @@ import { useEffect, useState } from 'react';
import { useSearchParams } from 'next/navigation';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { CreditCard, FileText, Wallet, XCircle, CheckCircle, Clock, Download } from 'lucide-react';
import { toast } from 'react-toastify';
import { notify } from '@/lib/notify';
import api from '@/lib/api';
import { useT, useLocale } from '@/i18n/I18nProvider';
import type { Invoice, InvoiceStatus } from '@/types';
@@ -69,11 +69,11 @@ export default function InvoicesPage() {
mutationFn: ({ invoiceId, trackingCode, amount }: { invoiceId: string; trackingCode: string; amount: number }) =>
api.post(`/billing/invoices/${invoiceId}/gateway/verify`, { trackingCode, amount }).then((r) => r.data),
onSuccess: (data) => {
toast.success(data.effect ? inv.paymentCompleteUpdated : inv.paymentComplete);
notify.success(data.effect ? inv.paymentCompleteUpdated : inv.paymentComplete);
refresh();
window.history.replaceState({}, '', window.location.pathname);
},
onError: (err: any) => toast.error(err.response?.data?.message || inv.gatewayFailed),
onError: (err: any) => notify.error(err, inv.gatewayFailed),
});
useEffect(() => {
@@ -99,10 +99,10 @@ export default function InvoicesPage() {
},
onSuccess: (data) => {
if (data.gatewayUrl && data.gatewayAmount > 0) return;
toast.success(data.effect ? inv.invoicePaidUpdated : inv.invoicePaid);
notify.success(data.effect ? inv.invoicePaidUpdated : inv.invoicePaid);
refresh();
},
onError: (err: any) => toast.error(err.response?.data?.message || inv.paymentFailed),
onError: (err: any) => notify.error(err, inv.paymentFailed),
});
const downloadPdfMutation = useMutation({
@@ -117,7 +117,7 @@ export default function InvoicesPage() {
link.remove();
window.URL.revokeObjectURL(url);
},
onError: () => toast.error(inv.downloadFailed),
onError: () => notify.error(inv.downloadFailed),
});
const formatPrice = (amount: number) => Number(amount || 0).toLocaleString('en-US');
@@ -4,7 +4,7 @@ import { useState, useEffect, useCallback } from 'react';
import { useParams } from 'next/navigation';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import api from '@/lib/api';
import { toast } from 'react-toastify';
import { notify } from '@/lib/notify';
import { Link } from '@/i18n/Link';
import { useT, useLocale } from '@/i18n/I18nProvider';
import { useLocalizedRouter } from '@/i18n/navigation';
@@ -127,14 +127,14 @@ export default function ManagedServiceDetailPage() {
notifyDeployStarted(serviceId, app?.name);
},
onSuccess: () => {
toast.success(sd.provisioningStarted);
notify.success(sd.provisioningStarted);
queryClient.invalidateQueries({ queryKey: ['deployments', serviceId] });
queryClient.invalidateQueries({ queryKey: ['application', serviceId] });
queryClient.invalidateQueries({ queryKey: ['applications'] });
},
onError: () => {
useDeployProgressStore.getState().stopTracking(serviceId);
toast.error(sd.provisioningFailed);
notify.error(sd.provisioningFailed);
},
});
@@ -144,37 +144,34 @@ export default function ManagedServiceDetailPage() {
notifyDeployStarted(serviceId, app?.name);
},
onSuccess: () => {
toast.success(sd.reprovisioningStarted);
notify.success(sd.reprovisioningStarted);
queryClient.invalidateQueries({ queryKey: ['deployments', serviceId] });
queryClient.invalidateQueries({ queryKey: ['application', serviceId] });
queryClient.invalidateQueries({ queryKey: ['applications'] });
},
onError: (err: unknown) => {
useDeployProgressStore.getState().stopTracking(serviceId);
const msg = (err as { response?: { data?: { message?: string } } })?.response?.data?.message;
toast.error(msg || sd.reprovisionFailed);
notify.error(err, sd.reprovisionFailed);
},
});
const restartMutation = useMutation({
mutationFn: () => api.post(`/deployments/applications/${serviceId}/restart`),
onSuccess: () => toast.success(sd.serviceRestarted),
onSuccess: () => notify.success(sd.serviceRestarted),
onError: (err: unknown) => {
const msg = (err as { response?: { data?: { message?: string } } })?.response?.data?.message;
toast.error(msg || sd.restartFailed);
notify.error(err, sd.restartFailed);
},
});
const renewMutation = useMutation({
mutationFn: (cycle: string) => api.post(`/billing/applications/${serviceId}/renew`, { cycle }),
onSuccess: () => {
toast.success(sd.serviceRenewed);
notify.success(sd.serviceRenewed);
queryClient.invalidateQueries({ queryKey: ['application', serviceId] });
setShowRenewalModal(false);
},
onError: (err: unknown) => {
const msg = (err as { response?: { data?: { message?: string } } })?.response?.data?.message;
toast.error(msg || sd.renewalFailed);
notify.error(err, sd.renewalFailed);
},
});
@@ -183,13 +180,13 @@ export default function ManagedServiceDetailPage() {
onSuccess: (data) => {
queryClient.invalidateQueries({ queryKey: ['application', serviceId] });
if (data?.resourceCredit) {
toast.success(sd.deletedWithCredit);
notify.success(sd.deletedWithCredit);
} else {
toast.success(sd.deleted);
notify.success(sd.deleted);
}
router.push('/dashboard/services');
},
onError: () => toast.error(sd.deleteFailed),
onError: () => notify.error(sd.deleteFailed),
});
const copyToClipboard = useCallback((text: string, field: string) => {
@@ -3,7 +3,7 @@
import { useState, useMemo } from 'react';
import { useMutation, useQuery } from '@tanstack/react-query';
import api from '@/lib/api';
import { toast } from 'react-toastify';
import { notify } from '@/lib/notify';
import { Link } from '@/i18n/Link';
import { useT } from '@/i18n/I18nProvider';
import { useLocalizedRouter } from '@/i18n/navigation';
@@ -187,7 +187,7 @@ export default function NewManagedServicePage() {
throw new Error(s.deployFailed);
}
setDeployStage('done');
toast.success(s.provisionedSuccess);
notify.success(s.provisionedSuccess);
router.push(`/dashboard/services/${appId}`);
};
@@ -220,12 +220,11 @@ export default function NewManagedServicePage() {
onSuccess: (appId) =>
finishDeploy(appId, form.name).catch(() => {
setDeployStage('error');
toast.error(s.paySucceededDeployFailed);
notify.error(s.paySucceededDeployFailed);
}),
onError: (err: unknown) => {
setDeployStage('error');
const msg = (err as { response?: { data?: { message?: string } } })?.response?.data?.message;
toast.error(msg || s.payProvisionFailed);
notify.error(err, s.payProvisionFailed);
setTimeout(() => setDeployStage('idle'), 2000);
},
});
@@ -256,12 +255,11 @@ export default function NewManagedServicePage() {
onSuccess: (appId) =>
finishDeploy(appId, form.name).catch(() => {
setDeployStage('error');
toast.error(s.paySucceededDeployFailed);
notify.error(s.paySucceededDeployFailed);
}),
onError: (err: unknown) => {
setDeployStage('error');
const msg = (err as { response?: { data?: { message?: string } } })?.response?.data?.message;
toast.error(msg || s.payFailed);
notify.error(err, s.payFailed);
setTimeout(() => setDeployStage('idle'), 2000);
},
});
@@ -283,7 +281,7 @@ export default function NewManagedServicePage() {
if (payAmount === 0) walletPayMutation.mutate();
else if (paymentMethod === 'wallet') {
if (!hasEnoughBalance) {
toast.error(s.insufficientBalance);
notify.error(s.insufficientBalance);
return;
}
walletPayMutation.mutate();
@@ -7,7 +7,7 @@ import { useLocalizedRouter } from '@/i18n/navigation';
import { useT, useLocale } from '@/i18n/I18nProvider';
import api from '@/lib/api';
import { useAuthStore } from '@/lib/store';
import { toast } from 'react-toastify';
import { notify } from '@/lib/notify';
import type { Ticket } from '@/types';
import { Wrench, Briefcase } from 'lucide-react';
@@ -40,16 +40,16 @@ export default function TicketDetailPage() {
onSuccess: () => {
setReply('');
queryClient.invalidateQueries({ queryKey: ['ticket', id] });
toast.success(tk.replySent);
notify.success(tk.replySent);
},
onError: (err: any) => toast.error(err.response?.data?.message || tk.replyFailed),
onError: (err: any) => notify.error(err, tk.replyFailed),
});
const closeMutation = useMutation({
mutationFn: () => api.patch(`/tickets/${id}/close`).then((r) => r.data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['ticket', id] });
toast.success(tk.ticketClosed);
notify.success(tk.ticketClosed);
},
});
@@ -6,7 +6,7 @@ import { Link } from '@/i18n/Link';
import { useT, useLocale } from '@/i18n/I18nProvider';
import api from '@/lib/api';
import { useAuthStore } from '@/lib/store';
import { toast } from 'react-toastify';
import { notify } from '@/lib/notify';
import type { Ticket, CreateTicketDto, TicketDepartment, TicketPriority } from '@/types';
import { Wrench, Briefcase, Ticket as TicketIcon, X } from 'lucide-react';
import { Select } from '@/components/ui/select';
@@ -56,12 +56,12 @@ export default function TicketsPage() {
const createMutation = useMutation({
mutationFn: (data: CreateTicketDto) => api.post('/tickets', data).then((r) => r.data),
onSuccess: () => {
toast.success(tk.createdSuccess);
notify.success(tk.createdSuccess);
queryClient.invalidateQueries({ queryKey: ['my-tickets'] });
setShowCreate(false);
setForm({ subject: '', department: availableDepartments[0]?.value || 'technical', priority: 'medium', message: '' });
},
onError: (err: any) => toast.error(err.response?.data?.message || tk.createFailed),
onError: (err: any) => notify.error(err, tk.createFailed),
});
const handleSubmit = (e: React.FormEvent) => {
@@ -3,7 +3,7 @@
import { useState } 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 type { WalletTransaction, TransactionType } from '@/types';
import { Link } from '@/i18n/Link';
import { useT, useLocale } from '@/i18n/I18nProvider';
@@ -48,11 +48,11 @@ export default function WalletPage() {
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['wallet-balance'] });
queryClient.invalidateQueries({ queryKey: ['wallet-transactions'] });
toast.success(w.chargedSuccess);
notify.success(w.chargedSuccess);
setChargeAmount('');
setShowCharge(false);
},
onError: (err: any) => toast.error(err.response?.data?.message || w.chargeFailed),
onError: (err: any) => notify.error(err, w.chargeFailed),
});
// Payment gateway charge
@@ -74,17 +74,17 @@ export default function WalletPage() {
});
queryClient.invalidateQueries({ queryKey: ['wallet-balance'] });
queryClient.invalidateQueries({ queryKey: ['wallet-transactions'] });
toast.success(w.paymentSuccess);
notify.success(w.paymentSuccess);
setChargeAmount('');
setShowCharge(false);
},
onError: (err: any) => toast.error(err.response?.data?.message || w.paymentFailed),
onError: (err: any) => notify.error(err, w.paymentFailed),
});
const handleCharge = (method: 'wallet' | 'gateway') => {
const amount = Number(chargeAmount);
if (!amount || amount < 1000) {
toast.error(w.minChargeError);
notify.error(w.minChargeError);
return;
}
if (method === 'wallet') {
+3 -3
View File
@@ -2,7 +2,7 @@
import { useState } from 'react';
import { useAuthStore } from '@/lib/store';
import { toast } from 'react-toastify';
import { notify } from '@/lib/notify';
import { LogIn, ArrowLeft } from 'lucide-react';
import { AuthShell } from '@/components/auth/AuthShell';
import { AuthField } from '@/components/auth/AuthField';
@@ -22,10 +22,10 @@ export default function LoginPage() {
setIsLoading(true);
try {
await login(email, password);
toast.success(tl.success);
notify.success(tl.success);
router.push('/dashboard');
} catch (err: any) {
toast.error(err.response?.data?.message || tl.error);
notify.error(err, tl.error);
} finally {
setIsLoading(false);
}
+3 -3
View File
@@ -2,7 +2,7 @@
import { useState } from 'react';
import { useAuthStore } from '@/lib/store';
import { toast } from 'react-toastify';
import { notify } from '@/lib/notify';
import { UserPlus, ArrowLeft } from 'lucide-react';
import { AuthShell } from '@/components/auth/AuthShell';
import { AuthField } from '@/components/auth/AuthField';
@@ -21,10 +21,10 @@ export default function RegisterPage() {
setIsLoading(true);
try {
await register(form);
toast.success(tr.success);
notify.success(tr.success);
router.push('/dashboard');
} catch (err: any) {
toast.error(err.response?.data?.message || tr.error);
notify.error(err, tr.error);
} finally {
setIsLoading(false);
}
+55
View File
@@ -237,3 +237,58 @@ html.lenis body {
font-size: 16px !important;
}
}
/* Toast notifications (react-toastify)
Reskin the library defaults into project-styled cards. The body content
(icon chip + message) is rendered by src/lib/notify.tsx; here we shape the
shell, spacing, shadow, RTL flow and the type-colored progress bar. */
.abrban-toast-container {
width: auto;
max-width: min(92vw, 26rem);
padding: 0;
}
.abrban-toast-container .abrban-toast {
border-radius: 0.875rem; /* rounded-xl */
border: 1px solid rgb(229 231 235); /* gray-200 */
background: #fff;
box-shadow:
0 10px 30px -12px rgba(2, 6, 23, 0.25),
0 2px 8px -4px rgba(2, 6, 23, 0.12);
padding: 0.75rem 0.875rem;
min-height: 0;
font-family: inherit;
color: rgb(31 41 55); /* gray-800 */
}
.abrban-toast-container .Toastify__toast-body {
padding: 0;
margin: 0;
font-family: inherit;
}
.abrban-toast-container .Toastify__close-button {
color: rgb(156 163 175); /* gray-400 */
opacity: 1;
align-self: center;
transition: color 0.15s ease;
}
.abrban-toast-container .Toastify__close-button:hover {
color: rgb(75 85 99); /* gray-600 */
}
/* Thin, type-colored progress bar in place of the default rainbow gradients. */
.abrban-toast-container .Toastify__progress-bar {
height: 3px;
opacity: 0.9;
}
.abrban-toast-container .Toastify__progress-bar--success { background: rgb(16 185 129); }
.abrban-toast-container .Toastify__progress-bar--error { background: rgb(239 68 68); }
.abrban-toast-container .Toastify__progress-bar--warning { background: rgb(245 158 11); }
.abrban-toast-container .Toastify__progress-bar--info { background: rgb(59 130 246); }
/* RTL: keep the colored accent border on the leading (right) edge. */
.abrban-toast-container.Toastify__toast-container--rtl {
right: 1rem;
left: auto;
}