@nocobase/portal-template-default 1.0.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.
Files changed (123) hide show
  1. package/.env.example +10 -0
  2. package/Dockerfile +36 -0
  3. package/README.MD +50 -0
  4. package/components.json +25 -0
  5. package/eslint.config.js +28 -0
  6. package/index.html +39 -0
  7. package/package.json +84 -0
  8. package/pnpm-workspace.yaml +2 -0
  9. package/public/favicon.ico +0 -0
  10. package/public/logo-dark.png +0 -0
  11. package/public/logo-mark-dark.png +0 -0
  12. package/public/logo-mark.png +0 -0
  13. package/public/logo.png +0 -0
  14. package/src/App.css +158 -0
  15. package/src/App.tsx +151 -0
  16. package/src/components/app-shell/brand.tsx +70 -0
  17. package/src/components/app-shell/breadcrumb.tsx +81 -0
  18. package/src/components/app-shell/document-title-handler.tsx +169 -0
  19. package/src/components/app-shell/error-component.tsx +134 -0
  20. package/src/components/app-shell/header.tsx +127 -0
  21. package/src/components/app-shell/layout.tsx +39 -0
  22. package/src/components/app-shell/loading-overlay.tsx +36 -0
  23. package/src/components/app-shell/sidebar.tsx +351 -0
  24. package/src/components/app-shell/user-avatar.tsx +42 -0
  25. package/src/components/app-shell/user-info.tsx +53 -0
  26. package/src/components/auth/auth-layout.tsx +82 -0
  27. package/src/components/auth/forgot-password-form.tsx +58 -0
  28. package/src/components/auth/input-password.tsx +38 -0
  29. package/src/components/auth/sign-in-form.tsx +111 -0
  30. package/src/components/auth/sign-up-form.tsx +127 -0
  31. package/src/components/data-table/data-table-filter.tsx +994 -0
  32. package/src/components/data-table/data-table-pagination.tsx +146 -0
  33. package/src/components/data-table/data-table-sorter.tsx +47 -0
  34. package/src/components/data-table/data-table.tsx +321 -0
  35. package/src/components/notifications/toaster.tsx +23 -0
  36. package/src/components/notifications/undoable-notification.tsx +84 -0
  37. package/src/components/notifications/use-notification-provider.tsx +62 -0
  38. package/src/components/resources/buttons/clone.tsx +81 -0
  39. package/src/components/resources/buttons/create.tsx +70 -0
  40. package/src/components/resources/buttons/delete.tsx +113 -0
  41. package/src/components/resources/buttons/edit.tsx +81 -0
  42. package/src/components/resources/buttons/list.tsx +70 -0
  43. package/src/components/resources/buttons/refresh.tsx +81 -0
  44. package/src/components/resources/buttons/show.tsx +81 -0
  45. package/src/components/resources/status-badge.tsx +37 -0
  46. package/src/components/resources/views/create-view.tsx +81 -0
  47. package/src/components/resources/views/edit-view.tsx +104 -0
  48. package/src/components/resources/views/list-view.tsx +86 -0
  49. package/src/components/resources/views/show-view.tsx +108 -0
  50. package/src/components/theme/theme-provider.tsx +71 -0
  51. package/src/components/theme/theme-select.tsx +97 -0
  52. package/src/components/theme/theme-toggle.tsx +66 -0
  53. package/src/components/ui/accordion.tsx +72 -0
  54. package/src/components/ui/alert-dialog.tsx +185 -0
  55. package/src/components/ui/alert.tsx +76 -0
  56. package/src/components/ui/aspect-ratio.tsx +22 -0
  57. package/src/components/ui/avatar.tsx +107 -0
  58. package/src/components/ui/badge.tsx +52 -0
  59. package/src/components/ui/breadcrumb.tsx +125 -0
  60. package/src/components/ui/button.tsx +58 -0
  61. package/src/components/ui/calendar.tsx +221 -0
  62. package/src/components/ui/card.tsx +103 -0
  63. package/src/components/ui/carousel.tsx +240 -0
  64. package/src/components/ui/chart.tsx +373 -0
  65. package/src/components/ui/checkbox.tsx +29 -0
  66. package/src/components/ui/collapsible.tsx +19 -0
  67. package/src/components/ui/command.tsx +192 -0
  68. package/src/components/ui/context-menu.tsx +271 -0
  69. package/src/components/ui/dialog.tsx +160 -0
  70. package/src/components/ui/drawer.tsx +226 -0
  71. package/src/components/ui/dropdown-menu.tsx +268 -0
  72. package/src/components/ui/form.tsx +174 -0
  73. package/src/components/ui/hover-card.tsx +49 -0
  74. package/src/components/ui/input-group.tsx +158 -0
  75. package/src/components/ui/input-otp.tsx +87 -0
  76. package/src/components/ui/input.tsx +20 -0
  77. package/src/components/ui/label.tsx +18 -0
  78. package/src/components/ui/menubar.tsx +278 -0
  79. package/src/components/ui/navigation-menu.tsx +168 -0
  80. package/src/components/ui/pagination.tsx +130 -0
  81. package/src/components/ui/popover.tsx +91 -0
  82. package/src/components/ui/progress.tsx +81 -0
  83. package/src/components/ui/radio-group.tsx +38 -0
  84. package/src/components/ui/resizable.tsx +48 -0
  85. package/src/components/ui/scroll-area.tsx +55 -0
  86. package/src/components/ui/select.tsx +199 -0
  87. package/src/components/ui/separator.tsx +25 -0
  88. package/src/components/ui/sheet.tsx +136 -0
  89. package/src/components/ui/sidebar.tsx +723 -0
  90. package/src/components/ui/skeleton.tsx +13 -0
  91. package/src/components/ui/slider.tsx +52 -0
  92. package/src/components/ui/sonner.tsx +47 -0
  93. package/src/components/ui/switch.tsx +32 -0
  94. package/src/components/ui/table.tsx +114 -0
  95. package/src/components/ui/tabs.tsx +82 -0
  96. package/src/components/ui/textarea.tsx +18 -0
  97. package/src/components/ui/toggle-group.tsx +87 -0
  98. package/src/components/ui/toggle.tsx +43 -0
  99. package/src/components/ui/tooltip.tsx +66 -0
  100. package/src/hooks/use-mobile.ts +19 -0
  101. package/src/index.tsx +17 -0
  102. package/src/lib/utils.ts +19 -0
  103. package/src/pages/blog-posts/create.tsx +177 -0
  104. package/src/pages/blog-posts/edit.tsx +197 -0
  105. package/src/pages/blog-posts/index.ts +4 -0
  106. package/src/pages/blog-posts/list.tsx +131 -0
  107. package/src/pages/blog-posts/show.tsx +81 -0
  108. package/src/pages/categories/create.tsx +73 -0
  109. package/src/pages/categories/edit.tsx +73 -0
  110. package/src/pages/categories/index.ts +4 -0
  111. package/src/pages/categories/list.tsx +86 -0
  112. package/src/pages/categories/show.tsx +37 -0
  113. package/src/pages/forgot-password/index.tsx +6 -0
  114. package/src/pages/forgotPassword/index.tsx +5 -0
  115. package/src/pages/login/index.tsx +5 -0
  116. package/src/pages/register/index.tsx +5 -0
  117. package/src/providers/auth.ts +204 -0
  118. package/src/providers/constants.ts +75 -0
  119. package/src/providers/data.ts +232 -0
  120. package/src/vite-env.d.ts +1 -0
  121. package/tsconfig.json +25 -0
  122. package/tsconfig.node.json +21 -0
  123. package/vite.config.ts +74 -0
@@ -0,0 +1,146 @@
1
+ "use client";
2
+
3
+ import {
4
+ ChevronLeft,
5
+ ChevronRight,
6
+ ChevronsLeft,
7
+ ChevronsRight,
8
+ } from "lucide-react";
9
+ import { useMemo } from "react";
10
+
11
+ import {
12
+ Select,
13
+ SelectContent,
14
+ SelectItem,
15
+ SelectTrigger,
16
+ SelectValue,
17
+ } from "@/components/ui/select";
18
+ import { Button } from "@/components/ui/button";
19
+ import { cn } from "@/lib/utils";
20
+
21
+ type DataTablePaginationProps = {
22
+ currentPage: number;
23
+ pageCount: number;
24
+ setCurrentPage: (page: number) => void;
25
+ pageSize: number;
26
+ setPageSize: (size: number) => void;
27
+ total?: number;
28
+ };
29
+
30
+ export function DataTablePagination({
31
+ currentPage,
32
+ pageCount,
33
+ setCurrentPage,
34
+ pageSize,
35
+ setPageSize,
36
+ total,
37
+ }: DataTablePaginationProps) {
38
+ const pageSizeOptions = useMemo(() => {
39
+ const baseOptions = [10, 20, 30, 40, 50];
40
+ const optionsSet = new Set(baseOptions);
41
+
42
+ if (!optionsSet.has(pageSize)) {
43
+ optionsSet.add(pageSize);
44
+ }
45
+
46
+ return Array.from(optionsSet).sort((a, b) => a - b);
47
+ }, [pageSize]);
48
+
49
+ return (
50
+ <div
51
+ className={cn(
52
+ "flex",
53
+ "items-center",
54
+ "justify-between",
55
+ "flex-wrap",
56
+ "px-2",
57
+ "w-full",
58
+ "gap-2"
59
+ )}
60
+ >
61
+ <div
62
+ className={cn(
63
+ "flex-1",
64
+ "text-sm",
65
+ "text-muted-foreground",
66
+ "whitespace-nowrap"
67
+ )}
68
+ >
69
+ {typeof total === "number" ? `${total} row(s)` : null}
70
+ </div>
71
+ <div className={cn("flex", "items-center", "flex-wrap", "gap-2")}>
72
+ <div className={cn("flex", "items-center", "gap-2")}>
73
+ <span className={cn("text-sm", "font-medium")}>Rows per page</span>
74
+ <Select
75
+ value={`${pageSize}`}
76
+ onValueChange={(v) => setPageSize(Number(v))}
77
+ >
78
+ <SelectTrigger className={cn("h-8", "w-[70px]")}>
79
+ <SelectValue placeholder={pageSize} />
80
+ </SelectTrigger>
81
+ <SelectContent side="top">
82
+ {pageSizeOptions.map((size) => (
83
+ <SelectItem key={size} value={`${size}`}>
84
+ {size}
85
+ </SelectItem>
86
+ ))}
87
+ </SelectContent>
88
+ </Select>
89
+ </div>
90
+ <div className={cn("flex", "items-center", "flex-wrap", "gap-2")}>
91
+ <div
92
+ className={cn(
93
+ "flex",
94
+ "items-center",
95
+ "justify-center",
96
+ "text-sm",
97
+ "font-medium"
98
+ )}
99
+ >
100
+ Page {currentPage} of {pageCount}
101
+ </div>
102
+ <div className={cn("flex", "items-center", "gap-2")}>
103
+ <Button
104
+ variant="outline"
105
+ className={cn("hidden", "h-8", "w-8", "p-0", "lg:flex")}
106
+ onClick={() => setCurrentPage(1)}
107
+ disabled={currentPage === 1}
108
+ aria-label="Go to first page"
109
+ >
110
+ <ChevronsLeft />
111
+ </Button>
112
+ <Button
113
+ variant="outline"
114
+ className={cn("h-8", "w-8", "p-0")}
115
+ onClick={() => setCurrentPage(currentPage - 1)}
116
+ disabled={currentPage === 1}
117
+ aria-label="Go to previous page"
118
+ >
119
+ <ChevronLeft />
120
+ </Button>
121
+ <Button
122
+ variant="outline"
123
+ className={cn("h-8", "w-8", "p-0")}
124
+ onClick={() => setCurrentPage(currentPage + 1)}
125
+ disabled={currentPage === pageCount}
126
+ aria-label="Go to next page"
127
+ >
128
+ <ChevronRight />
129
+ </Button>
130
+ <Button
131
+ variant="outline"
132
+ className={cn("hidden", "h-8", "w-8", "p-0", "lg:flex")}
133
+ onClick={() => setCurrentPage(pageCount)}
134
+ disabled={currentPage === pageCount}
135
+ aria-label="Go to last page"
136
+ >
137
+ <ChevronsRight />
138
+ </Button>
139
+ </div>
140
+ </div>
141
+ </div>
142
+ </div>
143
+ );
144
+ }
145
+
146
+ DataTablePagination.displayName = "DataTablePagination";
@@ -0,0 +1,47 @@
1
+ "use client";
2
+
3
+ import type { Column } from "@tanstack/react-table";
4
+ import { ArrowDown, ArrowUp, ChevronsUpDown } from "lucide-react";
5
+ import { Button } from "@/components/ui/button";
6
+ import { cn } from "@/lib/utils";
7
+
8
+ export type DataTableSorterProps<TData> = {
9
+ column: Column<TData>;
10
+ } & React.ComponentProps<typeof Button>;
11
+
12
+ export function DataTableSorter<TData>({
13
+ column,
14
+ className,
15
+ ...props
16
+ }: DataTableSorterProps<TData>) {
17
+ const title =
18
+ column.getIsSorted() === "desc"
19
+ ? `Sort by ${column.id} as descending`
20
+ : column.getIsSorted() === "asc"
21
+ ? `Sort by ${column.id} as ascending`
22
+ : `Sort by ${column.id}`;
23
+
24
+ return (
25
+ <Button
26
+ variant="ghost"
27
+ size="icon"
28
+ onClick={() => column.toggleSorting(undefined, true)}
29
+ title={title}
30
+ aria-label={title}
31
+ {...props}
32
+ className={cn("data-[state=open]:bg-accent", "w-5 h-5", className)}
33
+ >
34
+ {column.getIsSorted() === "desc" ? (
35
+ <ArrowDown className={cn("text-primary", "!w-3", "!h-3")} />
36
+ ) : column.getIsSorted() === "asc" ? (
37
+ <ArrowUp className={cn("text-primary", "!w-3", "!h-3")} />
38
+ ) : (
39
+ <ChevronsUpDown
40
+ className={cn("text-muted-foreground", "!w-3", "!h-3")}
41
+ />
42
+ )}
43
+ </Button>
44
+ );
45
+ }
46
+
47
+ DataTableSorter.displayName = "DataTableSorter";
@@ -0,0 +1,321 @@
1
+ "use client";
2
+
3
+ import type { HttpError, BaseRecord } from "@refinedev/core";
4
+ import type { UseTableReturnType } from "@refinedev/react-table";
5
+ import type { Column } from "@tanstack/react-table";
6
+ import { flexRender } from "@tanstack/react-table";
7
+ import { Loader2 } from "lucide-react";
8
+ import { useEffect, useRef, useState } from "react";
9
+
10
+ import {
11
+ Table,
12
+ TableBody,
13
+ TableCell,
14
+ TableHead,
15
+ TableHeader,
16
+ TableRow,
17
+ } from "@/components/ui/table";
18
+ import { DataTablePagination } from "@/components/data-table/data-table-pagination";
19
+ import { cn } from "@/lib/utils";
20
+
21
+ type DataTableProps<TData extends BaseRecord> = {
22
+ table: UseTableReturnType<TData, HttpError>;
23
+ };
24
+
25
+ export function DataTable<TData extends BaseRecord>({
26
+ table,
27
+ }: DataTableProps<TData>) {
28
+ const {
29
+ reactTable: { getHeaderGroups, getRowModel, getAllColumns },
30
+ refineCore: {
31
+ tableQuery,
32
+ currentPage,
33
+ setCurrentPage,
34
+ pageCount,
35
+ pageSize,
36
+ setPageSize,
37
+ },
38
+ } = table;
39
+
40
+ const columns = getAllColumns();
41
+ const leafColumns = table.reactTable.getAllLeafColumns();
42
+ const isLoading = tableQuery.isLoading;
43
+
44
+ const tableContainerRef = useRef<HTMLDivElement>(null);
45
+ const tableRef = useRef<HTMLTableElement>(null);
46
+ const [isOverflowing, setIsOverflowing] = useState({
47
+ horizontal: false,
48
+ vertical: false,
49
+ });
50
+
51
+ useEffect(() => {
52
+ const checkOverflow = () => {
53
+ if (tableRef.current && tableContainerRef.current) {
54
+ const table = tableRef.current;
55
+ const container = tableContainerRef.current;
56
+
57
+ const horizontalOverflow = table.offsetWidth > container.clientWidth;
58
+ const verticalOverflow = table.offsetHeight > container.clientHeight;
59
+
60
+ setIsOverflowing({
61
+ horizontal: horizontalOverflow,
62
+ vertical: verticalOverflow,
63
+ });
64
+ }
65
+ };
66
+
67
+ checkOverflow();
68
+
69
+ // Check on window resize
70
+ window.addEventListener("resize", checkOverflow);
71
+
72
+ // Check when table data changes
73
+ const timeoutId = setTimeout(checkOverflow, 100);
74
+
75
+ return () => {
76
+ window.removeEventListener("resize", checkOverflow);
77
+ clearTimeout(timeoutId);
78
+ };
79
+ }, [tableQuery.data?.data, pageSize]);
80
+
81
+ return (
82
+ <div className={cn("flex", "flex-col", "flex-1", "gap-4")}>
83
+ <div
84
+ ref={tableContainerRef}
85
+ className={cn(
86
+ "overflow-hidden rounded-xl border bg-card shadow-[0_1px_2px_rgba(0,0,0,0.04)]"
87
+ )}
88
+ >
89
+ <Table ref={tableRef} style={{ tableLayout: "fixed", width: "100%" }}>
90
+ <TableHeader className="bg-muted/45">
91
+ {getHeaderGroups().map((headerGroup) => (
92
+ <TableRow key={headerGroup.id}>
93
+ {headerGroup.headers.map((header) => {
94
+ const isPlaceholder = header.isPlaceholder;
95
+
96
+ return (
97
+ <TableHead
98
+ key={header.id}
99
+ style={{
100
+ ...getCommonStyles({
101
+ column: header.column,
102
+ isOverflowing: isOverflowing,
103
+ }),
104
+ }}
105
+ >
106
+ {isPlaceholder ? null : (
107
+ <div className={cn("flex", "items-center", "gap-1")}>
108
+ {flexRender(
109
+ header.column.columnDef.header,
110
+ header.getContext()
111
+ )}
112
+ </div>
113
+ )}
114
+ </TableHead>
115
+ );
116
+ })}
117
+ </TableRow>
118
+ ))}
119
+ </TableHeader>
120
+ <TableBody className="relative">
121
+ {isLoading ? (
122
+ <>
123
+ {Array.from({ length: pageSize < 1 ? 1 : pageSize }).map(
124
+ (_, rowIndex) => (
125
+ <TableRow
126
+ key={`skeleton-row-${rowIndex}`}
127
+ aria-hidden="true"
128
+ >
129
+ {leafColumns.map((column) => (
130
+ <TableCell
131
+ key={`skeleton-cell-${rowIndex}-${column.id}`}
132
+ style={{
133
+ ...getCommonStyles({
134
+ column,
135
+ isOverflowing: isOverflowing,
136
+ }),
137
+ }}
138
+ className={cn("truncate")}
139
+ >
140
+ <div className="h-8" />
141
+ </TableCell>
142
+ ))}
143
+ </TableRow>
144
+ )
145
+ )}
146
+ <TableRow>
147
+ <TableCell
148
+ colSpan={columns.length}
149
+ className={cn("absolute", "inset-0", "pointer-events-none")}
150
+ >
151
+ <Loader2
152
+ className={cn(
153
+ "absolute",
154
+ "top-1/2",
155
+ "left-1/2",
156
+ "animate-spin",
157
+ "text-primary",
158
+ "h-8",
159
+ "w-8",
160
+ "-translate-x-1/2",
161
+ "-translate-y-1/2"
162
+ )}
163
+ />
164
+ </TableCell>
165
+ </TableRow>
166
+ </>
167
+ ) : getRowModel().rows?.length ? (
168
+ getRowModel().rows.map((row) => {
169
+ return (
170
+ <TableRow
171
+ key={row.original?.id ?? row.id}
172
+ data-state={row.getIsSelected() && "selected"}
173
+ >
174
+ {row.getVisibleCells().map((cell) => {
175
+ return (
176
+ <TableCell
177
+ key={cell.id}
178
+ style={{
179
+ ...getCommonStyles({
180
+ column: cell.column,
181
+ isOverflowing: isOverflowing,
182
+ }),
183
+ }}
184
+ >
185
+ <div className="truncate">
186
+ {flexRender(
187
+ cell.column.columnDef.cell,
188
+ cell.getContext()
189
+ )}
190
+ </div>
191
+ </TableCell>
192
+ );
193
+ })}
194
+ </TableRow>
195
+ );
196
+ })
197
+ ) : (
198
+ <DataTableNoData
199
+ isOverflowing={isOverflowing}
200
+ columnsLength={columns.length}
201
+ />
202
+ )}
203
+ </TableBody>
204
+ </Table>
205
+ </div>
206
+ {!isLoading && getRowModel().rows?.length > 0 && (
207
+ <DataTablePagination
208
+ currentPage={currentPage}
209
+ pageCount={pageCount}
210
+ setCurrentPage={setCurrentPage}
211
+ pageSize={pageSize}
212
+ setPageSize={setPageSize}
213
+ total={tableQuery.data?.total}
214
+ />
215
+ )}
216
+ </div>
217
+ );
218
+ }
219
+
220
+ function DataTableNoData({
221
+ isOverflowing,
222
+ columnsLength,
223
+ }: {
224
+ isOverflowing: { horizontal: boolean; vertical: boolean };
225
+ columnsLength: number;
226
+ }) {
227
+ return (
228
+ <TableRow className="hover:bg-transparent">
229
+ <TableCell
230
+ colSpan={columnsLength}
231
+ className={cn("relative", "text-center")}
232
+ style={{ height: "490px" }}
233
+ >
234
+ <div
235
+ className={cn(
236
+ "absolute",
237
+ "inset-0",
238
+ "flex",
239
+ "flex-col",
240
+ "items-center",
241
+ "justify-center",
242
+ "gap-2",
243
+ "bg-card"
244
+ )}
245
+ style={{
246
+ position: isOverflowing.horizontal ? "sticky" : "absolute",
247
+ left: isOverflowing.horizontal ? "50%" : "50%",
248
+ transform: "translateX(-50%)",
249
+ zIndex: isOverflowing.horizontal ? 2 : 1,
250
+ width: isOverflowing.horizontal ? "fit-content" : "100%",
251
+ minWidth: "300px",
252
+ }}
253
+ >
254
+ <div className={cn("text-lg", "font-semibold", "text-foreground")}>
255
+ No data to display
256
+ </div>
257
+ <div className={cn("text-sm", "text-muted-foreground")}>
258
+ This table is empty for the time being.
259
+ </div>
260
+ </div>
261
+ </TableCell>
262
+ </TableRow>
263
+ );
264
+ }
265
+
266
+ export function getCommonStyles<TData>({
267
+ column,
268
+ isOverflowing,
269
+ }: {
270
+ column: Column<TData>;
271
+ isOverflowing: {
272
+ horizontal: boolean;
273
+ vertical: boolean;
274
+ };
275
+ }): React.CSSProperties {
276
+ const isPinned = column.getIsPinned();
277
+ const isLastLeftPinnedColumn =
278
+ isPinned === "left" && column.getIsLastColumn("left");
279
+ const isFirstRightPinnedColumn =
280
+ isPinned === "right" && column.getIsFirstColumn("right");
281
+
282
+ return {
283
+ boxShadow:
284
+ isOverflowing.horizontal && isLastLeftPinnedColumn
285
+ ? "-4px 0 4px -4px var(--border) inset"
286
+ : isOverflowing.horizontal && isFirstRightPinnedColumn
287
+ ? "4px 0 4px -4px var(--border) inset"
288
+ : undefined,
289
+ left:
290
+ isOverflowing.horizontal && isPinned === "left"
291
+ ? `${column.getStart("left")}px`
292
+ : undefined,
293
+ right:
294
+ isOverflowing.horizontal && isPinned === "right"
295
+ ? `${column.getAfter("right")}px`
296
+ : undefined,
297
+ opacity: 1,
298
+ position: isOverflowing.horizontal && isPinned ? "sticky" : "relative",
299
+ background: isOverflowing.horizontal && isPinned ? "var(--background)" : "",
300
+ borderTopRightRadius:
301
+ isOverflowing.horizontal && isPinned === "right"
302
+ ? "var(--radius)"
303
+ : undefined,
304
+ borderBottomRightRadius:
305
+ isOverflowing.horizontal && isPinned === "right"
306
+ ? "var(--radius)"
307
+ : undefined,
308
+ borderTopLeftRadius:
309
+ isOverflowing.horizontal && isPinned === "left"
310
+ ? "var(--radius)"
311
+ : undefined,
312
+ borderBottomLeftRadius:
313
+ isOverflowing.horizontal && isPinned === "left"
314
+ ? "var(--radius)"
315
+ : undefined,
316
+ width: column.getSize(),
317
+ zIndex: isOverflowing.horizontal && isPinned ? 1 : 0,
318
+ };
319
+ }
320
+
321
+ DataTable.displayName = "DataTable";
@@ -0,0 +1,23 @@
1
+ "use client";
2
+
3
+ import { Toaster as Sonner, type ToasterProps } from "sonner";
4
+ import { useTheme } from "@/components/theme/theme-provider";
5
+
6
+ export function Toaster({ ...props }: ToasterProps) {
7
+ const { theme = "light" } = useTheme();
8
+
9
+ return (
10
+ <Sonner
11
+ theme={theme as ToasterProps["theme"]}
12
+ className="toaster group"
13
+ style={
14
+ {
15
+ "--normal-bg": "var(--popover)",
16
+ "--normal-text": "var(--popover-foreground)",
17
+ "--normal-border": "var(--border)",
18
+ } as React.CSSProperties
19
+ }
20
+ {...props}
21
+ />
22
+ );
23
+ }
@@ -0,0 +1,84 @@
1
+ import React from "react";
2
+ import { useTranslate } from "@refinedev/core";
3
+ import { Button } from "@/components/ui/button";
4
+ import { cn } from "@/lib/utils";
5
+
6
+ type UndoableNotificationProps = {
7
+ message: string;
8
+ description?: string;
9
+ undoableTimeout?: number;
10
+ cancelMutation?: () => void;
11
+ onClose?: () => void;
12
+ };
13
+
14
+ export function UndoableNotification({
15
+ message,
16
+ description,
17
+ undoableTimeout = 5,
18
+ cancelMutation,
19
+ onClose,
20
+ }: UndoableNotificationProps) {
21
+ const t = useTranslate();
22
+
23
+ React.useEffect(() => {
24
+ const timer = setTimeout(() => {
25
+ onClose?.();
26
+ }, undoableTimeout * 1000);
27
+
28
+ return () => clearTimeout(timer);
29
+ }, [onClose, undoableTimeout]);
30
+
31
+ const handleUndo = () => {
32
+ cancelMutation?.();
33
+ onClose?.();
34
+ };
35
+
36
+ return (
37
+ <div
38
+ className={cn(
39
+ "bg-card",
40
+ "text-card-foreground",
41
+ "rounded-lg",
42
+ "p-4",
43
+ "shadow-xl",
44
+ "border",
45
+ "border-border",
46
+ "min-w-[320px]",
47
+ "max-w-md"
48
+ )}
49
+ >
50
+ <div className={cn("flex", "items-center", "justify-between")}>
51
+ <div className={cn("flex-1", "mr-4")}>
52
+ <div className={cn("font-medium", "text-foreground", "text-sm")}>
53
+ {message}
54
+ </div>
55
+ {description && (
56
+ <div className={cn("text-muted-foreground", "text-sm", "mt-1")}>
57
+ {description}
58
+ </div>
59
+ )}
60
+ </div>
61
+ <Button
62
+ variant="secondary"
63
+ size="sm"
64
+ onClick={handleUndo}
65
+ className={cn(
66
+ "bg-secondary",
67
+ "hover:bg-secondary/80",
68
+ "text-secondary-foreground",
69
+ "border-0",
70
+ "px-4",
71
+ "py-2",
72
+ "text-sm",
73
+ "font-medium",
74
+ "rounded-md"
75
+ )}
76
+ >
77
+ {t("buttons.undo", "Undo")}
78
+ </Button>
79
+ </div>
80
+ </div>
81
+ );
82
+ }
83
+
84
+ UndoableNotification.displayName = "UndoableNotification";
@@ -0,0 +1,62 @@
1
+ import type { NotificationProvider } from "@refinedev/core";
2
+ import { toast } from "sonner";
3
+ import { UndoableNotification } from "@/components/notifications/undoable-notification";
4
+
5
+ export function useNotificationProvider(): NotificationProvider {
6
+ return {
7
+ open: ({
8
+ key,
9
+ type,
10
+ message,
11
+ description,
12
+ undoableTimeout,
13
+ cancelMutation,
14
+ }) => {
15
+ switch (type) {
16
+ case "success":
17
+ toast.success(message, {
18
+ id: key,
19
+ description,
20
+ richColors: true,
21
+ });
22
+ return;
23
+
24
+ case "error":
25
+ toast.error(message, {
26
+ id: key,
27
+ description,
28
+ richColors: true,
29
+ });
30
+ return;
31
+
32
+ case "progress": {
33
+ const toastId = key || Date.now();
34
+
35
+ toast(
36
+ () => (
37
+ <UndoableNotification
38
+ message={message}
39
+ description={description}
40
+ undoableTimeout={undoableTimeout}
41
+ cancelMutation={cancelMutation}
42
+ onClose={() => toast.dismiss(toastId)}
43
+ />
44
+ ),
45
+ {
46
+ id: toastId,
47
+ duration: (undoableTimeout || 5) * 1000,
48
+ unstyled: true,
49
+ }
50
+ );
51
+ return;
52
+ }
53
+
54
+ default:
55
+ return;
56
+ }
57
+ },
58
+ close: (id) => {
59
+ toast.dismiss(id);
60
+ },
61
+ };
62
+ }