kmod-cli 1.0.10

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.
Files changed (66) hide show
  1. package/README.md +53 -0
  2. package/bin/gen-components.js +68 -0
  3. package/bin/index.js +153 -0
  4. package/component-templates/components/access-denied.tsx +130 -0
  5. package/component-templates/components/breadcumb.tsx +42 -0
  6. package/component-templates/components/count-down.tsx +94 -0
  7. package/component-templates/components/count-input.tsx +221 -0
  8. package/component-templates/components/date-range-calendar/button.tsx +61 -0
  9. package/component-templates/components/date-range-calendar/calendar.tsx +132 -0
  10. package/component-templates/components/date-range-calendar/date-input.tsx +259 -0
  11. package/component-templates/components/date-range-calendar/date-range-picker.tsx +594 -0
  12. package/component-templates/components/date-range-calendar/label.tsx +31 -0
  13. package/component-templates/components/date-range-calendar/popover.tsx +32 -0
  14. package/component-templates/components/date-range-calendar/select.tsx +125 -0
  15. package/component-templates/components/date-range-calendar/switch.tsx +30 -0
  16. package/component-templates/components/datetime-picker/button.tsx +61 -0
  17. package/component-templates/components/datetime-picker/calendar.tsx +156 -0
  18. package/component-templates/components/datetime-picker/datetime-picker.tsx +75 -0
  19. package/component-templates/components/datetime-picker/input.tsx +20 -0
  20. package/component-templates/components/datetime-picker/label.tsx +18 -0
  21. package/component-templates/components/datetime-picker/period-input.tsx +62 -0
  22. package/component-templates/components/datetime-picker/popover.tsx +32 -0
  23. package/component-templates/components/datetime-picker/select.tsx +125 -0
  24. package/component-templates/components/datetime-picker/time-picker-input.tsx +131 -0
  25. package/component-templates/components/datetime-picker/time-picker-utils.tsx +204 -0
  26. package/component-templates/components/datetime-picker/time-picker.tsx +59 -0
  27. package/component-templates/components/gradient-outline.tsx +233 -0
  28. package/component-templates/components/gradient-svg.tsx +157 -0
  29. package/component-templates/components/grid-layout.tsx +69 -0
  30. package/component-templates/components/hydrate-guard.tsx +40 -0
  31. package/component-templates/components/image.tsx +92 -0
  32. package/component-templates/components/loader-slash-gradient.tsx +85 -0
  33. package/component-templates/components/masonry-gallery.tsx +221 -0
  34. package/component-templates/components/modal.tsx +110 -0
  35. package/component-templates/components/multi-select.tsx +447 -0
  36. package/component-templates/components/non-hydration.tsx +27 -0
  37. package/component-templates/components/portal.tsx +34 -0
  38. package/component-templates/components/segments-circle.tsx +235 -0
  39. package/component-templates/components/single-select.tsx +248 -0
  40. package/component-templates/components/stroke-circle.tsx +57 -0
  41. package/component-templates/components/table/column-table.tsx +15 -0
  42. package/component-templates/components/table/data-table.tsx +339 -0
  43. package/component-templates/components/table/readme.tsx +95 -0
  44. package/component-templates/components/table/table.tsx +60 -0
  45. package/component-templates/components/text-hover-effect.tsx +120 -0
  46. package/component-templates/components/timout-loader.tsx +52 -0
  47. package/component-templates/components/toast.tsx +994 -0
  48. package/component-templates/configs/config.ts +33 -0
  49. package/component-templates/configs/feature-config.tsx +432 -0
  50. package/component-templates/configs/keys.ts +7 -0
  51. package/component-templates/core/api-service.ts +202 -0
  52. package/component-templates/core/calculate.ts +18 -0
  53. package/component-templates/core/idb.ts +166 -0
  54. package/component-templates/core/storage.ts +213 -0
  55. package/component-templates/hooks/count-down.ts +38 -0
  56. package/component-templates/hooks/fade-on-scroll.ts +52 -0
  57. package/component-templates/hooks/safe-action.ts +59 -0
  58. package/component-templates/hooks/spam-guard.ts +31 -0
  59. package/component-templates/lib/utils.ts +6 -0
  60. package/component-templates/providers/feature-guard.tsx +432 -0
  61. package/component-templates/queries/query.tsx +775 -0
  62. package/component-templates/utils/colors/color-by-text.ts +307 -0
  63. package/component-templates/utils/colors/stripe-effect.ts +100 -0
  64. package/component-templates/utils/hash/hash-aes.ts +35 -0
  65. package/components.json +348 -0
  66. package/package.json +60 -0
@@ -0,0 +1,339 @@
1
+ "use client";
2
+
3
+ import {
4
+ HTMLAttributes,
5
+ ReactNode,
6
+ useEffect,
7
+ useRef,
8
+ useState,
9
+ } from 'react';
10
+
11
+ import {
12
+ ColumnDef,
13
+ flexRender,
14
+ getCoreRowModel,
15
+ getFilteredRowModel,
16
+ getPaginationRowModel,
17
+ useReactTable,
18
+ } from '@tanstack/react-table';
19
+ import {
20
+ Cell,
21
+ getSortedRowModel,
22
+ Header,
23
+ HeaderGroup,
24
+ InitialTableState,
25
+ Row,
26
+ Table as ITable,
27
+ } from '@tanstack/table-core';
28
+
29
+ import { cn } from '../../lib/utils';
30
+ import {
31
+ Table,
32
+ TableBody,
33
+ TableCell,
34
+ TableHead,
35
+ TableHeader,
36
+ TableRow,
37
+ } from './table';
38
+
39
+ export type TableHeaderClassNames = {
40
+ header?: string;
41
+ row?: string;
42
+ head?: string;
43
+ };
44
+ export type TableBodyClassNames = {
45
+ body?: string;
46
+ row?: string;
47
+ cell?: string;
48
+ };
49
+ export type TableClassNames = {
50
+ wrapper?: string;
51
+ container?: string;
52
+ table?: string;
53
+ header?: TableHeaderClassNames;
54
+ body?: TableBodyClassNames;
55
+ };
56
+ export type TableHeaderProps<TData> = HTMLAttributes<HTMLTableSectionElement> & {
57
+ handleClick: ({ e, table }: { e: React.MouseEvent<HTMLTableSectionElement>; table: ITable<TData> }) => void;
58
+
59
+ };
60
+ export type TableBodyProps<TData> = HTMLAttributes<HTMLTableSectionElement> & {
61
+ handleClick: ({ e, table }: { e: React.MouseEvent<HTMLTableSectionElement>; table: ITable<TData> }) => void;
62
+ };
63
+ export type TableHeadProps<TData> = HTMLAttributes<HTMLTableCellElement> & {
64
+ handleClick: ({
65
+ e,
66
+ table,
67
+ cell,
68
+ }: {
69
+ e: React.MouseEvent<HTMLTableCellElement>;
70
+ cell: Header<TData, unknown>;
71
+ table: ITable<TData>;
72
+ }) => void;
73
+ };
74
+ export type TableCellProps<TData, TValue> = HTMLAttributes<HTMLTableCellElement> & {
75
+ handleClick: ({ e, table, cell }: { e: React.MouseEvent<HTMLTableCellElement>; cell: Cell<TData, TValue>; table: ITable<TData> }) => void;
76
+ };
77
+ export type TableRowHeadProps<TData> = HTMLAttributes<HTMLTableRowElement> & {
78
+ handleClick: ({ e, table, row }: { e: React.MouseEvent<HTMLTableRowElement>; row: HeaderGroup<TData>; table: ITable<TData> }) => void;
79
+ };
80
+ export type TableRowBodyProps<TData> = HTMLAttributes<HTMLTableRowElement> & {
81
+ handleClick: ({ e, table, row }: { e: React.MouseEvent<HTMLTableRowElement>; row: Row<TData>; table: ITable<TData> }) => void;
82
+ };
83
+ export type TableProps<TData> = HTMLAttributes<HTMLTableElement> & {
84
+ handleClick: ({ e, table }: { e: React.MouseEvent<HTMLTableElement>; table: ITable<TData> }) => void;
85
+ };
86
+
87
+ export type UseTableProps<TData, TValue> = {
88
+ tableProps?: TableProps<TData>;
89
+ headerProps?: TableHeaderProps<TData>;
90
+ bodyProps?: TableBodyProps<TData>;
91
+ cellBodyProps?: TableCellProps<TData, TValue>;
92
+ rowHeadProps?: TableRowHeadProps<TData>;
93
+ rowBodyProps?: TableRowBodyProps<TData>;
94
+ cellHeadProps?: TableHeadProps<TData>;
95
+ };
96
+
97
+ // export type Handles = {
98
+ // globalFilter?: () => void
99
+ // setGlobalFilter?: () => void
100
+ // sorting?: () => void
101
+ // setSorting?: () => void
102
+ // getColumn?: () => void
103
+ // previousPage?: () => void
104
+ // nextPage?: () => void
105
+ // getCanPreviousPage?: () => void
106
+ // getCanNextPage?: () => void
107
+ // pageIndex?: () => void
108
+ // pageSize?: () => void
109
+ // }
110
+
111
+ export type DataTableProps<TData, TValue> = {
112
+ columns: ColumnDef<TData, TValue>[];
113
+ data: TData[];
114
+ toolbarTable?: ({ table, fns }: { table: ITable<TData>; fns: DataTableToolbarFns<TData> }) => ReactNode | ReactNode[];
115
+ paginationTable?: ({ table, fns }: { table: ITable<TData>; fns: DataTablePaginationFns<TData> }) => ReactNode | ReactNode[];
116
+ isLoading?: boolean;
117
+ classNames?: TableClassNames;
118
+ emptyLabel?: string;
119
+ showSortIconHeader?: boolean;
120
+ surfix?: ({
121
+ header,
122
+ showSortIconHeader,
123
+ }: {
124
+ header: Header<TData, TValue | unknown>;
125
+ showSortIconHeader: boolean;
126
+ }) => ReactNode | ReactNode[];
127
+ enableSort?: boolean;
128
+ useTableProps?: UseTableProps<TData, TValue>;
129
+ initialState?: InitialTableState;
130
+ // handles?: Handles
131
+ };
132
+
133
+ export type DataTableToolbarFns<TData> = {
134
+ globalFilter: string;
135
+ setGlobalFilter: (value: string) => void;
136
+ sorting: any;
137
+ setSorting: (value: any) => void;
138
+ getColumn: (columnId: string) => ReturnType<ITable<TData>["getColumn"]>;
139
+ };
140
+
141
+ export type DataTablePaginationFns<TData> = {
142
+ previousPage: () => void;
143
+ nextPage: () => void;
144
+ getCanPreviousPage: () => boolean;
145
+ getCanNextPage: () => boolean;
146
+ pageIndex: number;
147
+ pageSize: number;
148
+ };
149
+
150
+ export function DataTable<TData, TValue>({
151
+ columns,
152
+ data,
153
+ toolbarTable,
154
+ paginationTable,
155
+ classNames,
156
+ isLoading = false,
157
+ emptyLabel = "No data",
158
+ showSortIconHeader = true,
159
+ surfix,
160
+ enableSort = true,
161
+ useTableProps,
162
+ initialState,
163
+ // handles
164
+ }: DataTableProps<TData, TValue>) {
165
+ const table = useReactTable({
166
+ data,
167
+ columns,
168
+ getCoreRowModel: getCoreRowModel(),
169
+ getFilteredRowModel: getFilteredRowModel(),
170
+ getPaginationRowModel: getPaginationRowModel(),
171
+ getSortedRowModel: enableSort ? getSortedRowModel() : undefined,
172
+ initialState: initialState,
173
+ });
174
+ const toolbarFns: DataTableToolbarFns<TData> = {
175
+ globalFilter: table.getState().globalFilter as string,
176
+ setGlobalFilter: table.setGlobalFilter,
177
+ sorting: table.getState().sorting,
178
+ setSorting: table.setSorting,
179
+ getColumn: table.getColumn,
180
+ };
181
+ const paginationFns: DataTablePaginationFns<TData> = {
182
+ previousPage: table.previousPage,
183
+ nextPage: table.nextPage,
184
+ getCanPreviousPage: table.getCanPreviousPage,
185
+ getCanNextPage: table.getCanNextPage,
186
+ pageIndex: table.getState().pagination.pageIndex,
187
+ pageSize: table.getState().pagination.pageSize,
188
+ };
189
+
190
+ return (
191
+ <div className={cn("space-y-4", classNames?.wrapper)}>
192
+ {toolbarTable && toolbarTable({ table, fns: toolbarFns })}
193
+ <div className={cn(classNames?.container)}>
194
+ <Table
195
+ className={cn(classNames?.table)}
196
+ {...useTableProps?.tableProps}
197
+ onClick={(e) => useTableProps?.tableProps?.handleClick({ e, table })}
198
+ >
199
+ <TableHeader
200
+ className={cn(classNames?.header?.header)}
201
+ {...useTableProps?.headerProps}
202
+ onClick={(e) => useTableProps?.headerProps?.handleClick({ e, table })}
203
+ >
204
+ {table.getHeaderGroups().map((headerGroup) => (
205
+ <TableRow
206
+ key={headerGroup.id}
207
+ className={cn(classNames?.header?.row)}
208
+ {...useTableProps?.rowHeadProps}
209
+ onClick={(e) => useTableProps?.rowHeadProps?.handleClick({ e, row: headerGroup, table })}
210
+ >
211
+ {headerGroup.headers.map((header) => (
212
+ <TableHead
213
+ key={header.id}
214
+ className={cn("cursor-pointer select-none", classNames?.header?.head)}
215
+ onClick={(e) => {
216
+ header.column.getToggleSortingHandler();
217
+
218
+ if (useTableProps?.headerProps?.handleClick) {
219
+ useTableProps?.cellHeadProps?.handleClick({ e, cell: header, table });
220
+ }
221
+ }}
222
+ {...useTableProps?.cellHeadProps}
223
+ >
224
+ {flexRender(header.column.columnDef.header, header.getContext())}
225
+ {table.getRowModel().rows.length > 0 && surfix && surfix({ header, showSortIconHeader })}
226
+ </TableHead>
227
+ ))}
228
+ </TableRow>
229
+ ))}
230
+ </TableHeader>
231
+
232
+ <TableBody
233
+ className={cn(classNames?.body?.body)}
234
+ {...useTableProps?.bodyProps}
235
+ onClick={(e) => useTableProps?.bodyProps?.handleClick({ e, table })}
236
+ >
237
+ {isLoading && (
238
+ <TableSkeleton
239
+ props={useTableProps}
240
+ isLoading={isLoading}
241
+ classNames={classNames}
242
+ emptyLabel={emptyLabel}
243
+ columns={columns}
244
+ />
245
+ )}
246
+ {!isLoading &&
247
+ table.getRowModel().rows.length > 0 &&
248
+ table.getRowModel().rows.map((row) => (
249
+ <TableRow
250
+ key={row.id}
251
+ className={cn(classNames?.body?.row)}
252
+ data-state={row.getIsSelected() && "selected"}
253
+ {...useTableProps?.rowBodyProps}
254
+ onClick={(e) => useTableProps?.rowBodyProps?.handleClick({ e, row, table })}
255
+ >
256
+ {row.getVisibleCells().map((cell) => (
257
+ <TableCell
258
+ key={cell.id}
259
+ className={cn(classNames?.body?.cell)}
260
+ {...useTableProps?.cellBodyProps}
261
+ onClick={(e) => useTableProps?.cellBodyProps?.handleClick({ e, cell, table })}
262
+ >
263
+ {flexRender(cell.column.columnDef.cell, cell.getContext())}
264
+ </TableCell>
265
+ ))}
266
+ </TableRow>
267
+ ))}
268
+ {!isLoading && table.getRowModel().rows.length === 0 && (
269
+ <TableRow key="no-data" className={cn(classNames?.body?.row)} {...useTableProps?.rowBodyProps}>
270
+ <TableCell
271
+ colSpan={columns.length}
272
+ className={cn("h-24 text-center", classNames?.body?.cell)}
273
+ {...useTableProps?.cellBodyProps}
274
+ >
275
+ {emptyLabel}
276
+ </TableCell>
277
+ </TableRow>
278
+ )}
279
+ </TableBody>
280
+ </Table>
281
+ </div>
282
+ {paginationTable && paginationTable({ table, fns: paginationFns })}
283
+ </div>
284
+ );
285
+ }
286
+
287
+ type TableSkeletonProps<TData, TValue> = {
288
+ isLoading: boolean;
289
+ classNames?: TableClassNames;
290
+ emptyLabel?: string;
291
+ columns: ColumnDef<TData, TValue>[];
292
+ props?: UseTableProps<TData, TValue>;
293
+ };
294
+
295
+ export const TableSkeleton = <TData, TValue>({ isLoading, classNames, emptyLabel, props, columns }: TableSkeletonProps<TData, TValue>) => {
296
+ const [showNoData, setShowNoData] = useState(false);
297
+ const timerRef = useRef<NodeJS.Timeout | null>(null);
298
+
299
+ useEffect(() => {
300
+ if (isLoading) {
301
+ timerRef.current = setTimeout(() => setShowNoData(true), 10000);
302
+ } else {
303
+ setShowNoData(false);
304
+ if (timerRef.current) {
305
+ clearTimeout(timerRef.current);
306
+ timerRef.current = null;
307
+ }
308
+ }
309
+ return () => {
310
+ if (timerRef.current) {
311
+ clearTimeout(timerRef.current);
312
+ timerRef.current = null;
313
+ }
314
+ };
315
+ }, [isLoading]);
316
+
317
+ if (showNoData) {
318
+ return (
319
+ <TableRow key="no-data-skeleton" className={cn(classNames?.body?.row)} {...props?.rowBodyProps}>
320
+ <TableCell colSpan={columns.length} className={cn("h-24 text-center", classNames?.body?.cell)} {...props?.cellBodyProps}>
321
+ {emptyLabel}
322
+ </TableCell>
323
+ </TableRow>
324
+ );
325
+ }
326
+ return (
327
+ <>
328
+ {[...Array(5)].map((_, rowIndex) => (
329
+ <TableRow key={`skeleton-${rowIndex}`} className={cn(classNames?.body?.row)} {...props?.rowBodyProps}>
330
+ {columns.map((_, colIndex) => (
331
+ <TableCell key={`skeleton-${rowIndex}-${colIndex}`} className={cn(classNames?.body?.cell)} {...props?.cellBodyProps}>
332
+ <div className="shimmer h-4 w-full" />
333
+ </TableCell>
334
+ ))}
335
+ </TableRow>
336
+ ))}
337
+ </>
338
+ );
339
+ };
@@ -0,0 +1,95 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+
5
+ import { ColumnDef } from "@tanstack/react-table";
6
+
7
+ import { DataTable } from "./data-table";
8
+
9
+ type User = {
10
+ id: string;
11
+ name: string;
12
+ email: string;
13
+ };
14
+
15
+ // 1. Columns
16
+ const columns: ColumnDef<User>[] = [
17
+ {
18
+ accessorKey: "name",
19
+ header: "Name",
20
+ cell: ({ row }) => <div className="font-medium">{row.getValue("name")}</div>,
21
+ enableSorting: true,
22
+ },
23
+ {
24
+ accessorKey: "email",
25
+ header: "Email",
26
+ enableSorting: true,
27
+ },
28
+ ];
29
+
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
+ ];
37
+
38
+ // 3. Component chính
39
+ export default function UserTableExample() {
40
+ const [data, setData] = useState<User[]>(users);
41
+ const [isLoading, setIsLoading] = useState(false);
42
+
43
+ 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
+ />
94
+ );
95
+ }
@@ -0,0 +1,60 @@
1
+ import * as React from "react";
2
+
3
+ import { cn } from "../../lib/utils";
4
+
5
+ const Table = React.forwardRef<HTMLTableElement, React.HTMLAttributes<HTMLTableElement>>(({ className, ...props }, ref) => (
6
+ <div className="relative w-full overflow-auto">
7
+ <table ref={ref} className={cn("w-full caption-bottom text-sm", className)} {...props} />
8
+ </div>
9
+ ));
10
+ Table.displayName = "Table";
11
+
12
+ const TableHeader = React.forwardRef<HTMLTableSectionElement, React.HTMLAttributes<HTMLTableSectionElement>>(
13
+ ({ className, ...props }, ref) => <thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />,
14
+ );
15
+ TableHeader.displayName = "TableHeader";
16
+
17
+ const TableBody = React.forwardRef<HTMLTableSectionElement, React.HTMLAttributes<HTMLTableSectionElement>>(
18
+ ({ className, ...props }, ref) => <tbody ref={ref} className={cn("[&_tr:last-child]:border-0", className)} {...props} />,
19
+ );
20
+ TableBody.displayName = "TableBody";
21
+
22
+ const TableFooter = React.forwardRef<HTMLTableSectionElement, React.HTMLAttributes<HTMLTableSectionElement>>(
23
+ ({ className, ...props }, ref) => (
24
+ <tfoot ref={ref} className={cn("bg-muted/50 border-t font-medium [&>tr]:last:border-b-0", className)} {...props} />
25
+ ),
26
+ );
27
+ TableFooter.displayName = "TableFooter";
28
+
29
+ const TableRow = React.forwardRef<HTMLTableRowElement, React.HTMLAttributes<HTMLTableRowElement>>(({ className, ...props }, ref) => (
30
+ <tr ref={ref} className={cn("hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors", className)} {...props} />
31
+ ));
32
+ TableRow.displayName = "TableRow";
33
+
34
+ const TableHead = React.forwardRef<HTMLTableCellElement, React.ThHTMLAttributes<HTMLTableCellElement>>(({ className, ...props }, ref) => (
35
+ <th
36
+ ref={ref}
37
+ className={cn(
38
+ "text-muted-foreground h-10 px-2 text-left align-middle font-medium [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
39
+ className,
40
+ )}
41
+ {...props}
42
+ />
43
+ ));
44
+ TableHead.displayName = "TableHead";
45
+
46
+ const TableCell = React.forwardRef<HTMLTableCellElement, React.TdHTMLAttributes<HTMLTableCellElement>>(({ className, ...props }, ref) => (
47
+ <td
48
+ ref={ref}
49
+ className={cn("p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]", className)}
50
+ {...props}
51
+ />
52
+ ));
53
+ TableCell.displayName = "TableCell";
54
+
55
+ const TableCaption = React.forwardRef<HTMLTableCaptionElement, React.HTMLAttributes<HTMLTableCaptionElement>>(
56
+ ({ className, ...props }, ref) => <caption ref={ref} className={cn("text-muted-foreground mt-4 text-sm", className)} {...props} />,
57
+ );
58
+ TableCaption.displayName = "TableCaption";
59
+
60
+ export { Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, TableRow };
@@ -0,0 +1,120 @@
1
+ "use client";
2
+ import {
3
+ useEffect,
4
+ useRef,
5
+ useState,
6
+ } from 'react';
7
+
8
+ import { motion } from 'motion/react';
9
+
10
+ export const TextHoverEffect = ({ text, duration }: { text: string; duration?: number; automatic?: boolean }) => {
11
+ const svgRef = useRef<SVGSVGElement>(null);
12
+ const [cursor, setCursor] = useState({ x: 0, y: 0 });
13
+ const [hovered, setHovered] = useState(false);
14
+ const [maskPosition, setMaskPosition] = useState({ cx: "50%", cy: "50%" });
15
+
16
+ useEffect(() => {
17
+ if (svgRef.current && cursor.x !== null && cursor.y !== null) {
18
+ const svgRect = svgRef.current.getBoundingClientRect();
19
+ const cxPercentage = ((cursor.x - svgRect.left) / svgRect.width) * 100;
20
+ const cyPercentage = ((cursor.y - svgRect.top) / svgRect.height) * 100;
21
+ setMaskPosition({
22
+ cx: `${cxPercentage}%`,
23
+ cy: `${cyPercentage}%`,
24
+ });
25
+ }
26
+ }, [cursor]);
27
+
28
+ return (
29
+ <svg
30
+ ref={svgRef}
31
+ width="100%"
32
+ height="100%"
33
+ viewBox="0 0 300 100"
34
+ xmlns="http://www.w3.org/2000/svg"
35
+ onMouseEnter={() => setHovered(true)}
36
+ onMouseLeave={() => setHovered(false)}
37
+ onMouseMove={(e) => setCursor({ x: e.clientX, y: e.clientY })}
38
+ className="select-none"
39
+ >
40
+ <defs>
41
+ <linearGradient id="textGradient" gradientUnits="userSpaceOnUse" cx="50%" cy="50%" r="25%">
42
+ {hovered && (
43
+ <>
44
+ <stop offset="0%" stopColor="#eab308" />
45
+ <stop offset="25%" stopColor="#ef4444" />
46
+ <stop offset="50%" stopColor="#3b82f6" />
47
+ <stop offset="75%" stopColor="#06b6d4" />
48
+ <stop offset="100%" stopColor="#8b5cf6" />
49
+ </>
50
+ )}
51
+ </linearGradient>
52
+
53
+ <motion.radialGradient
54
+ id="revealMask"
55
+ gradientUnits="userSpaceOnUse"
56
+ r="20%"
57
+ initial={{ cx: "50%", cy: "50%" }}
58
+ animate={maskPosition}
59
+ transition={{ duration: duration ?? 0, ease: "easeOut" }}
60
+
61
+ // example for a smoother animation below
62
+
63
+ // transition={{
64
+ // type: "spring",
65
+ // stiffness: 300,
66
+ // damping: 50,
67
+ // }}
68
+ >
69
+ <stop offset="0%" stopColor="white" />
70
+ <stop offset="100%" stopColor="black" />
71
+ </motion.radialGradient>
72
+ <mask id="textMask">
73
+ <rect x="0" y="0" width="100%" height="100%" fill="url(#revealMask)" />
74
+ </mask>
75
+ </defs>
76
+ <text
77
+ x="50%"
78
+ y="50%"
79
+ textAnchor="middle"
80
+ dominantBaseline="middle"
81
+ strokeWidth="0.3"
82
+ className="fill-transparent stroke-neutral-200 font-[helvetica] text-7xl font-bold dark:stroke-neutral-800"
83
+ style={{ opacity: hovered ? 0.7 : 0 }}
84
+ >
85
+ {text}
86
+ </text>
87
+ <motion.text
88
+ x="50%"
89
+ y="50%"
90
+ textAnchor="middle"
91
+ dominantBaseline="middle"
92
+ strokeWidth="0.3"
93
+ className="fill-transparent stroke-neutral-200 font-[helvetica] text-7xl font-bold dark:stroke-neutral-800"
94
+ initial={{ strokeDashoffset: 1000, strokeDasharray: 1000 }}
95
+ animate={{
96
+ strokeDashoffset: 0,
97
+ strokeDasharray: 1000,
98
+ }}
99
+ transition={{
100
+ duration: 4,
101
+ ease: "easeInOut",
102
+ }}
103
+ >
104
+ {text}
105
+ </motion.text>
106
+ <text
107
+ x="50%"
108
+ y="50%"
109
+ textAnchor="middle"
110
+ dominantBaseline="middle"
111
+ stroke="url(#textGradient)"
112
+ strokeWidth="0.3"
113
+ mask="url(#textMask)"
114
+ className="fill-transparent font-[helvetica] text-7xl font-bold"
115
+ >
116
+ {text}
117
+ </text>
118
+ </svg>
119
+ );
120
+ };
@@ -0,0 +1,52 @@
1
+ "use client";
2
+ import React, {
3
+ useEffect,
4
+ useState,
5
+ } from "react";
6
+
7
+ type TimeoutLoaderProps = {
8
+ children: React.ReactNode;
9
+ fallback: React.ReactNode | (() => void);
10
+ timeoutMs?: number;
11
+ isLoaded?: boolean;
12
+ isActive?: boolean;
13
+ loadingComponent?: React.ReactNode;
14
+ };
15
+
16
+ export const TimeoutLoader: React.FC<TimeoutLoaderProps> = ({
17
+ children,
18
+ fallback,
19
+ timeoutMs = 10000,
20
+ isLoaded = false,
21
+ isActive = true,
22
+ loadingComponent = <div>Đang tải...</div>,
23
+ }) => {
24
+ const [timedOut, setTimedOut] = useState(false);
25
+
26
+ useEffect(() => {
27
+ if (!isActive || isLoaded) return;
28
+
29
+ const timeout = setTimeout(() => {
30
+ setTimedOut(true);
31
+ }, timeoutMs);
32
+
33
+ return () => clearTimeout(timeout);
34
+ }, [isLoaded, isActive, timeoutMs]);
35
+
36
+ // Không active: render children ngay lập tức
37
+ if (!isActive) return <>{children}</>;
38
+
39
+ // Nếu đã load
40
+ if (isLoaded) return <>{children}</>;
41
+
42
+ // Nếu hết thời gian và fallback là hàm
43
+ if (timedOut) {
44
+ if (typeof fallback === 'function') {
45
+ fallback(); // optional: có thể return fallback() nếu muốn hiện kết quả từ hàm
46
+ return null;
47
+ }
48
+ return <>{fallback}</>;
49
+ }
50
+
51
+ return <>{loadingComponent}</>;
52
+ };