Skip to content

Commit 0a712f5

Browse files
committed
feat: add FetchClient and related types for improved API handling
1 parent 7143d74 commit 0a712f5

File tree

13 files changed

+533
-78
lines changed

13 files changed

+533
-78
lines changed

.github/copilot-instructions.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# Copilot Instructions for the Codebase
2+
3+
## Overview
4+
5+
- **React Kit** is a React/TypeScript library
6+
- The app uses Vite for build/dev, Playwright for E2E tests, and Vitest for unit tests. State management is via Jotai, data fetching with React Query, and UI with MUI
7+
8+
## React & TypeScript Best Practices
9+
10+
1. Always use the latest React 19 version and TypeScript for ReactJS development. Reference the latest React docs at https://react.dev/learn.
11+
2. Use React Router and the latest MUI components for all routing and UI needs.
12+
3. Write only functional components and use React hooks for all state management.
13+
4. Use TypeScript for type safety in all React components and logic.
14+
5. Apply the latest React performance optimizations (e.g., memoization, lazy loading, code splitting).
15+
6. Ensure all components meet accessibility (a11y) standards.
16+
7. Use ESLint and Prettier for code formatting and linting. Follow project configs.
17+
8. Add clear, concise comments to explain complex logic or important decisions.
18+
9. Define component props using TypeScript interfaces (preferred) or PropTypes.
19+
10. Design components to be reusable and modular, following DRY principles.
20+
21+
---
22+

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,4 +43,6 @@ Thumbs.db
4343
.nx/workspace-data
4444

4545
vite.config.*.timestamp*
46-
vitest.config.*.timestamp*
46+
vitest.config.*.timestamp*
47+
.cursor/rules/nx-rules.mdc
48+
.github/instructions/nx.instructions.md

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
"update": "nx migrate latest"
1313
},
1414
"dependencies": {
15+
"@axa-fr/react-oidc": "^7.25.14",
1516
"@emotion/react": "^11.14.0",
1617
"@emotion/styled": "^11.14.0",
1718
"@mui/icons-material": "^7.1.0",

pnpm-lock.yaml

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

react-kit/src/index.ts

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,41 @@
11
// Export all buttons
22
export { CancelButton } from './lib/components/buttons/CancelButton';
33
export { DeleteButton } from './lib/components/buttons/DeleteButton';
4+
export { EditIconButton } from './lib/components/buttons/EditIconButton';
5+
export { ExcelButton } from './lib/components/buttons/ExcelButton';
46
export { GoBackButton } from './lib/components/buttons/GoBackButton';
57
export { HistoryButton } from './lib/components/buttons/HistoryButton';
68
export { LoadingSuccessButton } from './lib/components/buttons/LoadingSuccessButton';
79
export { ManageButton } from './lib/components/buttons/ManageButton';
810
export { SuccessButton } from './lib/components/buttons/SuccessButton';
9-
export { ExcelButton } from './lib/components/buttons/ExcelButton';
10-
export { EditIconButton } from './lib/components/buttons/EditIconButton';
1111

1212
// Export snackbar components
1313
export { AppSnackBar } from './lib/components/snack-bar/AppSnackBar';
1414
export { QuerySnackBar } from './lib/components/snack-bar/QuerySnackBar';
1515

1616
// Export all other components
17-
export { TablePaginationActions } from './lib/components/table/TablePaginationActions';
18-
export { TabPanel, a11yProps } from './lib/components/tabs/TabPanel';
19-
export { NextLink } from './lib/components/NextLink';
2017
export { CenteredCircularProgress } from './lib/components/CenteredCircularProgress';
2118
export { ConfirmDialog } from './lib/components/ConfirmationDialog';
2219
export { DismissibleAlert } from './lib/components/DismissibleAlert';
20+
export { NextLink } from './lib/components/NextLink';
2321
export { OpenInNewIconLink } from './lib/components/OpenInNewIconLink';
2422
export { ReactIf } from './lib/components/ReactIf';
23+
export { TablePaginationActions } from './lib/components/table/TablePaginationActions';
24+
export { a11yProps, TabPanel } from './lib/components/tabs/TabPanel';
25+
26+
// Export fetch client
27+
export { FetchClient as fetchClient, FetchClient, FetchClient as httpClient } from './lib/config/fetch/FetchClient';
28+
export * from './lib/config/fetch/FetchClientTypes';
29+
export { FetchInterceptor } from './lib/config/fetch/FetchInterceptor';
2530

2631
// Export all utilities
2732
export * from './lib/utils/BooleanUtils';
33+
export * from './lib/utils/CssUtils';
2834
export * from './lib/utils/DateUtils';
2935
export * from './lib/utils/NumberUtils';
3036
export * from './lib/utils/ProgressStateUtils';
3137
export * from './lib/utils/StringUtils';
3238
export * from './lib/utils/UrlUtils';
33-
export * from './lib/utils/fetchClient';
34-
export * from './lib/utils/CssUtils';
3539

3640
// Export all types
3741
export * from './lib/types/ProgressState';

react-kit/src/lib/components/NextLink.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,13 @@ import React from 'react';
22
import { Link as MuiLink } from '@mui/material';
33
import { Link } from 'react-router-dom';
44

5+
interface Props {
6+
href: string;
7+
linkText?: string;
8+
target?: string;
9+
children?: React.ReactNode;
10+
}
11+
512
/**
613
* Reusable custom Next.js 13 Link component.
714
*
@@ -10,7 +17,7 @@ import { Link } from 'react-router-dom';
1017
* @author Pavan Kumar Jadda
1118
* @since 0.3.2
1219
*/
13-
export function NextLink(props: { href: string; linkText?: string; target?: string; children?: React.ReactNode }): React.JSX.Element {
20+
export function NextLink(props: Readonly<Props>): React.JSX.Element {
1421
return (
1522
<MuiLink component={Link} to={props.href} className={'next-btn-link'} underline="hover">
1623
{props.linkText ?? props.children}

react-kit/src/lib/components/OpenInNewIconLink.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { Icon, Link as MuiLink } from '@mui/material';
33
import React from 'react';
44
import { Link } from 'react-router-dom';
55

6-
interface OpenInNewIconLinkProps {
6+
interface Props {
77
href: string;
88
linkText: string;
99
target: string;
@@ -18,7 +18,7 @@ interface OpenInNewIconLinkProps {
1818
* @author Pavan Kumar Jadda
1919
* @since 1.2.24
2020
*/
21-
export function OpenInNewIconLink(props: OpenInNewIconLinkProps) {
21+
export function OpenInNewIconLink(props: Readonly<Props>) {
2222
return (
2323
<MuiLink
2424
component={Link}

react-kit/src/lib/components/buttons/EditIconButton.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
1-
import React from 'react';
2-
import { IconButton, Tooltip } from '@mui/material';
31
import EditIcon from '@mui/icons-material/Edit';
2+
import { IconButton, Tooltip } from '@mui/material';
3+
import React from 'react';
44

55
interface EditIconButtonProps {
66
tooltipTitle: string;
77
onClick: React.Dispatch<React.SetStateAction<boolean>>;
88
color?: 'inherit' | 'default' | 'primary' | 'secondary' | 'error' | 'info' | 'success' | 'warning';
99
}
1010

11-
export function EditIconButton(props: EditIconButtonProps) {
11+
export function EditIconButton(props: Readonly<EditIconButtonProps>) {
1212
return (
1313
<Tooltip title={props.tooltipTitle}>
1414
<IconButton
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
import { FetchError, FetchResponse, RequestConfig } from './FetchClientTypes';
2+
import { createFormattedError } from './FetchInterceptor';
3+
4+
/**
5+
* Build URL with query parameters
6+
* @param baseURL Base URL
7+
* @param url Endpoint URL
8+
* @param params Query parameters
9+
* @returns Complete URL with query parameters
10+
*/
11+
const buildURL = (baseURL: string, url: string, params?: Record<string, any>): string => {
12+
const fullURL = url.startsWith('http') ? url : `${baseURL}${url}`;
13+
14+
if (!params) return fullURL;
15+
16+
const urlObj = new URL(fullURL);
17+
Object.entries(params).forEach(([key, value]) => {
18+
if (value !== undefined && value !== null) {
19+
urlObj.searchParams.append(key, String(value));
20+
}
21+
});
22+
23+
return urlObj.toString();
24+
};
25+
26+
/**
27+
* Parse response data based on content type
28+
* @param response Fetch response
29+
* @returns Parsed data
30+
*/
31+
const parseResponseData = async (response: Response): Promise<any> => {
32+
const contentType = response.headers.get('content-type');
33+
34+
if (contentType?.includes('application/json')) {
35+
return await response.json();
36+
} else if (contentType?.includes('text/')) {
37+
return await response.text();
38+
} else {
39+
return await response.blob();
40+
}
41+
};
42+
43+
/**
44+
* Reusable fetch client class
45+
*
46+
* @author Pavan Kumar Jadda
47+
* @since 0.2.19
48+
*/
49+
class FetchInstance {
50+
private readonly baseURL: string;
51+
private requestInterceptors: Array<(config: RequestConfig) => RequestConfig | Promise<RequestConfig>> = [];
52+
private responseInterceptors: Array<(response: FetchResponse) => FetchResponse | Promise<FetchResponse>> = [];
53+
private errorInterceptors: Array<(error: FetchError) => FetchError | Promise<FetchError> | void | Promise<void>> = [];
54+
/**
55+
* Add request interceptor
56+
*/
57+
interceptors = {
58+
request: {
59+
use: (interceptor: (config: RequestConfig) => RequestConfig | Promise<RequestConfig>) => {
60+
this.requestInterceptors.push(interceptor);
61+
},
62+
},
63+
response: {
64+
use: (
65+
successInterceptor?: (response: FetchResponse) => FetchResponse | Promise<FetchResponse>,
66+
errorInterceptor?: (error: FetchError) => FetchError | Promise<FetchError> | void | Promise<void>
67+
) => {
68+
if (successInterceptor) {
69+
this.responseInterceptors.push(successInterceptor);
70+
}
71+
if (errorInterceptor) {
72+
this.errorInterceptors.push(errorInterceptor);
73+
}
74+
},
75+
},
76+
};
77+
78+
constructor(baseURL: string = import.meta.env.VITE_REACT_APP_BASE_URL) {
79+
this.baseURL = baseURL;
80+
}
81+
82+
/**
83+
* GET request
84+
*/
85+
async get<T = any>(url: string, config?: Omit<RequestConfig, 'method' | 'url'>): Promise<FetchResponse<T>> {
86+
return this.executeRequest({ ...config, method: 'GET', url });
87+
}
88+
89+
/**
90+
* POST request
91+
*/
92+
async post<T = any>(url: string, data?: any, config?: Omit<RequestConfig, 'method' | 'url' | 'body'>): Promise<FetchResponse<T>> {
93+
const body = data ? JSON.stringify(data) : undefined;
94+
return this.executeRequest({ ...config, method: 'POST', url, body });
95+
}
96+
97+
/**
98+
* PUT request
99+
*/
100+
async put<T = any>(url: string, data?: any, config?: Omit<RequestConfig, 'method' | 'url' | 'body'>): Promise<FetchResponse<T>> {
101+
const body = data ? JSON.stringify(data) : undefined;
102+
return this.executeRequest({ ...config, method: 'PUT', url, body });
103+
}
104+
105+
/**
106+
* DELETE request
107+
*/
108+
async delete<T = any>(url: string, config?: Omit<RequestConfig, 'method' | 'url'>): Promise<FetchResponse<T>> {
109+
return this.executeRequest({ ...config, method: 'DELETE', url });
110+
}
111+
112+
/**
113+
* PATCH request
114+
*/
115+
async patch<T = any>(url: string, data?: any, config?: Omit<RequestConfig, 'method' | 'url' | 'body'>): Promise<FetchResponse<T>> {
116+
const body = data ? JSON.stringify(data) : undefined;
117+
return this.executeRequest<T>({ ...config, method: 'PATCH', url, body });
118+
}
119+
120+
/**
121+
* Generic request method
122+
*/
123+
async request<T = any>(config: RequestConfig): Promise<FetchResponse<T>> {
124+
return this.executeRequest(config);
125+
}
126+
127+
/**
128+
* Execute request with interceptors
129+
*/
130+
private async executeRequest<T>(config: RequestConfig): Promise<FetchResponse<T>> {
131+
// Apply request interceptors
132+
let processedConfig = config;
133+
for (const interceptor of this.requestInterceptors) {
134+
processedConfig = await interceptor(processedConfig);
135+
}
136+
137+
const { url, params, ...fetchConfig } = processedConfig;
138+
const fullURL = buildURL(this.baseURL, url || '', params);
139+
140+
try {
141+
const response = await fetch(fullURL, fetchConfig);
142+
const data = await parseResponseData(response);
143+
144+
const fetchResponse: FetchResponse<T> = {
145+
data: data as T,
146+
status: response.status,
147+
statusText: response.statusText,
148+
headers: response.headers,
149+
config: processedConfig,
150+
};
151+
152+
// Apply response interceptors
153+
let processedResponse = fetchResponse;
154+
for (const interceptor of this.responseInterceptors) {
155+
processedResponse = await interceptor(processedResponse);
156+
}
157+
158+
return processedResponse;
159+
} catch (error) {
160+
const fetchError = createFormattedError(error as Error, 0, 'Network error. Please check your connection and try again.');
161+
162+
// Apply error interceptors
163+
let processedError = fetchError;
164+
for (const interceptor of this.errorInterceptors) {
165+
const result = await interceptor(processedError);
166+
if (result !== undefined) {
167+
processedError = result;
168+
}
169+
}
170+
171+
throw processedError;
172+
}
173+
}
174+
}
175+
176+
/**
177+
* Create fetch client instance
178+
*/
179+
const fetchInstance = new FetchInstance();
180+
181+
/**
182+
* Callable fetch client that defaults to GET requests
183+
*/
184+
export const FetchClient = Object.assign(
185+
async <T = any>(url: string, config?: Omit<RequestConfig, 'method' | 'url'>): Promise<FetchResponse<T>> => {
186+
return fetchInstance.get(url, config);
187+
},
188+
fetchInstance
189+
);
190+
191+
// Export the client instance for direct use
192+
export default FetchClient;

0 commit comments

Comments
 (0)