@fajarmaulana/komerce-lp-helper 0.4.13 → 0.4.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +412 -100
- package/dist/utils/api.js +12 -3
- package/dist/utils/cookie.js +1 -3
- package/dist/utils/local.d.ts +6 -0
- package/dist/utils/local.js +6 -0
- package/dist/utils/useApi.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,56 +10,72 @@ npm install @fajarmaulana/komerce-lp-helper
|
|
|
10
10
|
|
|
11
11
|
## Features
|
|
12
12
|
|
|
13
|
-
###
|
|
13
|
+
### Components
|
|
14
14
|
|
|
15
|
-
#### `
|
|
15
|
+
#### `Form`
|
|
16
16
|
|
|
17
|
-
A
|
|
17
|
+
A reusable form component that simplifies form submission handling by preventing default browser submit behavior,
|
|
18
|
+
collecting form values into a `FormData` object, and passing it to an action callback.
|
|
18
19
|
|
|
19
|
-
|
|
20
|
-
|
|
20
|
+
| Prop | Type | Description |
|
|
21
|
+
| ---------- | ------------------------------- | ------------------------------------------------- |
|
|
22
|
+
| `action` | `(formData: FormData) => void` | Function called when the form is submitted. |
|
|
23
|
+
| `ref` | `Ref<HTMLFormElement>` | Exposes the underlying `<form>` element. |
|
|
24
|
+
| `children` | `ReactNode` | The form’s inner content (inputs, buttons, etc.). |
|
|
25
|
+
| `...props` | `ComponentPropsWithRef<'form'>` | All standard HTML form attributes. |
|
|
21
26
|
|
|
22
|
-
|
|
23
|
-
const users = await http.get('/users')
|
|
27
|
+
**Usage:**
|
|
24
28
|
|
|
25
|
-
|
|
26
|
-
|
|
29
|
+
```tsx
|
|
30
|
+
import { Form } from '@fajarmaulana/komerce-lp-helper'
|
|
27
31
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
32
|
+
const handleSubmit = (formData: FormData) => {
|
|
33
|
+
console.log(formData.get('username'))
|
|
34
|
+
}
|
|
31
35
|
|
|
32
|
-
|
|
36
|
+
;<Form action={handleSubmit} className="my-form">
|
|
37
|
+
<input name="username" />
|
|
38
|
+
<button type="submit">Submit</button>
|
|
39
|
+
</Form>
|
|
40
|
+
```
|
|
33
41
|
|
|
34
|
-
|
|
35
|
-
fetching and mutations with progress tracking.
|
|
42
|
+
#### `LazyBackground`
|
|
36
43
|
|
|
37
|
-
|
|
38
|
-
|
|
44
|
+
A wrapper component that lazily loads a background image when it enters the viewport using the Intersection Observer
|
|
45
|
+
API.
|
|
39
46
|
|
|
40
|
-
|
|
47
|
+
| Prop | Type | Description |
|
|
48
|
+
| ----------- | ----------- | ----------------------------------------------------------- |
|
|
49
|
+
| `url` | `string` | The image URL to be lazily loaded as the background. |
|
|
50
|
+
| `children` | `ReactNode` | Optional elements or content rendered inside the container. |
|
|
51
|
+
| `className` | `string` | Additional CSS class names applied to the outer container. |
|
|
41
52
|
|
|
42
|
-
|
|
43
|
-
const { data, isLoading, refetch } = api.fetch<User[]>('/users')
|
|
53
|
+
**Usage:**
|
|
44
54
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
initialOffset: 0,
|
|
51
|
-
setOffset: (lastItems, allItems, lastOffset) => (lastItems.length ? lastOffset + 1 : null),
|
|
52
|
-
})
|
|
55
|
+
```tsx
|
|
56
|
+
import { LazyBackground } from '@fajarmaulana/komerce-lp-helper'
|
|
57
|
+
;<LazyBackground url="https://example.com/bg.jpg" className="h-64 w-full">
|
|
58
|
+
<h1>Content inside lazy background</h1>
|
|
59
|
+
</LazyBackground>
|
|
53
60
|
```
|
|
54
61
|
|
|
55
62
|
### Hooks
|
|
56
63
|
|
|
57
64
|
#### `useDebounce`
|
|
58
65
|
|
|
59
|
-
Returns a debounced version of a value.
|
|
66
|
+
Returns a debounced version of a value. It updates the returned value only after a specified delay has passed without
|
|
67
|
+
any changes to the input value.
|
|
60
68
|
|
|
61
|
-
|
|
62
|
-
|
|
69
|
+
| Parameter | Type | Default | Description |
|
|
70
|
+
| --------- | -------- | ---------- | ----------------------------------- |
|
|
71
|
+
| `value` | `T` | (Required) | The input value to debounce. |
|
|
72
|
+
| `delay` | `number` | `500` | The debounce delay in milliseconds. |
|
|
73
|
+
|
|
74
|
+
**Returns:** `T` - The debounced value.
|
|
75
|
+
|
|
76
|
+
**Usage:**
|
|
77
|
+
|
|
78
|
+
```ts
|
|
63
79
|
const debouncedSearchTerm = useDebounce(searchTerm, 500)
|
|
64
80
|
```
|
|
65
81
|
|
|
@@ -67,134 +83,430 @@ const debouncedSearchTerm = useDebounce(searchTerm, 500)
|
|
|
67
83
|
|
|
68
84
|
Returns a debounced version of a callback function.
|
|
69
85
|
|
|
70
|
-
|
|
71
|
-
|
|
86
|
+
| Parameter | Type | Default | Description |
|
|
87
|
+
| ---------- | -------------- | ---------- | ------------------------------------------------------ |
|
|
88
|
+
| `callback` | `T` (Function) | (Required) | The original function to debounce. |
|
|
89
|
+
| `delay` | `number` | (Required) | Delay in milliseconds before the callback is executed. |
|
|
90
|
+
|
|
91
|
+
**Returns:** `(...args: Parameters<T>) => void` - A debounced function.
|
|
92
|
+
|
|
93
|
+
**Usage:**
|
|
94
|
+
|
|
95
|
+
```ts
|
|
72
96
|
const debouncedSearch = useDebounceFunc(q => fetchResults(q), 300)
|
|
73
97
|
```
|
|
74
98
|
|
|
75
99
|
#### `useConditionalDebounce`
|
|
76
100
|
|
|
77
|
-
Conditionally executes a callback function after a specified debounce delay.
|
|
101
|
+
Conditionally executes a callback function after a specified debounce delay. Only triggers the callback if a given
|
|
102
|
+
condition is `true`.
|
|
78
103
|
|
|
79
|
-
|
|
104
|
+
| Parameter | Type | Default | Description |
|
|
105
|
+
| --------- | -------- | ------- | ----------------------------------- |
|
|
106
|
+
| `delay` | `number` | `500` | The debounce delay in milliseconds. |
|
|
80
107
|
|
|
81
|
-
|
|
108
|
+
**Returns:** `(condition: boolean, callback: () => void) => void` - The function to execute.
|
|
82
109
|
|
|
83
|
-
|
|
84
|
-
|
|
110
|
+
**Usage:**
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
const run = useConditionalDebounce(300)
|
|
114
|
+
run(isValid, () => submitData())
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
#### `useForm`
|
|
118
|
+
|
|
119
|
+
Manages form fields, retrieval of values, and error handling for both named inputs and standalone fields.
|
|
85
120
|
|
|
86
|
-
|
|
121
|
+
| Parameter | Type | Default | Description |
|
|
122
|
+
| ---------------------- | ---------- | ------- | ------------------------------------------------------ |
|
|
123
|
+
| `fieldsWithoutNameIds` | `string[]` | `[]` | List of field IDs that exist outside the form element. |
|
|
87
124
|
|
|
88
|
-
|
|
89
|
-
|
|
125
|
+
**Returns:**
|
|
126
|
+
|
|
127
|
+
| Property | Type | Description |
|
|
128
|
+
|---|---|---|
|
|
129
|
+
| `form` | `RefObject<HTMLFormElement>` | Ref to attach to the target `<form>`. |
|
|
130
|
+
| `fields` | `() => TFields<T>` | Function returning the values and elements of named form inputs. |
|
|
131
|
+
| `fieldsWithoutName` | `() => TFields<U>` | Function returning the values and elements of external inputs. |
|
|
132
|
+
|
|
133
|
+
_Note: `TFields` returns an object where keys are the field names and values are of type `TFieldItem<T>` containing
|
|
134
|
+
`field_value`, `field_id`, `field_error`, and `field_info`. Create a input component with small element with id
|
|
135
|
+
`{fieldName}_error` to show error message, and with id `{fieldName}_info` to show info message._
|
|
136
|
+
|
|
137
|
+
**Usage:**
|
|
138
|
+
|
|
139
|
+
```tsx
|
|
140
|
+
const { form, fields, fieldsWithoutName } = useForm<{ email: string }>(['custom-input-id'])
|
|
141
|
+
|
|
142
|
+
const handleSubmit = e => {
|
|
143
|
+
e.preventDefault()
|
|
144
|
+
const data = fields()
|
|
145
|
+
console.log(data.email.field_value)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
;<form ref={form} onSubmit={handleSubmit}>
|
|
149
|
+
<input name="email" id="email" />
|
|
150
|
+
<span id="email_error"></span>
|
|
151
|
+
</form>
|
|
90
152
|
```
|
|
91
153
|
|
|
154
|
+
#### `useRouter`
|
|
155
|
+
|
|
156
|
+
Custom router API built on top of React Router DOM that provides easier navigation methods and state management.
|
|
157
|
+
|
|
158
|
+
**Returns:**
|
|
159
|
+
|
|
160
|
+
| Property | Type | Description |
|
|
161
|
+
|---|---|---|
|
|
162
|
+
| `push` | `IRouterPush` | Navigate to a new page (string or object with pathname, query, hash). |
|
|
163
|
+
| `route` | `(pathname, query?, options?) => void` | Navigate to a new page with query parameters. |
|
|
164
|
+
| `replace` | `IRouterPush` | Replace the current history entry. |
|
|
165
|
+
| `back` | `() => void` | Go back to previous page. |
|
|
166
|
+
| `next` | `() => void` | Go forward to next page. |
|
|
167
|
+
| `go` | `(num: number) => void` | Jump to specific history index. |
|
|
168
|
+
| `refresh` | `() => void` | Reload the current page. |
|
|
169
|
+
| `path` | `string` | The current pathname without query/hash. |
|
|
170
|
+
| `hash` | `string` | The hash portion of current URL. |
|
|
171
|
+
| `fullpath` | `string` | Full path including query and hash. |
|
|
172
|
+
| `origin` | `string` | The base URL. |
|
|
173
|
+
| `href` | `string` | Full URL including origin. |
|
|
174
|
+
| `query` | `TRouterQuery` | Parsed query parameters as object. |
|
|
175
|
+
| `params` | `Record<string, string>` | Current route parameters. |
|
|
176
|
+
| `beforePopState` | `(cb) => void` | Hook to cancel navigation if callback returns false. |
|
|
177
|
+
|
|
92
178
|
#### `useQueryParams`
|
|
93
179
|
|
|
94
180
|
A hook for reading and updating query parameters in the URL locally.
|
|
95
181
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
182
|
+
| Parameter | Type | Default | Description |
|
|
183
|
+
| --------------- | ------------ | ----------- | --------------------------------------------- |
|
|
184
|
+
| `defaultValues` | `Partial<T>` | `undefined` | Optional default values for query parameters. |
|
|
185
|
+
|
|
186
|
+
**Returns:** `[T, (updates: Partial<T>) => void]` - Tuple containing query object and updater function.
|
|
187
|
+
|
|
188
|
+
#### `useSectionObserver`
|
|
189
|
+
|
|
190
|
+
Uses the Intersection Observer API to detect when a trigger element comes into view and updates the `data-active`
|
|
191
|
+
attribute of a target element.
|
|
192
|
+
|
|
193
|
+
| Parameter | Type | Default | Description |
|
|
194
|
+
| ------------ | ------------------------ | ---------- | --------------------------------------------------- |
|
|
195
|
+
| `triggerRef` | `RefObject<HTMLElement>` | (Required) | Ref pointing to the element triggering observation. |
|
|
196
|
+
| `targetId` | `string` | (Required) | ID of the target element to toggle `data-active`. |
|
|
197
|
+
| `threshold` | `number` | `0.8` | Visibility threshold (0 to 1). |
|
|
100
198
|
|
|
101
199
|
#### `useSlider`
|
|
102
200
|
|
|
103
201
|
Manages logic for custom slider components, including touch/drag support and navigation.
|
|
104
202
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
203
|
+
| Parameter | Type | Default | Description |
|
|
204
|
+
| --------------- | ------------ | ----------- | ----------------------------------------------------- |
|
|
205
|
+
| `data` | `any[]` | (Required) | Array of data to display. |
|
|
206
|
+
| `mobileOnly` | `boolean` | `false` | If true, slider only works below `mobileBound` width. |
|
|
207
|
+
| `infiniteSlide` | `boolean` | `false` | Allow infinite sliding. |
|
|
208
|
+
| `isLoading` | `boolean` | `false` | Is data still loading. |
|
|
209
|
+
| `mobileBound` | `number` | `640` | Screen width bound for mobile only mode. |
|
|
210
|
+
| `onNext` | `() => void` | `undefined` | Override the next function. |
|
|
211
|
+
| `onBack` | `() => void` | `undefined` | Override the back function. |
|
|
212
|
+
|
|
213
|
+
**Returns:**
|
|
214
|
+
|
|
215
|
+
| Property | Type | Description |
|
|
216
|
+
|---|---|---|
|
|
217
|
+
| `currentSlide` | `number` | The current active slide index. |
|
|
218
|
+
| `movement` | `number` | Current drag movement value. |
|
|
219
|
+
| `grab` | `boolean` | Whether the slider is currently being grabbed/dragged. |
|
|
220
|
+
| `disableLeftArrow` | `boolean` | Whether left arrow should be disabled. |
|
|
221
|
+
| `disableRightArrow`| `boolean` | Whether right arrow should be disabled. |
|
|
222
|
+
| `setCurrentSlide` | `(val) => void` | Manually set the slide. |
|
|
223
|
+
| `startSlide` | `(param) => void` | Call on mouse down / touch start. |
|
|
224
|
+
| `moveSlide` | `(param) => void` | Call on mouse move / touch move. |
|
|
225
|
+
| `endSlide` | `(param) => void` | Call on mouse up / touch end. |
|
|
226
|
+
| `next`, `back` | `() => void` | Navigate to next/prev slide. |
|
|
227
|
+
|
|
228
|
+
### Utilities
|
|
229
|
+
|
|
230
|
+
#### HTTP Client & API
|
|
231
|
+
|
|
232
|
+
**`http`** A robust wrapper around `fetch` with caching, retry, and interceptors.
|
|
233
|
+
|
|
234
|
+
| Method | Parameters | Returns | Description |
|
|
235
|
+
| -------------------------------- | --------------------------------------------------- | -------------------------- | ----------------------------- |
|
|
236
|
+
| `get` | `url: string`, `config?: Omit<THttpConfig, 'body'>` | `Promise<TApiResponse<T>>` | Performs a GET request. |
|
|
237
|
+
| `post`, `put`, `patch`, `delete` | `url: string`, `body?: U`, `config?: THttpConfig` | `Promise<TApiResponse<T>>` | Performs a mutation request. |
|
|
238
|
+
| `request` | `config: TApiConfig` | `Promise<TApiResponse<T>>` | Full configuration request. |
|
|
239
|
+
| `getCache` | `key: string` | `T \| undefined` | Retrieves cached data. |
|
|
240
|
+
| `setCache` | `key: string, data: T, ttl?: number` | `void` | Manually store data in cache. |
|
|
241
|
+
| `removeCache` | `key: string` | `void` | Remove specific cache entry. |
|
|
242
|
+
| `clearCache` | | `void` | Clears all cache. |
|
|
243
|
+
| `create` | `options?: TApiInstanceOptions` | `IApiInstance` | Creates a new instance. |
|
|
244
|
+
|
|
245
|
+
**`createApi(options)`** Creates a new API instance with built-in React hooks for data fetching, mutation, and infinite
|
|
246
|
+
pagination. Returns an object with the following hooks:
|
|
247
|
+
|
|
248
|
+
**1. `fetch<T>(url, config?, enabled?)`**
|
|
249
|
+
|
|
250
|
+
| Parameter | Type | Default | Description |
|
|
251
|
+
|---|---|---|---|
|
|
252
|
+
| `url` | `string` | (Required) | The endpoint URL. Changing this automatically triggers a new request. |
|
|
253
|
+
| `config` | `Omit<THttpConfig, 'onUpload' \| 'onDownload'>` | `undefined` | Optional HTTP configuration (headers, params, cache, etc.). |
|
|
254
|
+
| `enabled` | `boolean` | `true` | Whether the fetch should run automatically on mount or when URL changes. |
|
|
255
|
+
|
|
256
|
+
**Returns:**
|
|
257
|
+
| Property | Type | Description |
|
|
258
|
+
|---|---|---|
|
|
259
|
+
| `data` | `T \| null` | Fetched data. |
|
|
260
|
+
| `error` | `unknown` | Any encountered error. |
|
|
261
|
+
| `isLoading`| `boolean` | Request state. |
|
|
262
|
+
| `cacheKey` | `string \| null` | Identifier for cache. |
|
|
263
|
+
| `refetch` | `() => Promise<TFetchState<T>>` | Manually trigger a fresh fetch. |
|
|
264
|
+
| `setData` | `(data) => void` | Manually update state data. |
|
|
265
|
+
|
|
266
|
+
**Example:**
|
|
267
|
+
```tsx
|
|
268
|
+
const { data, isLoading, refetch } = api.fetch<User[]>('/users', { params: { status: 'active' } })
|
|
108
269
|
```
|
|
109
270
|
|
|
110
|
-
|
|
271
|
+
**2. `mutation<TData, TRequest>(url, config?)`**
|
|
111
272
|
|
|
112
|
-
|
|
273
|
+
| Parameter | Type | Default | Description |
|
|
274
|
+
|---|---|---|---|
|
|
275
|
+
| `url` | `string` | (Required) | Endpoint URL for the mutation. |
|
|
276
|
+
| `config` | `TMutationOptions` | `undefined` | Optional mutation config (method, headers, progress, etc.). |
|
|
113
277
|
|
|
114
|
-
|
|
115
|
-
|
|
278
|
+
**Returns:**
|
|
279
|
+
| Property | Type | Description |
|
|
280
|
+
|---|---|---|
|
|
281
|
+
| `mutate` | `(request?: TRequest) => Promise<TApiResponse<TData>>` | Trigger the mutation. |
|
|
282
|
+
| `isLoading`| `boolean` | In progress state. |
|
|
283
|
+
| `progress` | `TProgress \| null` | Upload/Download progress. |
|
|
116
284
|
|
|
117
|
-
|
|
285
|
+
**Example:**
|
|
286
|
+
```tsx
|
|
287
|
+
const { mutate, isLoading, progress } = api.mutation<User, FormData>('/users/upload', {
|
|
288
|
+
method: 'POST',
|
|
289
|
+
progress: 'upload'
|
|
290
|
+
})
|
|
118
291
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
const data = fields()
|
|
122
|
-
console.log(data.email.field_value)
|
|
123
|
-
}
|
|
292
|
+
// To trigger:
|
|
293
|
+
// await mutate(formData)
|
|
124
294
|
```
|
|
125
295
|
|
|
126
|
-
|
|
296
|
+
**3. `infinite<T, TOffset>(url, options, config?, enabled?)`**
|
|
127
297
|
|
|
128
|
-
|
|
298
|
+
| Parameter | Type | Default | Description |
|
|
299
|
+
|---|---|---|---|
|
|
300
|
+
| `url` | `string` | (Required) | Endpoint URL. Changing this resets items and fetches from the beginning. |
|
|
301
|
+
| `options` | `TInfiniteFetchOptions` | (Required) | Options containing `initialOffset`, `offsetKey`, and `setOffset` logic. |
|
|
302
|
+
| `config` | `Omit<THttpConfig, 'onUpload' \| 'onDownload'>` | `undefined` | Optional HTTP configuration. |
|
|
303
|
+
| `enabled` | `boolean` | `true` | Whether the fetch should run automatically. |
|
|
129
304
|
|
|
130
|
-
|
|
131
|
-
|
|
305
|
+
**Returns:**
|
|
306
|
+
| Property | Type | Description |
|
|
307
|
+
|---|---|---|
|
|
308
|
+
| `data` | `T \| null` | Aggregated items. |
|
|
309
|
+
| `fetchNextPage`| `() => Promise<void>` | Trigger fetch for next offset. |
|
|
310
|
+
| `hasNextPage`| `boolean` | More pages available. |
|
|
311
|
+
| `isFetchingNextPage`| `boolean` | Fetching next page state. |
|
|
132
312
|
|
|
133
|
-
|
|
134
|
-
|
|
313
|
+
**Example:**
|
|
314
|
+
```tsx
|
|
315
|
+
const { data, fetchNextPage, hasNextPage } = api.infinite<User[], number>(
|
|
316
|
+
'/users',
|
|
317
|
+
{
|
|
318
|
+
initialOffset: 0,
|
|
319
|
+
offsetKey: 'page',
|
|
320
|
+
setOffset: (lastItems, allItems, lastOffset) => {
|
|
321
|
+
return lastItems.length > 0 ? lastOffset + 1 : null
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
)
|
|
135
325
|
```
|
|
136
326
|
|
|
137
|
-
|
|
327
|
+
**4. `batch<T>(initialRequests?)`**
|
|
328
|
+
Execute multiple API requests in parallel.
|
|
329
|
+
|
|
330
|
+
| Parameter | Type | Default | Description |
|
|
331
|
+
|---|---|---|---|
|
|
332
|
+
| `initialRequests` | `{ [K in keyof T]: string \| TApiConfig }` | `undefined` | Array of URLs or full request configurations. |
|
|
333
|
+
|
|
334
|
+
**Returns:**
|
|
335
|
+
| Property | Type | Description |
|
|
336
|
+
|---|---|---|
|
|
337
|
+
| `mutate` | `(overrideRequests?) => Promise<TBatchResponse<T>>` | Trigger the batch execution. |
|
|
338
|
+
| `isLoading`| `boolean` | Whether any request in the batch is in progress. |
|
|
339
|
+
| `error` | `unknown` | The first error encountered. |
|
|
340
|
+
| `cacheKeys`| `(string \| null)[]` | Array of cache keys for each request. |
|
|
341
|
+
|
|
342
|
+
**Example:**
|
|
343
|
+
```tsx
|
|
344
|
+
const { mutate, isLoading } = api.batch<[User[], Post[]]>([
|
|
345
|
+
'/users',
|
|
346
|
+
{ url: '/posts', method: 'GET' }
|
|
347
|
+
])
|
|
348
|
+
|
|
349
|
+
// To trigger:
|
|
350
|
+
// const [usersResponse, postsResponse] = await mutate()
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
#### Interceptor Setup Example
|
|
354
|
+
|
|
355
|
+
You can set up custom interceptors on an API instance to attach tokens globally, or handle specific error codes:
|
|
356
|
+
|
|
357
|
+
```typescript
|
|
358
|
+
import { ApiMeta, createApi, getCookie, type TApiConfig } from '@fajarmaulana/komerce-lp-helper'
|
|
359
|
+
|
|
360
|
+
import { INTERNAL_API } from '@/constants/env'
|
|
361
|
+
|
|
362
|
+
import { COOKIE_KEY_JWT, forceLogout } from './auth'
|
|
363
|
+
|
|
364
|
+
const PUBLIC_ENDPOINTS: Record<string, string[]> = {
|
|
365
|
+
[INTERNAL_API.baseURL!]: ['/auth/api/v1/live-chat/verify-token'],
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const isPublicEndpoint = (path: string, baseUrl: string) => {
|
|
369
|
+
const publicPaths = PUBLIC_ENDPOINTS[baseUrl]
|
|
370
|
+
return publicPaths && publicPaths.some(publicPath => path.startsWith(publicPath))
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const interceptor = {
|
|
374
|
+
request: (config: TApiConfig) => {
|
|
375
|
+
if (!config.baseURL || isPublicEndpoint(config.url, config.baseURL)) return config
|
|
376
|
+
|
|
377
|
+
const token = getCookie<string>(COOKIE_KEY_JWT)
|
|
378
|
+
if (!token) {
|
|
379
|
+
forceLogout()
|
|
380
|
+
throw new Error('Token expired or missing')
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
return {
|
|
384
|
+
...config,
|
|
385
|
+
headers: {
|
|
386
|
+
...config.headers,
|
|
387
|
+
Authorization: `Bearer ${token}`,
|
|
388
|
+
},
|
|
389
|
+
}
|
|
390
|
+
},
|
|
391
|
+
error: async (error: unknown) => {
|
|
392
|
+
if (error instanceof ApiMeta && [401, 403].includes(error.code)) forceLogout()
|
|
393
|
+
|
|
394
|
+
const isNetworkError =
|
|
395
|
+
error instanceof Error &&
|
|
396
|
+
(error.message === 'Network Error' || (error as { code?: string }).code === 'ERR_NETWORK')
|
|
397
|
+
|
|
398
|
+
if (isNetworkError && window.location.pathname !== '/error-network') {
|
|
399
|
+
window.location.replace('/error-network')
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
return Promise.reject(error)
|
|
403
|
+
},
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const internalApi = createApi({ ...INTERNAL_API })
|
|
407
|
+
internalApi.setInterceptors(interceptor)
|
|
408
|
+
|
|
409
|
+
export { internalApi }
|
|
410
|
+
```
|
|
138
411
|
|
|
139
412
|
#### Cookie
|
|
140
413
|
|
|
141
414
|
Managed wrappers for `document.cookie`.
|
|
142
415
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
416
|
+
| Function | Parameters | Description |
|
|
417
|
+
| --------------- | ------------------------------------ | ----------------------------------------- |
|
|
418
|
+
| `setCookie` | `{ key, value, maxAge?: number, sameSite?: Lax/Strict/None }` | Sets a cookie. Value is JSON stringified. |
|
|
419
|
+
| `setCookies` | `items: TCookie[]` | Sets multiple cookies. |
|
|
420
|
+
| `getCookie` | `key: string` | Gets and parses a cookie value. |
|
|
421
|
+
| `getCookies` | `keys: string[]` | Retrieves multiple cookies as an object. |
|
|
422
|
+
| `removeCookie` | `key: string` | Removes a cookie. |
|
|
423
|
+
| `removeCookies` | `keys: string[]` | Removes multiple cookies. |
|
|
424
|
+
| `clearCookies` | | Clears all cookies. |
|
|
150
425
|
|
|
151
426
|
#### Local Storage
|
|
152
427
|
|
|
153
428
|
Type-safe wrappers for `localStorage`.
|
|
154
429
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
430
|
+
| Function | Parameters | Description |
|
|
431
|
+
| -------------- | ------------------------- | --------------------------------------- |
|
|
432
|
+
| `setLocal` | `key: string, value: T` | Stores a value (JSON stringified). |
|
|
433
|
+
| `setLocals` | `items: { key, value }[]` | Stores multiple values. |
|
|
434
|
+
| `getLocal` | `key: string` | Retrieves and parses a value. |
|
|
435
|
+
| `getLocals` | `keys: string[]` | Retrieves multiple values as an object. |
|
|
436
|
+
| `removeLocal` | `key: string` | Removes an item. |
|
|
437
|
+
| `removeLocals` | `keys: string[]` | Removes multiple items. |
|
|
438
|
+
| `clearLocals` | | Clears local storage. |
|
|
161
439
|
|
|
162
440
|
#### File
|
|
163
441
|
|
|
164
442
|
Helpers for file and blob manipulation.
|
|
165
443
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
444
|
+
| Function | Parameters | Returns | Description |
|
|
445
|
+
| ----------------------- | -------------------------------------------- | --------------------- | ------------------------------------------------------- |
|
|
446
|
+
| `checkImage` | `url: string` | `Promise<string>` | Checks if an image URL is valid (returns URL or empty). |
|
|
447
|
+
| `convertBlob` | `blob: Blob` | `Promise<string>` | Converts a Blob to a Base64 string. |
|
|
448
|
+
| `getExtension` | `mimeType: string` | `string` | Gets file extension from MIME type. |
|
|
449
|
+
| `filenameWithExtension` | `blob: Blob, prefix?: string` | `string` | Generates a filename. |
|
|
450
|
+
| `createDownloadAnchor` | `blob: Blob, options?: { filename, target }` | `{ anchor, blobUrl }` | Creates an anchor for downloading. |
|
|
451
|
+
| `downloadBlob` | `blob: Blob, filename: string` | `void` | Triggers a file download. |
|
|
172
452
|
|
|
173
453
|
#### General
|
|
174
454
|
|
|
175
455
|
Common DOM and string utilities.
|
|
176
456
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
457
|
+
| Function | Parameters | Returns | Description |
|
|
458
|
+
| ---------------- | ------------------------------------------ | ---------------- | ------------------------------------------- |
|
|
459
|
+
| `getById` | `id: string` | `HTMLElement` | Type-safe `document.getElementById`. |
|
|
460
|
+
| `getByAny` | `selector: string` | `HTMLElement` | Type-safe `document.querySelector`. |
|
|
461
|
+
| `clickById` | `id: string` | `void` | Triggers a click on an element by ID. |
|
|
462
|
+
| `focusById` | `id: string` | `void` | Focuses an element by ID. |
|
|
463
|
+
| `handleHashLink` | `hash: string, currentHash: string, func?` | `void` | Smooth scrolling for hash links. |
|
|
464
|
+
| `acronym` | `name: string` | `string` | Generates a 1-2 letter acronym from a name. |
|
|
465
|
+
| `isNotPrimitive` | `value: unknown` | `boolean` | Checks if a value is an object/array. |
|
|
466
|
+
| `toWA` | `message: string, options?: { urlOnly?: boolean; phoneNumber?: string }` | `string \| void` | Get WhatsApp URL or open in new window. |
|
|
184
467
|
|
|
185
|
-
#### Form Validation
|
|
468
|
+
#### Form Validation (Error Provider)
|
|
186
469
|
|
|
187
|
-
Helpers for validating inputs and displaying error messages.
|
|
470
|
+
Helpers for validating inputs and displaying error messages dynamically.
|
|
188
471
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
numbers, symbols).
|
|
472
|
+
**`provideFieldError(params)`** Validates a field against a set of rules, updates the target error element, and modifies
|
|
473
|
+
the target `<label>` border color.
|
|
192
474
|
|
|
193
|
-
|
|
475
|
+
- `params`: `{ field_id, field_value, field_error, rules }`
|
|
476
|
+
- `rules`: A mapping of error messages to their boolean validation results (`true` = invalid).
|
|
477
|
+
|
|
478
|
+
**Example Rules:**
|
|
479
|
+
|
|
480
|
+
```typescript
|
|
481
|
+
export const VALID_EMAIL = {
|
|
482
|
+
re: /^(?![.])[A-Za-z0-9._-]+(?<![.])@[A-Za-z0-9-]+(\.[A-Za-z0-9-]+)*\.[A-Za-z]{2,4}$/,
|
|
483
|
+
text: 'masukkan email yang valid',
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
export const ALPHASPACE = {
|
|
487
|
+
re: /^[A-Za-z]+(?: [A-Za-z]+)*$/,
|
|
488
|
+
text: ' hanya boleh berisi huruf dan spasi antar kata',
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
export const REGISTER_RULES = {
|
|
492
|
+
emailRules: (email: string) => ({
|
|
493
|
+
'email harus diisi': !email,
|
|
494
|
+
[VALID_EMAIL.text]: !VALID_EMAIL.re.test(email),
|
|
495
|
+
}),
|
|
496
|
+
fullnameRules: (fullname: string) => ({
|
|
497
|
+
'nama harus diisi': !fullname,
|
|
498
|
+
'panjang nama minimal adalah 3 karakter': fullname.length < 3,
|
|
499
|
+
[`nama ${ALPHASPACE.text}`]: !ALPHASPACE.re.test(fullname),
|
|
500
|
+
'panjang nama maksimal adalah 40 karakter': fullname.length > 40,
|
|
501
|
+
}),
|
|
502
|
+
}
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
**`providePasswordFieldError(params)`** Specialized validation for password strength (length, uppercase, lowercase,
|
|
506
|
+
numbers, symbols, no spaces).
|
|
194
507
|
|
|
195
|
-
- `
|
|
196
|
-
- `LazyBackground`: Component for lazy loading background images.
|
|
508
|
+
- `params`: `{ field_id, field_value, field_error }`
|
|
197
509
|
|
|
198
510
|
## License
|
|
199
511
|
|
|
200
|
-
MIT
|
|
512
|
+
MIT
|
package/dist/utils/api.js
CHANGED
|
@@ -43,8 +43,10 @@ export class ApiInstance {
|
|
|
43
43
|
}
|
|
44
44
|
buildcacheKey(config) {
|
|
45
45
|
const keyObj = {
|
|
46
|
+
baseURL: config.baseURL,
|
|
46
47
|
url: config.url,
|
|
47
48
|
params: config.params,
|
|
49
|
+
method: config.method || 'GET',
|
|
48
50
|
};
|
|
49
51
|
const keyString = JSON.stringify(keyObj);
|
|
50
52
|
if (keyString.length > 200) {
|
|
@@ -221,7 +223,15 @@ export class ApiInstance {
|
|
|
221
223
|
}
|
|
222
224
|
async request(config) {
|
|
223
225
|
try {
|
|
224
|
-
const
|
|
226
|
+
const mergedConfig = {
|
|
227
|
+
baseURL: this.baseURL,
|
|
228
|
+
...config,
|
|
229
|
+
headers: {
|
|
230
|
+
...this.defaultHeaders,
|
|
231
|
+
...config.headers,
|
|
232
|
+
},
|
|
233
|
+
};
|
|
234
|
+
const finalConfig = await this.handleInterceptors(mergedConfig);
|
|
225
235
|
const cacheKey = this.buildcacheKey(finalConfig);
|
|
226
236
|
if (finalConfig.cache && finalConfig.cache.enabled && finalConfig.method === 'GET') {
|
|
227
237
|
const cached = this.getCacheEntry(cacheKey);
|
|
@@ -233,14 +243,13 @@ export class ApiInstance {
|
|
|
233
243
|
this.removeCacheEntry(cacheKey);
|
|
234
244
|
}
|
|
235
245
|
}
|
|
236
|
-
const url =
|
|
246
|
+
const url = finalConfig.baseURL ? `${finalConfig.baseURL}${finalConfig.url}` : finalConfig.url;
|
|
237
247
|
const finalURL = buildURL(url, finalConfig.params);
|
|
238
248
|
const isFormData = finalConfig.body instanceof FormData;
|
|
239
249
|
const response = await this.fetchWithRetry(finalURL, {
|
|
240
250
|
method: finalConfig.method || 'GET',
|
|
241
251
|
headers: {
|
|
242
252
|
...(isFormData ? {} : { 'Content-Type': 'application/json' }),
|
|
243
|
-
...this.defaultHeaders,
|
|
244
253
|
...finalConfig.headers,
|
|
245
254
|
},
|
|
246
255
|
body: isFormData ? finalConfig.body : finalConfig.body ? JSON.stringify(finalConfig.body) : undefined,
|
package/dist/utils/cookie.js
CHANGED
|
@@ -61,9 +61,7 @@ export const removeCookie = (key) => {
|
|
|
61
61
|
*
|
|
62
62
|
* @param keys - Array of keys to remove.
|
|
63
63
|
*/
|
|
64
|
-
export const removeCookies = (keys) =>
|
|
65
|
-
keys.forEach(key => removeCookie(key));
|
|
66
|
-
};
|
|
64
|
+
export const removeCookies = (keys) => keys.forEach(key => removeCookie(key));
|
|
67
65
|
/**
|
|
68
66
|
* Clears all cookies by setting their max age to 0.
|
|
69
67
|
*/
|
package/dist/utils/local.d.ts
CHANGED
|
@@ -8,6 +8,12 @@ export declare const clearLocals: () => void;
|
|
|
8
8
|
* @param key - The key of the item to remove.
|
|
9
9
|
*/
|
|
10
10
|
export declare const removeLocal: (key: string) => void;
|
|
11
|
+
/**
|
|
12
|
+
* Removes multiple localStorage items by keys.
|
|
13
|
+
*
|
|
14
|
+
* @param keys - Array of keys to remove.
|
|
15
|
+
*/
|
|
16
|
+
export declare const removeLocals: (keys: string[]) => void;
|
|
11
17
|
/**
|
|
12
18
|
* Sets an item in localStorage after serializing it to JSON.
|
|
13
19
|
*
|
package/dist/utils/local.js
CHANGED
|
@@ -8,6 +8,12 @@ export const clearLocals = () => localStorage.clear();
|
|
|
8
8
|
* @param key - The key of the item to remove.
|
|
9
9
|
*/
|
|
10
10
|
export const removeLocal = (key) => localStorage.removeItem(key);
|
|
11
|
+
/**
|
|
12
|
+
* Removes multiple localStorage items by keys.
|
|
13
|
+
*
|
|
14
|
+
* @param keys - Array of keys to remove.
|
|
15
|
+
*/
|
|
16
|
+
export const removeLocals = (keys) => keys.forEach(key => removeLocal(key));
|
|
11
17
|
/**
|
|
12
18
|
* Sets an item in localStorage after serializing it to JSON.
|
|
13
19
|
*
|
package/dist/utils/useApi.js
CHANGED
|
@@ -193,7 +193,7 @@ export default function createApi(options = {}) {
|
|
|
193
193
|
onDownload: shouldTrackDownload
|
|
194
194
|
? progress => {
|
|
195
195
|
if (isMountedRef.current) {
|
|
196
|
-
setState(s => ({ ...s, progress
|
|
196
|
+
setState(s => ({ ...s, progress }));
|
|
197
197
|
}
|
|
198
198
|
}
|
|
199
199
|
: undefined,
|