Skip to content

Commit a497a2f

Browse files
authored
Merge pull request #423 from JSE19/main
2 parents 3e75947 + 631ab9d commit a497a2f

6 files changed

Lines changed: 157 additions & 13 deletions

File tree

components/shared/PageTransition.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,10 @@ export function PageTransition({ children }: { children: React.ReactNode }) {
1111
<AnimatePresence mode="wait">
1212
<motion.div
1313
key={pathname}
14-
initial={prefersReducedMotion ? {} : { opacity: 0, y: 4 }}
14+
initial={prefersReducedMotion ? {} : { opacity: 0, y: 3 }}
1515
animate={{ opacity: 1, y: 0 }}
16-
exit={prefersReducedMotion ? {} : { opacity: 0, y: -4 }}
17-
transition={{ duration: prefersReducedMotion ? 0 : 0.2, ease: "easeInOut" }}
16+
exit={prefersReducedMotion ? {} : { opacity: 0, y: -3 }}
17+
transition={{ duration: prefersReducedMotion ? 0 : 0.12, ease: "easeOut" }}
1818
>
1919
{children}
2020
</motion.div>

lib/api/axios.ts

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,16 @@ import { getAppRouter } from '../navigation/appRouter';
1010
// Deduplication: avoid showing multiple toasts for simultaneous errors
1111
const recentErrors = new Map<string, number>();
1212
const ERROR_DEDUP_WINDOW_MS = 3000;
13+
let pendingErrorBatch: {
14+
count: number;
15+
message?: string;
16+
timer: ReturnType<typeof setTimeout> | null;
17+
} | null = null;
18+
19+
function showErrorToast(message: string) {
20+
toast.error(message, { duration: 5000 });
21+
announce(message);
22+
}
1323

1424
function notifyError(message: string, key?: string) {
1525
const dedupKey = key || message;
@@ -21,8 +31,25 @@ function notifyError(message: string, key?: string) {
2131
}
2232

2333
recentErrors.set(dedupKey, now);
24-
toast.error(message, { duration: 5000 });
25-
announce(message);
34+
35+
if (pendingErrorBatch) {
36+
pendingErrorBatch.count += 1;
37+
return;
38+
}
39+
40+
pendingErrorBatch = {
41+
count: 1,
42+
message,
43+
timer: setTimeout(() => {
44+
if (!pendingErrorBatch) {
45+
return;
46+
}
47+
48+
const summaryMessage = pendingErrorBatch.count > 1 ? 'Multiple errors occurred' : pendingErrorBatch.message || message;
49+
showErrorToast(summaryMessage);
50+
pendingErrorBatch = null;
51+
}, 150),
52+
};
2653

2754
// Clean up old entries
2855
if (recentErrors.size > 50) {
@@ -164,15 +191,16 @@ apiClient.interceptors.response.use(
164191
if (error.response?.status === 429) {
165192
const retryAfter = error.response?.headers?.['retry-after'];
166193
const seconds = parseInt(String(retryAfter), 10) || 30;
167-
const endpoint = originalRequest?.url || error.config?.url;
194+
const endpoint = originalRequest?.url || error.config?.url || 'unknown';
168195
const rawLimit = error.response?.headers?.['x-ratelimit-limit'] || error.response?.headers?.['X-RateLimit-Limit'];
169196
const limit = rawLimit ? parseInt(String(rawLimit), 10) : undefined;
170197
useRateLimitStore.getState().setRateLimited(seconds, endpoint, limit);
171-
notifyError(`Too many attempts. Please try again in ${seconds} seconds.`);
198+
notifyError(`Too many attempts. Please try again in ${seconds} seconds.`, `429_${endpoint}`);
172199
} else if (!error.response) {
173200
notifyError('Network error. Please check your connection.', 'network_error');
174201
} else if (error.response?.status >= 500) {
175-
notifyError('A server error occurred. Please try again later.', `5xx_${error.response.status}`);
202+
const endpoint = originalRequest?.url || error.config?.url || 'unknown';
203+
notifyError('A server error occurred. Please try again later.', `5xx_${error.response.status}_${endpoint}`);
176204
}
177205

178206
return Promise.reject(parseApiError(error));

lib/mock/README.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# Mock Data Notes
2+
3+
These files in this directory are mock API responses used to support UI development and local testing.
4+
5+
## What this directory is for
6+
7+
- The files under this folder simulate backend data for dashboard, transactions, settlements, wallet, payment links, FX, and developer-related views.
8+
- They are intended for frontend development when the real API is unavailable, slow, or not yet wired up.
9+
- Mock data should help contributors build and review UI flows without requiring a live backend.
10+
11+
## When to use the mocks
12+
13+
Use these mocks as a fallback when:
14+
15+
- the backend service is down or unreachable,
16+
- a feature is being built before API integration is ready,
17+
- you need predictable sample data for demos, screenshots, or local testing.
18+
19+
## How to add new mock data
20+
21+
1. Create a new file in this directory with a descriptive name, for example `orders.ts`.
22+
2. Export one or more data objects or arrays that match the shape expected by the UI.
23+
3. Keep the data realistic and consistent with the existing mock style.
24+
4. Import the new mock from the relevant feature module or fallback layer.
25+
26+
## Contract for replacing mocks with real API data
27+
28+
When the backend becomes available, the UI should be able to swap from mock data to real API data without changing the feature contract.
29+
30+
Follow these rules:
31+
32+
- Keep the same data shape and field names expected by the component.
33+
- Preserve the same response structure used by the UI layer (for example, arrays, objects, pagination fields, and status values).
34+
- Treat the mock as a temporary stand-in, not as the final source of truth.
35+
- Replace the mock import or fallback logic with the real API fetch/update flow once the backend contract is confirmed.
36+
37+
In short, mocks should be easy to recognize, easy to replace, and easy to evolve into real API integrations.

lib/utils/retry.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,17 @@
1+
import { ApiError } from './apiError';
2+
13
export interface RetryOptions {
24
maxRetries?: number;
35
baseDelay?: number;
46
maxDelay?: number;
57
onRetry?: (error: unknown, attempt: number) => void;
8+
isRetryable?: (error: unknown) => boolean;
69
}
710

11+
const defaultIsRetryable = (error: unknown): boolean => {
12+
return error instanceof ApiError && (error.status ?? 0) >= 500;
13+
};
14+
815
export async function retryWithBackoff<T>(
916
fn: () => Promise<T>,
1017
options?: RetryOptions,
@@ -14,6 +21,7 @@ export async function retryWithBackoff<T>(
1421
baseDelay = 1000,
1522
maxDelay = 30000,
1623
onRetry,
24+
isRetryable = defaultIsRetryable,
1725
} = options || {};
1826

1927
let attempt = 0;
@@ -22,6 +30,10 @@ export async function retryWithBackoff<T>(
2230
try {
2331
return await fn();
2432
} catch (error) {
33+
if (!isRetryable(error)) {
34+
throw error;
35+
}
36+
2537
attempt++;
2638

2739
if (attempt > maxRetries) {

pnpm-lock.yaml

Lines changed: 69 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pnpm-workspace.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
allowBuilds:
2+
core-js: set this to true or false
3+
unrs-resolver: set this to true or false

0 commit comments

Comments
 (0)