kmod-cli 1.4.15 → 1.6.0

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.
@@ -0,0 +1,60 @@
1
+ import React from 'react';
2
+
3
+ interface ListProps<T> {
4
+ items: T[];
5
+ keyExtractor: (item: T) => React.Key;
6
+ renderItem: (item: T, index: number) => React.ReactNode;
7
+ }
8
+
9
+ export function List<T>({ items, keyExtractor, renderItem }: ListProps<T>) {
10
+ return (
11
+ <>
12
+ {items.map((item, index) => (
13
+ <ListItem
14
+ key={keyExtractor(item)}
15
+ item={item}
16
+ index={index}
17
+ renderItem={renderItem}
18
+ />
19
+ ))}
20
+ </>
21
+ );
22
+ }
23
+
24
+ interface ListItemProps<T> {
25
+ item: T;
26
+ index: number;
27
+ renderItem: (item: T, index: number) => React.ReactNode;
28
+ }
29
+
30
+ // Generic memo component
31
+ function _ListItem<T>({ item, index, renderItem }: ListItemProps<T>) {
32
+ return <>{renderItem(item, index)}</>;
33
+ }
34
+
35
+ const ListItem = React.memo(_ListItem) as <T>(
36
+ props: ListItemProps<T>
37
+ ) => React.ReactNode;
38
+
39
+
40
+ // Example usage of the List component
41
+ // interface User {
42
+ // id: number;
43
+ // name: string;
44
+ // }
45
+
46
+ // const users: User[] = [
47
+ // { id: 1, name: 'Alice' },
48
+ // { id: 2, name: 'Bob' },
49
+ // { id: 3, name: 'Charlie' },
50
+ // ];
51
+
52
+ // export function UserList() {
53
+ // return (
54
+ // <List
55
+ // items={users}
56
+ // keyExtractor={(user) => user.id}
57
+ // renderItem={(user) => <div>{user.name}</div>}
58
+ // />
59
+ // );
60
+ // }
@@ -8,7 +8,6 @@ import {
8
8
  useState,
9
9
  } from 'react';
10
10
 
11
- // Nếu bạn cần alias cho ITable type, dùng:
12
11
  import type { Table as ITable } from '@tanstack/react-table';
13
12
  import {
14
13
  Cell,
@@ -55,7 +54,7 @@ export type TableClassNames = {
55
54
  };
56
55
  export type TableHeaderProps<TData> =
57
56
  HTMLAttributes<HTMLTableSectionElement> & {
58
- handleClick: ({
57
+ handleClick?: ({
59
58
  e,
60
59
  table,
61
60
  }: {
@@ -64,7 +63,7 @@ export type TableHeaderProps<TData> =
64
63
  }) => void;
65
64
  };
66
65
  export type TableBodyProps<TData> = HTMLAttributes<HTMLTableSectionElement> & {
67
- handleClick: ({
66
+ handleClick?: ({
68
67
  e,
69
68
  table,
70
69
  }: {
@@ -73,7 +72,8 @@ export type TableBodyProps<TData> = HTMLAttributes<HTMLTableSectionElement> & {
73
72
  }) => void;
74
73
  };
75
74
  export type TableHeadProps<TData> = HTMLAttributes<HTMLTableCellElement> & {
76
- handleClick: ({
75
+ classNameCondition?: | (({cell, table}: {cell?: Header<TData, unknown>; table?: ITable<TData>}) => string) | string;
76
+ handleClick?: ({
77
77
  e,
78
78
  table,
79
79
  cell,
@@ -85,7 +85,8 @@ export type TableHeadProps<TData> = HTMLAttributes<HTMLTableCellElement> & {
85
85
  };
86
86
  export type TableCellProps<TData, TValue> =
87
87
  HTMLAttributes<HTMLTableCellElement> & {
88
- handleClick: ({
88
+ classNameCondition?: | (({cell, table}: {cell?: Cell<TData, unknown>; table?: ITable<TData>}) => string) | string;
89
+ handleClick?: ({
89
90
  e,
90
91
  table,
91
92
  cell,
@@ -96,7 +97,7 @@ export type TableCellProps<TData, TValue> =
96
97
  }) => void;
97
98
  };
98
99
  export type TableRowHeadProps<TData> = HTMLAttributes<HTMLTableRowElement> & {
99
- handleClick: ({
100
+ handleClick?: ({
100
101
  e,
101
102
  table,
102
103
  row,
@@ -107,7 +108,8 @@ export type TableRowHeadProps<TData> = HTMLAttributes<HTMLTableRowElement> & {
107
108
  }) => void;
108
109
  };
109
110
  export type TableRowBodyProps<TData> = HTMLAttributes<HTMLTableRowElement> & {
110
- handleClick: ({
111
+ classNameCondition?: | (({row, table}: {row?: Row<TData>; table?: ITable<TData>}) => string) | string;
112
+ handleClick?: ({
111
113
  e,
112
114
  table,
113
115
  row,
@@ -118,7 +120,7 @@ export type TableRowBodyProps<TData> = HTMLAttributes<HTMLTableRowElement> & {
118
120
  }) => void;
119
121
  };
120
122
  export type TableProps<TData> = HTMLAttributes<HTMLTableElement> & {
121
- handleClick: ({
123
+ handleClick?: ({
122
124
  e,
123
125
  table,
124
126
  }: {
@@ -173,6 +175,15 @@ export type DataTableProps<TData, TValue> = {
173
175
  // handles?: Handles
174
176
  };
175
177
 
178
+ export interface UseTablePropsFn<TData, TValue> {
179
+ table: ITable<TData>;
180
+ row?: Row<TData>;
181
+ cell?: Cell<TData, TValue>;
182
+ header?: Header<TData, TValue>;
183
+ headerGroup?: HeaderGroup<TData>;
184
+ }
185
+
186
+
176
187
  export type DataTableToolbarFns<TData> = {
177
188
  globalFilter: string;
178
189
  setGlobalFilter: (value: string) => void;
@@ -204,7 +215,7 @@ export function DataTable<TData, TValue>({
204
215
  useTableProps,
205
216
  initialState,
206
217
  alternate = "even",
207
- alternateColor = "#f5f5f5",
218
+ alternateColor = "#fbfbfb",
208
219
  // handles
209
220
  }: DataTableProps<TData, TValue>) {
210
221
  const table = useReactTable({
@@ -241,47 +252,74 @@ export function DataTable<TData, TValue>({
241
252
  };
242
253
 
243
254
  const {
244
- handleClick: tableHandleClick,
245
- onClick: tableOnClick,
246
- ...tableDomProps
247
- } = useTableProps?.tableProps || {};
255
+ handleClick: tableHandleClick,
256
+ onClick: tableOnClick,
257
+ ...tableDomProps
258
+ } = useTableProps?.tableProps || {};
248
259
 
249
- const {
250
- handleClick: headerHandleClick,
251
- onClick: headerOnClick,
252
- ...headerDomProps
253
- } = useTableProps?.headerProps || {};
254
- const {
255
- handleClick: rowHeadHandleClick,
256
- onClick: rowHeadOnClick,
257
- ...rowHeadDomProps
258
- } = useTableProps?.rowHeadProps || {};
259
- const {
260
- handleClick: bodyHandleClick,
261
- onClick: bodyOnClick,
262
- ...bodyDomProps
263
- } = useTableProps?.bodyProps || {};
264
- const {
265
- handleClick: rowBodyHandleClick,
266
- onClick: rowBodyOnClick,
267
- style: rowBodyStyle,
268
- ...rowBodyDomProps
269
- } = useTableProps?.rowBodyProps || {};
270
- const {
271
- handleClick: cellBodyHandleClick,
272
- onClick: cellBodyOnClick,
273
- ...cellBodyDomProps
274
- } = useTableProps?.cellBodyProps || {};
260
+ const {
261
+ handleClick: headerHandleClick,
262
+ onClick: headerOnClick,
263
+ ...headerDomProps
264
+ } = useTableProps?.headerProps || {};
265
+ const {
266
+ handleClick: rowHeadHandleClick,
267
+ onClick: rowHeadOnClick,
268
+ ...rowHeadDomProps
269
+ } = useTableProps?.rowHeadProps || {};
270
+ const {
271
+ handleClick: cellHeadHandleClick,
272
+ onClick: cellHeadOnClick,
273
+ classNameCondition: cellHeadClassNameCondition,
274
+ ...cellHeadDomProps
275
+ } = useTableProps?.cellHeadProps || {};
276
+ const {
277
+ handleClick: bodyHandleClick,
278
+ onClick: bodyOnClick,
279
+ ...bodyDomProps
280
+ } = useTableProps?.bodyProps || {};
281
+ const {
282
+ handleClick: rowBodyHandleClick,
283
+ onClick: rowBodyOnClick,
284
+ style: rowBodyStyle,
285
+ classNameCondition: rowBodyClassNameCondition,
286
+ ...rowBodyDomProps
287
+ } = useTableProps?.rowBodyProps || {};
288
+ const {
289
+ handleClick: cellBodyHandleClick,
290
+ onClick: cellBodyOnClick,
291
+ classNameCondition: cellBodyClassNameCondition,
292
+ ...cellBodyDomProps
293
+ } = useTableProps?.cellBodyProps || {};
275
294
 
276
- const {
277
- handleClick: skRowHandleClick,
278
- ...skRowDomProps
279
- } = useTableProps?.rowBodyProps || {};
295
+ const { handleClick: skRowHandleClick, classNameCondition: skRowClassNameCondition, ...skRowDomProps } =
296
+ useTableProps?.rowBodyProps || {};
280
297
 
281
- const {
282
- handleClick: skCellHandleClick,
283
- ...skCellDomProps
284
- } = useTableProps?.cellBodyProps || {};
298
+ const { handleClick: skCellHandleClick, classNameCondition: skCellClassNameCondition, ...skCellDomProps } =
299
+ useTableProps?.cellBodyProps || {};
300
+
301
+
302
+ function getCellHeadClassNameByCondition({cell,table}: {cell?: Header<TData, unknown>; table?: ITable<TData>}) {
303
+ if(!cell || !table) return "";
304
+ const classNameCondition = useTableProps?.cellHeadProps?.classNameCondition;
305
+ if(!classNameCondition) return "";
306
+ if(typeof classNameCondition === "string") return classNameCondition;
307
+ return classNameCondition({cell,table});
308
+ }
309
+ function getCellBodyClassNameByCondition({cell,table}: {cell?: Cell<TData, unknown>; table?: ITable<TData>}) {
310
+ if(!cell || !table) return "";
311
+ const classNameCondition = useTableProps?.cellBodyProps?.classNameCondition;
312
+ if(!classNameCondition) return "";
313
+ if(typeof classNameCondition === "string") return classNameCondition;
314
+ return classNameCondition({cell,table});
315
+ }
316
+ function getRowBodyClassNameByCondition({row,table}: {row?: Row<TData>; table?: ITable<TData>}) {
317
+ if(!row || !table) return "";
318
+ const classNameCondition = useTableProps?.rowBodyProps?.classNameCondition;
319
+ if(!classNameCondition) return "";
320
+ if(typeof classNameCondition === "string") return classNameCondition;
321
+ return classNameCondition({row,table});
322
+ }
285
323
 
286
324
 
287
325
  return (
@@ -321,11 +359,7 @@ const {
321
359
  {headerGroup.headers.map((header) => (
322
360
  <TableHead
323
361
  key={header.id}
324
- {...(() => {
325
- const { handleClick, onClick, ...rest } =
326
- useTableProps?.cellHeadProps || {};
327
- return rest;
328
- })()}
362
+ {...cellHeadDomProps}
329
363
  className={cn(
330
364
  "cursor-pointer select-none",
331
365
  classNames?.header?.head
@@ -334,22 +368,11 @@ const {
334
368
  width: header.getSize() ? `${header.getSize()}px !important` : "auto",
335
369
  }}
336
370
  onClick={(e) => {
337
- // Just call the parent's onClick if provided
338
- if (useTableProps?.cellHeadProps?.onClick) {
339
- useTableProps.cellHeadProps.onClick(e);
340
- }
341
-
342
- // Just call the parent's handleClick if provided
343
- if (useTableProps?.cellHeadProps?.handleClick) {
344
- useTableProps.cellHeadProps.handleClick({
345
- e,
346
- cell: header,
347
- table,
348
- });
349
- }
371
+ cellHeadOnClick?.(e);
372
+ cellHeadHandleClick?.({ e, table, cell: header });
350
373
  }}
351
374
  >
352
- <div className={cn("flex items-center gap-1 w-fit", classNames?.header?.content)}>
375
+ <div className={cn("flex items-center gap-1 w-fit", classNames?.header?.content, getCellHeadClassNameByCondition({cell: header, table}))}>
353
376
  {flexRender(
354
377
  header.column.columnDef.header,
355
378
  header.getContext()
@@ -384,15 +407,13 @@ const {
384
407
  {!isLoading &&
385
408
  table.getRowModel().rows.length > 0 &&
386
409
  table.getRowModel().rows.map((row, index) => {
387
- const { handleClick, onClick, ...rest } =
388
- useTableProps?.rowBodyProps || {};
389
410
 
390
411
  return (
391
412
  <TableRow
392
413
  {...rowBodyDomProps}
393
414
  key={row.id}
394
415
  style={{...rowBodyStyle, backgroundColor: getAlternateColor(index)}}
395
- className={cn(classNames?.body?.row)}
416
+ className={cn(classNames?.body?.row, getRowBodyClassNameByCondition({row,table}))}
396
417
  data-state={row.getIsSelected() && "selected"}
397
418
  onClick={(e) => {
398
419
  rowBodyOnClick?.(e);
@@ -403,7 +424,7 @@ const {
403
424
  <TableCell
404
425
  {...cellBodyDomProps}
405
426
  key={cell.id}
406
- className={cn(classNames?.body?.cell)}
427
+ className={cn(classNames?.body?.cell, getCellBodyClassNameByCondition({cell, table}))}
407
428
  onClick={(e) => {
408
429
  cellBodyOnClick?.(e);
409
430
  cellBodyHandleClick?.({ e, cell, table });
@@ -480,26 +501,27 @@ export const TableSkeleton = <TData, TValue>({
480
501
  const {
481
502
  handleClick: _rowHandleClick,
482
503
  onClick: _rowOnClick,
504
+ classNameCondition: rowClassNameCondition,
483
505
  ...rowDomProps
484
506
  } = props?.rowBodyProps || {};
485
507
 
486
508
  const {
487
509
  handleClick: _cellHandleClick,
488
510
  onClick: _cellOnClick,
511
+ classNameCondition: cellClassNameCondition,
489
512
  ...cellDomProps
490
513
  } = props?.cellBodyProps || {};
491
514
 
492
-
493
515
  if (showNoData) {
494
516
  return (
495
517
  <TableRow
496
518
  key="no-data-skeleton"
497
- className={cn(classNames?.body?.row)}
519
+ className={cn(classNames?.body?.row, )}
498
520
  {...rowDomProps}
499
521
  >
500
522
  <TableCell
501
523
  colSpan={columns.length}
502
- className={cn("h-24 text-center", classNames?.body?.cell)}
524
+ className={cn("h-24 text-center")}
503
525
  {...cellDomProps}
504
526
  >
505
527
  {emptyLabel}
@@ -1,95 +1,200 @@
1
1
  "use client";
2
2
 
3
- import { useState } from "react";
3
+ import React from 'react';
4
4
 
5
- import { ColumnDef } from "@tanstack/react-table";
5
+ import { ColumnDef } from '@tanstack/react-table';
6
6
 
7
- import { DataTable } from "./data-table";
7
+ import { cn } from '../../lib/utils';
8
+ import {
9
+ DataTable,
10
+ UseTableProps,
11
+ } from './data-table'; // chỉnh path cho đúng
8
12
 
13
+ /* ======================================================
14
+ * 1. Data type
15
+ * ====================================================== */
9
16
  type User = {
10
17
  id: string;
11
18
  name: string;
12
19
  email: string;
20
+ role: "admin" | "user";
21
+ status: "active" | "inactive";
13
22
  };
14
23
 
15
- // 1. Columns
24
+ /* ======================================================
25
+ * 2. Mock data
26
+ * ====================================================== */
27
+ const USERS: User[] = [
28
+ {
29
+ id: "1",
30
+ name: "John Doe",
31
+ email: "john@example.com",
32
+ role: "admin",
33
+ status: "active",
34
+ },
35
+ {
36
+ id: "2",
37
+ name: "Jane Smith",
38
+ email: "jane@example.com",
39
+ role: "user",
40
+ status: "inactive",
41
+ },
42
+ {
43
+ id: "3",
44
+ name: "Alex Johnson",
45
+ email: "alex@example.com",
46
+ role: "user",
47
+ status: "active",
48
+ },
49
+ ];
50
+
51
+ /* ======================================================
52
+ * 3. Columns
53
+ * ====================================================== */
16
54
  const columns: ColumnDef<User>[] = [
17
55
  {
18
56
  accessorKey: "name",
19
57
  header: "Name",
20
- cell: ({ row }) => <div className="font-medium">{row.getValue("name")}</div>,
21
- enableSorting: true,
58
+ cell: ({ row }) => (
59
+ <span className="font-medium">{row.original.name}</span>
60
+ ),
22
61
  },
23
62
  {
24
63
  accessorKey: "email",
25
64
  header: "Email",
26
- enableSorting: true,
65
+ cell: ({ getValue }) => (
66
+ <span className="text-blue-600">{getValue<string>()}</span>
67
+ ),
68
+ },
69
+ {
70
+ accessorKey: "role",
71
+ header: "Role",
72
+ cell: ({ getValue }) => (
73
+ <span className="capitalize">{getValue<string>()}</span>
74
+ ),
75
+ },
76
+ {
77
+ accessorKey: "status",
78
+ header: "Status",
79
+ cell: ({ getValue }) => {
80
+ const value = getValue<string>();
81
+ return (
82
+ <span
83
+ className={cn(
84
+ "px-2 py-0.5 rounded text-xs font-medium",
85
+ value === "active"
86
+ ? "bg-green-100 text-green-700"
87
+ : "bg-red-100 text-red-700"
88
+ )}
89
+ >
90
+ {value}
91
+ </span>
92
+ );
93
+ },
27
94
  },
28
95
  ];
29
96
 
30
- // 2. Dummy Data
31
- const users: User[] = [
32
- { id: "1", name: "Nguyễn Văn A", email: "a@example.com" },
33
- { id: "2", name: "Trần Thị B", email: "b@example.com" },
34
- { id: "3", name: "Lê Văn C", email: "c@example.com" },
35
- { id: "4", name: "Phạm Thị D", email: "d@example.com" },
36
- ];
97
+ /* ======================================================
98
+ * 4. Toolbar
99
+ * ====================================================== */
100
+ const Toolbar = ({ fns }: any) => {
101
+ return (
102
+ <div className="flex items-center gap-2">
103
+ <input
104
+ value={fns.globalFilter ?? ""}
105
+ onChange={(e) => fns.setGlobalFilter(e.target.value)}
106
+ placeholder="Search..."
107
+ className="border rounded px-2 py-1 text-sm"
108
+ />
109
+ </div>
110
+ );
111
+ };
37
112
 
38
- // 3. Component chính
39
- export default function UserTableExample() {
40
- const [data, setData] = useState<User[]>(users);
41
- const [isLoading, setIsLoading] = useState(false);
113
+ /* ======================================================
114
+ * 5. Pagination
115
+ * ====================================================== */
116
+ const Pagination = ({ fns }: any) => {
117
+ return (
118
+ <div className="flex items-center justify-end gap-2">
119
+ <button
120
+ onClick={fns.previousPage}
121
+ disabled={!fns.getCanPreviousPage()}
122
+ className="px-3 py-1 border rounded disabled:opacity-50"
123
+ >
124
+ Prev
125
+ </button>
126
+
127
+ <span className="text-sm">
128
+ Page {fns.pageIndex + 1}
129
+ </span>
130
+
131
+ <button
132
+ onClick={fns.nextPage}
133
+ disabled={!fns.getCanNextPage()}
134
+ className="px-3 py-1 border rounded disabled:opacity-50"
135
+ >
136
+ Next
137
+ </button>
138
+ </div>
139
+ );
140
+ };
42
141
 
142
+ /* ======================================================
143
+ * 6. useTableProps (row / cell behavior)
144
+ * ====================================================== */
145
+ const useTableProps: UseTableProps<User, unknown> = {
146
+ rowBodyProps: {
147
+ classNameCondition: ({ row }) =>
148
+ row?.original.status === "inactive"
149
+ ? "opacity-60"
150
+ : "",
151
+ handleClick: ({ row }) => {
152
+ console.log("Row clicked:", row.original);
153
+ },
154
+ },
155
+ cellBodyProps: {
156
+ classNameCondition: ({ cell }) =>
157
+ cell?.column.id === "email"
158
+ ? "underline"
159
+ : "",
160
+ },
161
+ };
162
+
163
+ /* ======================================================
164
+ * 7. Final Component
165
+ * ====================================================== */
166
+ export default function UserTableExample() {
43
167
  return (
44
- <DataTable
45
- data={data}
46
- columns={columns}
47
- isLoading={isLoading}
48
- emptyLabel="Không có dữ liệu nào"
49
- classNames={{
50
- wrapper: "p-4",
51
- table: "bg-transparent",
52
- header: {
53
- header: "bg-muted",
54
- row: "border-b",
55
- head: "text-left text-sm font-semibold",
56
- },
57
- body: {
58
- body: "bg-transparent",
59
- row: "hover:bg-muted/30",
60
- cell: "text-sm px-2 py-3",
61
- },
62
- }}
63
- toolbarTable={({ table, fns }) => (
64
- <div className="flex items-center gap-2">
65
- <input
66
- type="text"
67
- placeholder="Tìm kiếm..."
68
- value={fns.globalFilter ?? ""}
69
- onChange={(e) => fns.setGlobalFilter(e.target.value)}
70
- className="px-3 py-2 border rounded-md w-64"
71
- />
72
- </div>
73
- )}
74
- paginationTable={({ fns }) => (
75
- <div className="flex items-center justify-end gap-4">
76
- <button
77
- onClick={fns.previousPage}
78
- disabled={!fns.getCanPreviousPage()}
79
- className="px-3 py-1 rounded-md bg-gray-200 disabled:opacity-50 dark:bg-gray-700 dark:text-white"
80
- >
81
- Trang trước
82
- </button>
83
- <span>Trang {fns.pageIndex + 1}</span>
84
- <button
85
- onClick={fns.nextPage}
86
- disabled={!fns.getCanNextPage()}
87
- className="px-3 py-1 rounded-md bg-gray-200 disabled:opacity-50 dark:bg-gray-700 dark:text-white"
88
- >
89
- Trang sau
90
- </button>
91
- </div>
92
- )}
93
- />
168
+ <div className="p-4 space-y-4">
169
+ <h1 className="text-lg font-semibold">User Table</h1>
170
+
171
+ <DataTable<User, unknown>
172
+ data={USERS}
173
+ columns={columns}
174
+ enableSort
175
+ alternate="even"
176
+ alternateColor="#f9fafb"
177
+ emptyLabel="No users found"
178
+ initialState={{
179
+ pagination: {
180
+ pageSize: 5,
181
+ pageIndex: 0,
182
+ },
183
+ }}
184
+ toolbarTable={({ fns }) => <Toolbar fns={fns} />}
185
+ paginationTable={({ fns }) => <Pagination fns={fns} />}
186
+ useTableProps={useTableProps}
187
+ classNames={{
188
+ table: "border rounded-md",
189
+ header: {
190
+ head: "bg-gray-50 text-sm font-semibold",
191
+ },
192
+ body: {
193
+ row: "hover:bg-gray-50 cursor-pointer",
194
+ cell: "text-sm",
195
+ },
196
+ }}
197
+ />
198
+ </div>
94
199
  );
95
200
  }
@@ -994,33 +994,70 @@ export const useDataProvider = () => {
994
994
 
995
995
  /**
996
996
  * Create HTTP client with authentication
997
+ * @param url Base URL for the HTTP client
998
+ * @param options Configuration options for the HTTP client
999
+ * @param options.tokenName Name of the token to use for authentication
1000
+ * @param options.tokenStorage Storage mechanism for the token
1001
+ * @param options.authorizationType Type of authorization (e.g., Bearer, Basic)
1002
+ * @param options.withCredentials Whether to send cookies with requests (defaults to true if tokenStorage is "http-only")
1003
+ * @returns Configured Axios instance
997
1004
  */
998
- export function createHttpClient(
999
- baseURL: string,
1000
- authTokenKey: string = 'token',
1001
- authTokenStorage: 'localStorage' | 'sessionStorage' | 'cookie' = 'cookie',
1002
- typeAuthorization: "Bearer" | "Basic" | string = "Bearer"
1003
- ): AxiosInstance {
1005
+
1006
+ export interface ICreateHttpClientOptions {
1007
+ tokenName?: string ;
1008
+ tokenStorage?: "local" | "session" | "cookie" | "http-only";
1009
+ authorizationType?: "Bearer" | "Basic" | string;
1010
+ withCredentials?: boolean;
1011
+ }
1012
+
1013
+ export interface ICreateHttpClient {
1014
+ url?: string;
1015
+ options?: ICreateHttpClientOptions
1016
+ }
1017
+ export function createHttpClient({url, options = {}}:ICreateHttpClient): AxiosInstance {
1018
+ const {
1019
+ tokenName = "token",
1020
+ tokenStorage = "http-only",
1021
+ authorizationType = "Bearer",
1022
+ } = options;
1023
+
1024
+ const withCredentials =
1025
+ options.withCredentials ?? tokenStorage === "http-only";
1026
+
1027
+
1004
1028
  const axiosInstance = axios.create({
1005
- baseURL: baseURL || 'https://api.example.com',
1029
+ baseURL: url || "https://api.example.com",
1030
+ withCredentials,
1006
1031
  });
1007
1032
 
1008
- axiosInstance.interceptors.request.use(config => {
1033
+ axiosInstance.interceptors.request.use((config) => {
1009
1034
  let token: string | null = null;
1010
-
1011
- if (authTokenStorage === 'localStorage') {
1012
- token = localStorage.getItem(authTokenKey);
1013
- } else if (authTokenStorage === 'sessionStorage') {
1014
- token = sessionStorage.getItem(authTokenKey);
1015
- } else if (authTokenStorage === 'cookie') {
1016
- const match = document.cookie.match(new RegExp('(^| )' + authTokenKey + '=([^;]+)'));
1017
- if (match) token = match[2];
1035
+
1036
+ switch (options.tokenStorage) {
1037
+ case "local":
1038
+ token = localStorage.getItem(tokenName);
1039
+ break;
1040
+
1041
+ case "session":
1042
+ token = sessionStorage.getItem(tokenName);
1043
+ break;
1044
+
1045
+ case "cookie":
1046
+ token = cookiesProvider.get(tokenName) ?? null;
1047
+ break;
1048
+
1049
+ case "http-only":
1050
+ // NO READ
1051
+ // HttpOnly cookies are not accessible via JavaScript
1052
+ // Browser will send it automatically
1053
+ break;
1018
1054
  }
1019
-
1055
+
1056
+ // Just set token if available in storage
1020
1057
  if (token) {
1021
- config.headers.Authorization = `${typeAuthorization} ${token}`;
1022
- }
1023
-
1058
+ config.headers.Authorization = `${authorizationType} ${token}`;
1059
+ }
1060
+
1024
1061
  return config;
1025
1062
  });
1026
1063
 
@@ -1050,15 +1087,36 @@ interface AuthContextValue {
1050
1087
  isAuthenticated: () => boolean;
1051
1088
  setUser: (user: AuthUser | null) => void;
1052
1089
  login: (payload: LoginPayload, type?: "full" | "simple") => Promise<any>;
1053
- logout: () => void;
1090
+ logout: (params: CustomParams, type?: TypeResponse) => Promise<any>;
1054
1091
  getMe: (type?: "full" | "simple") => Promise<any>;
1055
1092
  }
1056
1093
 
1094
+ /**
1095
+ * Authentication Provider Urls Props
1096
+ * @param loginUrl - URL for login API
1097
+ * @param logoutUrl - URL for logout API
1098
+ * @param meUrl - URL for fetching current user info
1099
+ * @returns AuthProviderUrls
1100
+ */
1101
+ export interface AuthProviderUrls {
1102
+ loginUrl: string;
1103
+ logoutUrl?: string;
1104
+ meUrl?: string;
1105
+ }
1106
+
1107
+ /**
1108
+ * Authentication Provider Props
1109
+ * @param children - React children nodes
1110
+ * @param urls - AuthProviderUrls
1111
+ * @param tokenKey - Key for token storage
1112
+ * @param keysCleanUpOnLogout - Additional keys to clean up on logout
1113
+ * @returns AuthProviderProps
1114
+ */
1057
1115
  export interface AuthProviderProps {
1058
1116
  children: React.ReactNode;
1059
- loginUrl: string;
1060
- meUrl: string;
1117
+ urls: AuthProviderUrls;
1061
1118
  tokenKey: string;
1119
+ keysCleanUpOnLogout?: string[] | string;
1062
1120
  }
1063
1121
 
1064
1122
  export type TypeResponse = "full" | "simple";
@@ -1075,13 +1133,21 @@ export const useAuth = () => {
1075
1133
 
1076
1134
  export const AuthProvider: React.FC<AuthProviderProps> = ({
1077
1135
  children,
1078
- loginUrl = '/auth/login',
1079
- meUrl = '/auth/me',
1136
+ urls,
1080
1137
  tokenKey = 'token',
1138
+ keysCleanUpOnLogout = ["token"],
1081
1139
  }) => {
1082
1140
  const dataProvider = useDataProvider();
1083
1141
  const [user, setUser] = useState<AuthUser | null>(null);
1084
1142
 
1143
+ const { loginUrl, meUrl, logoutUrl } = urls;
1144
+
1145
+ function removeKeys(key: string) {
1146
+ cookiesProvider.remove(key);
1147
+ localStorage.removeItem(key);
1148
+ sessionStorage.removeItem(key);
1149
+ }
1150
+
1085
1151
  const login = useCallback(async (payload: LoginPayload, type: TypeResponse = "full") => {
1086
1152
  try {
1087
1153
  const res = await dataProvider.custom<any>({
@@ -1100,15 +1166,37 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({
1100
1166
  }
1101
1167
  , [dataProvider, loginUrl]);
1102
1168
 
1103
- const logout = useCallback(() => {
1104
- cookiesProvider.remove(tokenKey);
1105
- localStorage.clear();
1106
- sessionStorage.clear();
1107
- dataProvider.clearAllCache();
1108
- }, [dataProvider]);
1169
+ const logout = useCallback(async (params: CustomParams, type: TypeResponse = "full") => {
1170
+
1171
+ try {
1172
+ const res = await dataProvider.custom<any>({
1173
+ url: params.url || logoutUrl,
1174
+ ...params,
1175
+ });
1176
+
1177
+ if (type === "simple") {
1178
+ return res.data;
1179
+ }
1180
+
1181
+ return res;
1182
+ } catch (error) {
1183
+ throw error;
1184
+ } finally {
1185
+ setUser(null);
1186
+ dataProvider.clearAllCache();
1187
+ if(Array.isArray(keysCleanUpOnLogout)){
1188
+ keysCleanUpOnLogout.forEach((key) => {
1189
+ removeKeys(key);
1190
+ });
1191
+ } else {
1192
+ removeKeys(keysCleanUpOnLogout);
1193
+ }
1194
+ }
1195
+
1196
+ }, [dataProvider, logoutUrl]);
1109
1197
 
1110
1198
  const isAuthenticated = () => {
1111
- return (cookiesProvider.get(tokenKey) !== null && cookiesProvider.get(tokenKey) !== undefined && cookiesProvider.get(tokenKey) !== "" && typeof cookiesProvider.get(tokenKey) === "string" && user !== null) ? true : false;
1199
+ return user !== null;
1112
1200
  };
1113
1201
  const getToken = useCallback(() => {
1114
1202
  return cookiesProvider.get(tokenKey);
@@ -1128,12 +1216,17 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({
1128
1216
  } catch {
1129
1217
  return null;
1130
1218
  }
1131
- }, [dataProvider, meUrl, logout]);
1219
+ }, [dataProvider, meUrl]);
1220
+
1221
+ useEffect(() => {
1222
+ getMe().then((u: any) => u && setUser(u));
1223
+ }, [getMe]);
1224
+
1132
1225
 
1133
1226
  const value: AuthContextValue = {
1134
1227
  user,
1135
1228
  setUser,
1136
- token: getToken(),
1229
+ token: getToken() ?? null,
1137
1230
  isAuthenticated: isAuthenticated,
1138
1231
  login,
1139
1232
  logout,
@@ -1171,28 +1264,39 @@ export const cookiesProvider = {
1171
1264
 
1172
1265
  // =================== Example ===================
1173
1266
 
1174
- // create httpClient
1267
+ // create httpClient - (can create multiple httpClients for different apis)
1175
1268
 
1176
1269
  // const TOKEN = "token";
1177
1270
 
1178
- // const httpClient = createHttpClient(
1179
- // `${process.env.NEXT_PUBLIC_API_URL}`,
1180
- // TOKEN, --> key_name_cookie
1181
- // "cookie", --> storage
1182
- // "Bearer" --> prefix
1183
- // );
1271
+ // const httpClient = createHttpClient({
1272
+ // url: `${process.env.NEXT_PUBLIC_API_URL}`,
1273
+ // options: {
1274
+ // authorizationType: "Bearer",
1275
+ // tokenName: TOKEN,
1276
+ // tokenStorage: "cookie",
1277
+ // withCredentials: true, --- optionals (default to true if tokenStorage is "http-only")
1278
+ // },
1279
+ // });
1184
1280
 
1185
1281
 
1186
1282
  // create dataProvider
1187
1283
 
1188
1284
  // const dataProvider = useDataProvider(httpClient);
1189
1285
 
1286
+ // const urls = {
1287
+ // loginUrl: "/auth/login", --> api_login
1288
+ // logoutUrl: "/auth/logout", --> api_logout
1289
+ // meUrl: "/auth/me", --> api_get_me_by_token
1290
+ // }
1291
+
1292
+ // const keysRemoveOnLogout = [TOKEN, "refreshToken", "user"];
1293
+
1190
1294
  // wrapped all into:
1191
1295
  // <DataProvider dataProvider={dataProvider}>
1192
1296
  // <AuthProvider
1193
- // loginUrl={"/auth/login"} --> api_login
1297
+ // urls={urls} --> api_login
1194
1298
  // tokenKey={TOKEN}
1195
- // meUrl='/auth/me' --> api_get_me_by_token
1299
+ // keysCleanUpOnLogout={keysRemoveOnLogout} --> optional (default to ["token"]) - additional keys to clean up on logout
1196
1300
  // >
1197
1301
  // <App />
1198
1302
  // </AuthProvider>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kmod-cli",
3
- "version": "1.4.15",
3
+ "version": "1.6.0",
4
4
  "description": "Stack components utilities fast setup in projects",
5
5
  "author": "kumo_d",
6
6
  "license": "MIT",