feat: ApiContext

This commit is contained in:
Firman Syah 2026-06-02 13:17:58 +07:00
parent 25c9bc21b3
commit 1797af7ba9
8 changed files with 4444 additions and 4262 deletions

8444
dist/index.js vendored

File diff suppressed because it is too large Load Diff

27
src/App.jsx Normal file
View File

@ -0,0 +1,27 @@
import ListVirtual from "./components/ListVirtual";
import { ApiProvider } from "./components/ApiContext";
import { API_TOKEN } from "./config/constants";
const Testing = () => {
const token = API_TOKEN;
return (
<div style={{ padding: 8, marginTop: 12 }}>
<ApiProvider
headers={{
Authorization: `Bearer ${token}`,
}}
>
<ListVirtual
id="company-select"
label="Company"
name="name"
url="/company"
defaultValue={"442"}
size="small"
/>
</ApiProvider>
</div>
)
}
export default Testing;

View File

@ -1,3 +1,5 @@
// src/api/request.js
import { formatApiResponse } from '../utils/response';
import { oslogApi, apiGo } from './axiosClient';
@ -10,31 +12,26 @@ const request = async ({
endpoint = '',
suffix = '',
data = null,
params = {},
headers = {},
customUrl = '',
axiosConfig = {},
axiosConfig = {}
}) => {
const url = customUrl || `${endpoint}${suffix}`;
try {
const client = isApiGo ? apiGo : oslogApi;
const response = await client({
method,
url,
data,
params,
headers,
...axiosConfig,
// highest priority
...axiosConfig
});
return formatApiResponse(response);
} catch (error) {
const responseError = formatApiResponse(error);
//logError(`at endpoint: ${url} error: `, responseError);
return responseError;
return formatApiResponse(error);
}
};
@ -42,65 +39,86 @@ const request = async ({
* CREATE NEW
* POST /endpoint/new
*/
export const newRequest = (endpoint, data = null, config = {}) =>
export const newRequest = (
endpoint,
data = null,
axiosConfig = {}
) =>
request({
method: 'post',
endpoint,
suffix: '/new',
data,
...config,
axiosConfig
});
/**
* ADD
* POST /endpoint/add
*/
export const addRequest = (endpoint, data = null, config = {}) =>
export const addRequest = (
endpoint,
data = null,
axiosConfig = {}
) =>
request({
method: 'post',
endpoint,
suffix: '/add',
data,
...config,
axiosConfig
});
/**
* GET BY ID
* GET /endpoint/:id
*/
export const getByIdRequest = (id, endpoint, params = {}, config = {}) =>
export const getByIdRequest = (
id,
endpoint,
axiosConfig = {}
) =>
request({
method: 'get',
endpoint,
suffix: `/${id}`,
params,
...config,
axiosConfig
});
/**
* EDIT
* PUT /endpoint/edit/:id
*/
export const editRequest = (id, endpoint, data = null, config = {}) =>
export const editRequest = (
id,
endpoint,
data = null,
axiosConfig = {}
) =>
request({
method: 'put',
endpoint,
suffix: `/edit/${id}`,
data,
...config,
axiosConfig
});
/**
* DELETE
* DELETE /endpoint/delete/:id
*/
export const deleteRequest = (id, endpoint, data = null, config = {}) =>
export const deleteRequest = (
id,
endpoint,
data = null,
axiosConfig = {}
) =>
request({
method: 'delete',
endpoint,
suffix: `/delete/${id}`,
data,
...config,
axiosConfig
});
/**
@ -110,8 +128,7 @@ export const deleteRequest = (id, endpoint, data = null, config = {}) =>
export const searchRequest = (
endpoint,
data = null,
params = {},
config = {},
axiosConfig = {}
) => {
const timezone = -new Date().getTimezoneOffset() / 60;
@ -120,37 +137,38 @@ export const searchRequest = (
endpoint,
suffix: '/search',
data,
params: {
tz: timezone,
...params,
},
...config,
axiosConfig: {
...axiosConfig,
params: {
tz: timezone,
...(axiosConfig.params || {})
}
}
});
};
/**
* CUSTOM REQUEST
* untuk endpoint bebas / beda sendiri
*/
export const customRequest = ({
method = 'get',
url = '',
data = null,
params = {},
headers = {},
axiosConfig = {},
isApiGo = false,
axiosConfig = {}
}) =>
request({
method,
customUrl: url,
data,
params,
headers,
axiosConfig,
isApiGo,
axiosConfig
});
/**
* Optional Export Object
* EXPORT OBJECT
*/
export const apiRequest = {
new: newRequest,
@ -159,5 +177,5 @@ export const apiRequest = {
edit: editRequest,
delete: deleteRequest,
search: searchRequest,
custom: customRequest,
custom: customRequest
};

View File

@ -0,0 +1,39 @@
import React, { createContext, useContext } from "react";
// Sentinel value to reliably detect if the hook is called outside the provider
const sentinel = {};
/**
* ApiContext created using the React Context API.
* Defaults to a sentinel value to detect out-of-provider usage.
*/
export const ApiContext = createContext(sentinel);
/**
* ApiProvider component that accepts a `headers` prop and provides it through context.
*
* @param {Object} props
* @param {Object} props.headers - The headers to provide to API requests
* @param {React.ReactNode} props.children - Child components
*/
export function ApiProvider({ headers, children }) {
return React.createElement(
ApiContext.Provider,
{ value: headers },
children
);
}
/**
* Custom hook `useApiHeaders()` that returns the current headers from context.
* Throws a clear error if called outside of `ApiProvider`.
*
* @returns {Object} The current headers from context
*/
export function useApiHeaders() {
const context = useContext(ApiContext);
if (context === sentinel) {
throw new Error("useApiHeaders must be used within an ApiProvider");
}
return context;
}

View File

@ -16,6 +16,7 @@ import {
import ClearIcon from "@mui/icons-material/Clear";
import FilterTableBuilder from "../utils/filterTableBuilder";
import { apiRequest } from "../api/request";
import { useApiHeaders } from "./ApiContext";
export default function ListVirtual({
id,
@ -33,8 +34,8 @@ export default function ListVirtual({
helperText,
filter,
isApiGo = false,
headersRequest,
}) {
const headersRequest = useApiHeaders();
const [open, setOpen] = useState(false);
const [search, setSearch] = useState("");
const [rows, setRows] = useState([]);
@ -65,18 +66,17 @@ export default function ListVirtual({
if (keyword) payload.like(name, keyword);
if (filter && filter.length > 0) {
filter.forEach(f => payload.where(f.logic_operator || "=", f.name, f.value, f.operator || "AND", f.value1 || null, f.table_name || null));
filter.forEach(f => payload.where(f.logic_operator || "=", f.name, f.value, f.operator || "AND", f.value1 || null, f.table_name || null));
}
try {
const res = await apiRequest.search(
url,
payload.build(),
{},
{
isApiGo,
headers: headersRequest
}
},
isApiGo,
);
const incomingData = res.data || [];
@ -125,14 +125,13 @@ export default function ListVirtual({
.equal("id", id);
try {
const res = await apiRequest.search(
url,
const res = await apiRequest.search(
url,
payload.build(),
{},
{
isApiGo,
headers: headersRequest
}
},
isApiGo,
);
const data = res.data?.[0];

2
src/components/index.js Normal file
View File

@ -0,0 +1,2 @@
export { default as ListVirtual } from "./ListVirtual";
export * from "./ApiContext";

View File

@ -1 +1,3 @@
export { default as ListVirtual } from './components/ListVirtual.jsx';
export * from './api/request';
export * from './components';
//export * from './utils';

77
src/main.jsx Normal file
View File

@ -0,0 +1,77 @@
// src/main.jsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import { ThemeProvider, createTheme } from '@mui/material/styles';
import { CssBaseline } from '@mui/material';
import App from './App'
const theme = createTheme({
palette: {
mode: 'light',
primary: {
main: '#2563eb'
},
background: {
default: '#f8fafc'
}
},
shape: {
borderRadius: 12
},
typography: {
fontFamily:
'"Inter", "Roboto", "Helvetica", "Arial", sans-serif',
h5: {
fontWeight: 700
},
button: {
textTransform: 'none',
fontWeight: 600
}
},
components: {
MuiPaper: {
styleOverrides: {
root: {
borderRadius: 16
}
}
},
MuiButton: {
styleOverrides: {
root: {
borderRadius: 10,
paddingInline: 16
}
}
},
MuiOutlinedInput: {
styleOverrides: {
root: {
borderRadius: 12
}
}
}
}
});
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<ThemeProvider theme={theme}>
<CssBaseline />
<App />
</ThemeProvider>
</React.StrictMode>
);