stackkit 0.3.5 → 0.3.6

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 (192) hide show
  1. package/README.md +50 -42
  2. package/dist/cli/add.js +122 -56
  3. package/dist/cli/create.d.ts +2 -0
  4. package/dist/cli/create.js +271 -95
  5. package/dist/cli/doctor.js +1 -0
  6. package/dist/cli/list.d.ts +1 -1
  7. package/dist/cli/list.js +6 -4
  8. package/dist/index.js +234 -191
  9. package/dist/lib/constants.d.ts +4 -0
  10. package/dist/lib/constants.js +4 -0
  11. package/dist/lib/discovery/module-discovery.d.ts +4 -0
  12. package/dist/lib/discovery/module-discovery.js +56 -0
  13. package/dist/lib/generation/code-generator.d.ts +11 -2
  14. package/dist/lib/generation/code-generator.js +42 -3
  15. package/dist/lib/generation/generator-utils.js +3 -1
  16. package/dist/lib/pm/package-manager.js +16 -13
  17. package/dist/lib/ui/logger.js +3 -2
  18. package/dist/lib/utils/path-resolver.d.ts +2 -0
  19. package/dist/lib/utils/path-resolver.js +8 -0
  20. package/dist/meta.json +8312 -0
  21. package/modules/auth/better-auth/files/{shared → express}/config/env.ts +48 -50
  22. package/modules/auth/better-auth/files/express/middlewares/authorize.ts +20 -1
  23. package/modules/auth/better-auth/files/express/modules/auth.controller.ts +349 -0
  24. package/modules/auth/better-auth/files/express/modules/{auth/auth.route.ts → auth.route.ts} +9 -4
  25. package/modules/auth/better-auth/files/express/modules/auth.service.ts +664 -0
  26. package/modules/auth/better-auth/files/express/modules/{auth/auth.type.ts → auth.type.ts} +22 -9
  27. package/modules/auth/better-auth/files/{shared/mongoose/auth/helper.ts → express/mongo-modules/auth.helper.ts} +11 -1
  28. package/modules/auth/better-auth/files/express/types/express.d.ts +11 -0
  29. package/modules/auth/better-auth/files/nextjs/api-route.ts +74 -0
  30. package/modules/auth/better-auth/files/nextjs/dashboard/pages/(user)/page.tsx +6 -0
  31. package/modules/auth/better-auth/files/nextjs/dashboard/pages/admin/page.tsx +6 -0
  32. package/modules/auth/better-auth/files/nextjs/dashboard/pages/layout.tsx +48 -0
  33. package/modules/auth/better-auth/files/nextjs/dashboard/pages/my-profile/page.tsx +5 -0
  34. package/modules/auth/better-auth/files/nextjs/features/services/auth.service.ts +102 -0
  35. package/modules/auth/better-auth/files/nextjs/layout/layout.tsx +13 -0
  36. package/modules/auth/better-auth/files/nextjs/lib/axios/http.ts +158 -0
  37. package/modules/auth/better-auth/files/nextjs/lib/env.ts +35 -0
  38. package/modules/auth/better-auth/files/nextjs/lib/utils/auth.ts +75 -0
  39. package/modules/auth/better-auth/files/nextjs/lib/utils/cookie.ts +29 -0
  40. package/modules/auth/better-auth/files/nextjs/lib/utils/jwt.ts +28 -0
  41. package/modules/auth/better-auth/files/nextjs/lib/utils/token.ts +49 -0
  42. package/modules/auth/better-auth/files/nextjs/pages/forgot-password/page.tsx +5 -0
  43. package/modules/auth/better-auth/files/nextjs/pages/layout.tsx +11 -0
  44. package/modules/auth/better-auth/files/nextjs/pages/login/page.tsx +9 -0
  45. package/modules/auth/better-auth/files/nextjs/pages/register/page.tsx +5 -0
  46. package/modules/auth/better-auth/files/nextjs/pages/reset-password/page.tsx +10 -0
  47. package/modules/auth/better-auth/files/nextjs/pages/verify-email/page.tsx +10 -0
  48. package/modules/auth/better-auth/files/nextjs/proxy.ts +154 -42
  49. package/modules/auth/better-auth/files/nextjs/theme/providers/theme-provider.tsx +11 -0
  50. package/modules/auth/better-auth/files/nextjs/types/api.types.ts +18 -0
  51. package/modules/auth/better-auth/files/react/components/protected-route.tsx +39 -0
  52. package/modules/auth/better-auth/files/react/components/route-guards.tsx +13 -0
  53. package/modules/auth/better-auth/files/react/dashboard/admin/pages/overview.tsx +3 -0
  54. package/modules/auth/better-auth/files/react/dashboard/pages/overview.tsx +3 -0
  55. package/modules/auth/better-auth/files/react/features/pages/forgot-password.tsx +5 -0
  56. package/modules/auth/better-auth/files/react/features/pages/login.tsx +5 -0
  57. package/modules/auth/better-auth/files/react/features/pages/my-profile.tsx +5 -0
  58. package/modules/auth/better-auth/files/react/features/pages/oauth-callback.tsx +59 -0
  59. package/modules/auth/better-auth/files/react/features/pages/register.tsx +5 -0
  60. package/modules/auth/better-auth/files/react/features/pages/reset-password.tsx +10 -0
  61. package/modules/auth/better-auth/files/react/features/pages/verify-email.tsx +10 -0
  62. package/modules/auth/better-auth/files/react/layout/dashboard-layout.tsx +54 -0
  63. package/modules/auth/better-auth/files/react/lib/axios/http.ts +68 -0
  64. package/modules/auth/better-auth/files/react/lib/env.ts +25 -0
  65. package/modules/auth/better-auth/files/react/router.tsx +73 -0
  66. package/modules/auth/better-auth/files/react/theme/components/providers/theme-provider-context.ts +13 -0
  67. package/modules/auth/better-auth/files/react/theme/components/providers/theme-provider.tsx +51 -0
  68. package/modules/auth/better-auth/files/react/theme/hooks/use-theme.ts +8 -0
  69. package/modules/auth/better-auth/files/shared/features/components/change-password-dialog.tsx +113 -0
  70. package/modules/auth/better-auth/files/shared/features/components/forgot-password-form.tsx +84 -0
  71. package/modules/auth/better-auth/files/shared/features/components/login-form.tsx +134 -0
  72. package/modules/auth/better-auth/files/shared/features/components/my-profile.tsx +147 -0
  73. package/modules/auth/better-auth/files/shared/features/components/profile-form.tsx +205 -0
  74. package/modules/auth/better-auth/files/shared/features/components/register-form.tsx +100 -0
  75. package/modules/auth/better-auth/files/shared/features/components/reset-password-form.tsx +111 -0
  76. package/modules/auth/better-auth/files/shared/features/components/social-login-buttons.tsx +47 -0
  77. package/modules/auth/better-auth/files/shared/features/components/user-profile-menu.tsx +106 -0
  78. package/modules/auth/better-auth/files/shared/features/components/verify-email-form.tsx +110 -0
  79. package/modules/auth/better-auth/files/shared/features/queries/auth.mutations.tsx +312 -0
  80. package/modules/auth/better-auth/files/shared/features/queries/auth.querie.ts +19 -0
  81. package/modules/auth/better-auth/files/shared/features/services/auth.api.ts +81 -0
  82. package/modules/auth/better-auth/files/shared/features/types/auth.type.ts +47 -0
  83. package/modules/auth/better-auth/files/shared/features/validators/change-password.validator.ts +18 -0
  84. package/modules/auth/better-auth/files/shared/features/validators/forgot.validator.ts +7 -0
  85. package/modules/auth/better-auth/files/shared/features/validators/login.validator.ts +14 -0
  86. package/modules/auth/better-auth/files/shared/features/validators/profile.validator.ts +8 -0
  87. package/modules/auth/better-auth/files/shared/features/validators/register.validator.ts +9 -0
  88. package/modules/auth/better-auth/files/shared/features/validators/reset.validator.ts +9 -0
  89. package/modules/auth/better-auth/files/shared/features/validators/verify.validator.ts +8 -0
  90. package/modules/auth/better-auth/files/shared/lib/auth-client.ts +2 -1
  91. package/modules/auth/better-auth/files/shared/lib/auth.ts +5 -19
  92. package/modules/auth/better-auth/files/shared/lib/constant/dashboard.ts +90 -0
  93. package/modules/auth/better-auth/files/shared/theme/mode-toggle.tsx +30 -0
  94. package/modules/auth/better-auth/files/shared/ui/shadcn/components/dashboard/dashboard-header.tsx +94 -0
  95. package/modules/auth/better-auth/files/shared/ui/shadcn/components/dashboard/dashboard-sidebar.tsx +255 -0
  96. package/modules/auth/better-auth/files/shared/ui/shadcn/components/footer.tsx +35 -0
  97. package/modules/auth/better-auth/files/shared/ui/shadcn/components/navbar.tsx +145 -0
  98. package/modules/auth/better-auth/files/shared/ui/shadcn/form-field/input-field.tsx +440 -0
  99. package/modules/auth/better-auth/files/shared/utils/email.ts +2 -17
  100. package/modules/auth/better-auth/generator.json +172 -51
  101. package/modules/auth/better-auth/module.json +2 -2
  102. package/modules/components/files/shared/hooks/use-file-upload.ts +412 -0
  103. package/modules/components/files/shared/lib/utils/url-helpers.ts +110 -0
  104. package/modules/components/files/shared/shadcn/dashboard/data-table-column-selector.tsx +52 -0
  105. package/modules/components/files/shared/shadcn/dashboard/data-table-footer.tsx +156 -0
  106. package/modules/components/files/shared/shadcn/dashboard/data-table.tsx +405 -0
  107. package/modules/components/files/shared/shadcn/global/form-field/input-field.tsx +440 -0
  108. package/modules/components/files/shared/shadcn/global/form-field/media-uploader-field.tsx +745 -0
  109. package/modules/components/files/shared/shadcn/global/form-field/multi-select-field.tsx +207 -0
  110. package/modules/components/files/shared/shadcn/global/form-field/select-field.tsx +247 -0
  111. package/modules/components/files/shared/shadcn/global/form-field/textarea-field.tsx +277 -0
  112. package/modules/components/files/shared/shadcn/global/form-field/tiptap-editor-field.tsx +35 -0
  113. package/modules/components/files/shared/shadcn/global/no-results.tsx +41 -0
  114. package/modules/components/files/shared/shadcn/tiptap-editor/editor-menu-bar.tsx +217 -0
  115. package/modules/components/files/shared/shadcn/tiptap-editor/tiptap-editor.tsx +104 -0
  116. package/modules/components/files/shared/url/load-more.tsx +93 -0
  117. package/modules/components/files/shared/url/search-bar.tsx +131 -0
  118. package/modules/components/files/shared/url/sort-select.tsx +118 -0
  119. package/modules/components/files/shared/url/url-tabs.tsx +77 -0
  120. package/modules/components/generator.json +109 -0
  121. package/modules/components/module.json +11 -0
  122. package/modules/database/mongoose/generator.json +3 -14
  123. package/modules/database/mongoose/module.json +2 -2
  124. package/modules/database/prisma/generator.json +6 -12
  125. package/modules/database/prisma/module.json +2 -2
  126. package/modules/storage/cloudinary/files/express/config/env.ts +65 -0
  127. package/modules/storage/cloudinary/files/express/config/media.ts +103 -0
  128. package/modules/storage/cloudinary/files/express/modules/media/media.controller.ts +59 -0
  129. package/modules/storage/cloudinary/files/express/modules/media/media.route.ts +29 -0
  130. package/modules/storage/cloudinary/files/express/modules/media/media.service.ts +113 -0
  131. package/modules/storage/cloudinary/files/express/modules/media/media.type.ts +32 -0
  132. package/modules/storage/cloudinary/generator.json +34 -0
  133. package/modules/storage/cloudinary/module.json +11 -0
  134. package/modules/ui/shadcn/generator.json +21 -0
  135. package/modules/ui/shadcn/module.json +11 -0
  136. package/package.json +24 -26
  137. package/templates/express/README.md +11 -16
  138. package/templates/express/src/config/env.ts +7 -5
  139. package/templates/nextjs/README.md +13 -18
  140. package/templates/nextjs/app/favicon.ico +0 -0
  141. package/templates/nextjs/app/layout.tsx +6 -4
  142. package/templates/nextjs/components/providers/query-provider.tsx +3 -0
  143. package/templates/nextjs/env.example +3 -1
  144. package/templates/nextjs/lib/axios/http.ts +23 -0
  145. package/templates/nextjs/lib/env.ts +7 -5
  146. package/templates/nextjs/package.json +2 -1
  147. package/templates/nextjs/template.json +1 -2
  148. package/templates/react/README.md +9 -14
  149. package/templates/react/index.html +1 -1
  150. package/templates/react/package.json +1 -1
  151. package/templates/react/src/assets/favicon.ico +0 -0
  152. package/templates/react/src/components/providers/query-provider.tsx +38 -0
  153. package/templates/react/src/{shared/components → components}/seo.tsx +4 -8
  154. package/templates/react/src/lib/axios/http.ts +24 -0
  155. package/templates/react/src/main.tsx +8 -11
  156. package/templates/react/src/{features/about/pages → pages}/about.tsx +1 -1
  157. package/templates/react/src/{features/home/pages → pages}/home.tsx +1 -1
  158. package/templates/react/src/router.tsx +6 -6
  159. package/templates/react/src/vite-env.d.ts +2 -1
  160. package/templates/react/template.json +0 -1
  161. package/templates/react/tsconfig.app.json +6 -0
  162. package/templates/react/tsconfig.json +7 -1
  163. package/templates/react/vite.config.ts +12 -0
  164. package/modules/auth/authjs/files/nextjs/api/auth/[...nextauth]/route.ts +0 -3
  165. package/modules/auth/authjs/files/nextjs/proxy.ts +0 -1
  166. package/modules/auth/authjs/files/shared/lib/auth.ts +0 -119
  167. package/modules/auth/authjs/files/shared/prisma/schema.prisma +0 -61
  168. package/modules/auth/authjs/generator.json +0 -64
  169. package/modules/auth/authjs/module.json +0 -13
  170. package/modules/auth/better-auth/files/express/modules/auth/auth.controller.ts +0 -264
  171. package/modules/auth/better-auth/files/express/modules/auth/auth.service.ts +0 -549
  172. package/modules/auth/better-auth/files/express/templates/google-redirect.ejs +0 -24
  173. package/modules/auth/better-auth/files/nextjs/api/auth/[...all]/route.ts +0 -4
  174. package/modules/auth/better-auth/files/nextjs/lib/auth/auth-guards.ts +0 -31
  175. package/modules/auth/better-auth/files/nextjs/templates/email-otp.tsx +0 -74
  176. package/templates/nextjs/lib/api/http.ts +0 -40
  177. package/templates/react/public/vite.svg +0 -1
  178. package/templates/react/src/app/layouts/dashboard-layout.tsx +0 -8
  179. package/templates/react/src/app/layouts/public-layout.tsx +0 -5
  180. package/templates/react/src/app/providers.tsx +0 -20
  181. package/templates/react/src/app/router.tsx +0 -21
  182. package/templates/react/src/assets/react.svg +0 -1
  183. package/templates/react/src/shared/api/http.ts +0 -39
  184. package/templates/react/src/shared/components/loading.tsx +0 -8
  185. package/templates/react/src/shared/lib/query-client.ts +0 -12
  186. package/templates/react/src/utils/storage.ts +0 -35
  187. package/templates/react/src/utils/utils.ts +0 -3
  188. /package/modules/auth/better-auth/files/{shared/mongoose/auth/constants.ts → express/mongo-modules/auth.constants.ts} +0 -0
  189. /package/templates/nextjs/app/{page.tsx → (public)/(root)/page.tsx} +0 -0
  190. /package/templates/react/src/{shared/components → components}/error-boundary.tsx +0 -0
  191. /package/templates/react/src/{shared/components → components}/layout.tsx +0 -0
  192. /package/templates/react/src/{shared/pages → pages}/not-found.tsx +0 -0
@@ -0,0 +1,156 @@
1
+ {{#if framework == "nextjs"}}
2
+ "use client";
3
+ {{/if}}
4
+
5
+ import { formUrlQuery, removeKeysFromQuery } from "@/lib/utils/url-helpers";
6
+ import {
7
+ IconChevronLeft,
8
+ IconChevronRight,
9
+ IconChevronsLeft,
10
+ IconChevronsRight,
11
+ } from "@tabler/icons-react";
12
+ import type { Table } from "@tanstack/react-table";
13
+ {{#if framework == "nextjs"}}
14
+ import { useRouter, useSearchParams } from "next/navigation";
15
+ {{else}}
16
+ import { useNavigate, useSearchParams } from "react-router";
17
+ {{/if}}
18
+ import SelectField from "../global/form-field/select-field";
19
+ import { Button } from "../ui/button";
20
+ import type { BaseRecord } from "./data-table";
21
+
22
+ interface DataTableFooterProps<TData extends BaseRecord> {
23
+ table: Table<TData>;
24
+ pageSize: number;
25
+ total: number;
26
+ }
27
+
28
+ export default function DataTableFooter<TData extends BaseRecord>({
29
+ table,
30
+ pageSize,
31
+ total,
32
+ }: DataTableFooterProps<TData>) {
33
+ {{#if framework == "nextjs"}}
34
+ const searchParams = useSearchParams();
35
+ const router = useRouter();
36
+ if (!searchParams) return null;
37
+ {{else}}
38
+ const navigate = useNavigate();
39
+ const [searchParams] = useSearchParams();
40
+ {{/if}}
41
+ const currentPage = Number(searchParams.get("pageIndex")) || 1;
42
+ const totalPages = Math.ceil(total / pageSize);
43
+
44
+ const handlePageChange = (newPage: number) => {
45
+ const query = formUrlQuery({
46
+ params: searchParams.toString(),
47
+ key: "pageIndex",
48
+ value: newPage.toString(),
49
+ });
50
+
51
+ {{#if framework == "nextjs"}}
52
+ router.push(query, { scroll: false });
53
+ {{else}}
54
+ navigate(query);
55
+ {{/if}}
56
+ table.setPageIndex(newPage - 1);
57
+ };
58
+
59
+ const handlePageSizeChange = (value: string | number | undefined) => {
60
+ if (typeof value === "undefined" || value === null) return;
61
+ const valueStr = String(value);
62
+ const newUrl = valueStr
63
+ ? formUrlQuery({
64
+ params: searchParams.toString(),
65
+ key: "pageSize",
66
+ value: valueStr,
67
+ })
68
+ : removeKeysFromQuery({
69
+ params: searchParams.toString(),
70
+ keysToRemove: ["pageSize"],
71
+ });
72
+
73
+ {{#if framework == "nextjs"}}
74
+ router.push(newUrl, { scroll: false });
75
+ {{else}}
76
+ navigate(newUrl);
77
+ {{/if}}
78
+ table.setPageSize(Number(valueStr));
79
+ };
80
+
81
+ return (
82
+ <div className="flex flex-col gap-4 px-4 py-3 lg:flex-row lg:items-center lg:justify-between">
83
+ {/* Selected Rows Summary */}
84
+ <div className="text-muted-foreground hidden text-sm lg:block">
85
+ {table.getFilteredSelectedRowModel().rows.length} of{" "}
86
+ {table.getFilteredRowModel().rows.length} row(s) selected
87
+ </div>
88
+
89
+ <div className="flex flex-col items-start gap-3 sm:w-full sm:flex-row sm:items-center sm:justify-between lg:w-auto">
90
+ {/* Rows Per Page - Only on large screens */}
91
+
92
+ <SelectField
93
+ label="Rows per page"
94
+ value={`${pageSize}`}
95
+ placeholder={`${pageSize}`}
96
+ onValueChange={handlePageSizeChange}
97
+ options={[25, 50, 75, 100].map((size) => ({
98
+ value: size,
99
+ label: `${size}`,
100
+ }))}
101
+ className="hidden items-center gap-2 lg:flex"
102
+ />
103
+
104
+ {/* Range Info */}
105
+ <div className="text-muted-foreground text-sm">
106
+ Showing {(currentPage - 1) * pageSize + 1}–
107
+ {Math.min(currentPage * pageSize, total)} of {total} rows
108
+ </div>
109
+
110
+ {/* Pagination Controls */}
111
+ <div className="flex items-center gap-2">
112
+ <Button
113
+ variant="outline"
114
+ size="icon"
115
+ className="hidden h-8 w-8 p-0 lg:inline-flex"
116
+ onClick={() => handlePageChange(1)}
117
+ disabled={currentPage <= 1}
118
+ >
119
+ <IconChevronsLeft className="h-4 w-4" />
120
+ <span className="sr-only">First page</span>
121
+ </Button>
122
+ <Button
123
+ variant="outline"
124
+ size="icon"
125
+ className="h-8 w-8"
126
+ onClick={() => handlePageChange(currentPage - 1)}
127
+ disabled={currentPage <= 1}
128
+ >
129
+ <IconChevronLeft className="h-4 w-4" />
130
+ <span className="sr-only">Previous page</span>
131
+ </Button>
132
+ <Button
133
+ variant="outline"
134
+ size="icon"
135
+ className="h-8 w-8"
136
+ onClick={() => handlePageChange(currentPage + 1)}
137
+ disabled={currentPage >= totalPages}
138
+ >
139
+ <IconChevronRight className="h-4 w-4" />
140
+ <span className="sr-only">Next page</span>
141
+ </Button>
142
+ <Button
143
+ variant="outline"
144
+ size="icon"
145
+ className="hidden h-8 w-8 lg:inline-flex"
146
+ onClick={() => handlePageChange(totalPages)}
147
+ disabled={currentPage >= totalPages}
148
+ >
149
+ <IconChevronsRight className="h-4 w-4" />
150
+ <span className="sr-only">Last page</span>
151
+ </Button>
152
+ </div>
153
+ </div>
154
+ </div>
155
+ );
156
+ }
@@ -0,0 +1,405 @@
1
+ {{#if framework == "nextjs"}}
2
+ "use client";
3
+ {{/if}}
4
+
5
+ import { Button } from "@/components/ui/button";
6
+ import { Checkbox } from "@/components/ui/checkbox";
7
+ import {
8
+ Dialog,
9
+ DialogContent,
10
+ DialogDescription,
11
+ DialogFooter,
12
+ DialogHeader,
13
+ DialogTitle,
14
+ DialogTrigger,
15
+ } from "@/components/ui/dialog";
16
+ import { Skeleton } from "@/components/ui/skeleton";
17
+ import {
18
+ Table,
19
+ TableBody,
20
+ TableCell,
21
+ TableHead,
22
+ TableHeader,
23
+ TableRow,
24
+ } from "@/components/ui/table";
25
+ import { Tabs, TabsContent } from "@/components/ui/tabs";
26
+ import { cn } from "@/lib/utils";
27
+ import { IconPlus } from "@tabler/icons-react";
28
+ import {
29
+ type ColumnDef,
30
+ type ColumnFiltersState,
31
+ type ColumnSort,
32
+ flexRender,
33
+ getCoreRowModel,
34
+ getFacetedRowModel,
35
+ getFacetedUniqueValues,
36
+ getFilteredRowModel,
37
+ getPaginationRowModel,
38
+ getSortedRowModel,
39
+ type OnChangeFn,
40
+ type PaginationState,
41
+ type Row,
42
+ type SortingState,
43
+ type Table as TableInstance,
44
+ useReactTable,
45
+ type VisibilityState,
46
+ } from "@tanstack/react-table";
47
+ {{#if framework == "nextjs"}}
48
+ import Link from "next/link";
49
+ import { useRouter } from "next/navigation";
50
+ {{else}}
51
+ import { Link } from "react-router";
52
+ {{/if}}
53
+ import * as React from "react";
54
+ import { toast } from "sonner";
55
+ import DataTableColumnSelector from "./data-table-column-selector";
56
+ import DataTableFooter from "./data-table-footer";
57
+
58
+ export interface BaseRecord {
59
+ _id?: string | number;
60
+ id?: string | number;
61
+ [key: string]: unknown;
62
+ }
63
+
64
+ interface DataTableProps<T extends BaseRecord> {
65
+ data?: T[];
66
+ columns?: ColumnDef<T>[];
67
+ pageSize?: number;
68
+ pageIndex?: number;
69
+ total?: number;
70
+ uniqueIdProperty?: string;
71
+ defaultSort?: ColumnSort[];
72
+ enableRowSelection?: boolean;
73
+ onDeleteMany?: (ids: string[]) => Promise<void>;
74
+ actionLink?: { href: string; label: string };
75
+ actionModal?: { form: React.ReactNode; label: string; title?: string };
76
+ rightSite?: React.ReactNode;
77
+ hasHeaderFooter?: boolean;
78
+ }
79
+
80
+ /** Unified way to read a record's unique id */
81
+ function getRowIdValue<T extends BaseRecord>(row: T, uniqueIdKey: string) {
82
+ const raw = row[uniqueIdKey] ?? row.id ?? row._id;
83
+ return typeof raw === "string" || typeof raw === "number" ? String(raw) : "";
84
+ }
85
+
86
+ export function DataTable<T extends BaseRecord>({
87
+ data: initialData = [],
88
+ columns = [],
89
+ pageSize = 20,
90
+ pageIndex = 1,
91
+ total = 0,
92
+ uniqueIdProperty = "id",
93
+ defaultSort = [],
94
+ enableRowSelection = true,
95
+ onDeleteMany,
96
+ actionLink,
97
+ actionModal,
98
+ rightSite,
99
+ hasHeaderFooter = true,
100
+ }: DataTableProps<T>) {
101
+ {{#if framework == "nextjs"}}
102
+ const router = useRouter();
103
+ {{/if}}
104
+ const [data, setData] = React.useState<T[]>(initialData);
105
+ const [isInitialLoad, setIsInitialLoad] = React.useState(true);
106
+ const [isDialogOpen, setIsDialogOpen] = React.useState(false);
107
+
108
+ const [rowSelection, setRowSelection] = React.useState({});
109
+ const [columnVisibility, setColumnVisibility] =
110
+ React.useState<VisibilityState>({});
111
+ const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
112
+ [],
113
+ );
114
+ const [sorting, setSorting] = React.useState<SortingState>(defaultSort);
115
+
116
+ const [pagination, setPagination] = React.useState<PaginationState>({
117
+ pageIndex: Math.max(0, pageIndex - 1),
118
+ pageSize,
119
+ });
120
+
121
+ // skeleton only during page changes after initial load
122
+ const [isTableLoading, setIsTableLoading] = React.useState(false);
123
+ const prevPageIndex = React.useRef(pagination.pageIndex);
124
+
125
+ const uniqueIdRef = React.useRef(uniqueIdProperty);
126
+ uniqueIdRef.current = uniqueIdProperty;
127
+
128
+ // eslint-disable-next-line react-hooks/incompatible-library
129
+ const table = useReactTable<T>({
130
+ data,
131
+ columns,
132
+ state: {
133
+ sorting,
134
+ columnVisibility,
135
+ rowSelection,
136
+ columnFilters,
137
+ pagination,
138
+ },
139
+ getRowId: (row) => getRowIdValue(row, uniqueIdRef.current),
140
+ enableRowSelection,
141
+ onRowSelectionChange: setRowSelection,
142
+ onSortingChange: setSorting as OnChangeFn<SortingState>,
143
+ onColumnFiltersChange: setColumnFilters as OnChangeFn<ColumnFiltersState>,
144
+ onColumnVisibilityChange: setColumnVisibility,
145
+ onPaginationChange: setPagination,
146
+ getCoreRowModel: getCoreRowModel(),
147
+ getFilteredRowModel: getFilteredRowModel(),
148
+ getPaginationRowModel: getPaginationRowModel(),
149
+ getSortedRowModel: getSortedRowModel(),
150
+ getFacetedRowModel: getFacetedRowModel(),
151
+ getFacetedUniqueValues: getFacetedUniqueValues(),
152
+ });
153
+
154
+ // Selected IDs derived from table state (lint-friendly deps)
155
+ const selection = table.getState().rowSelection as Record<string, boolean>;
156
+ const selectedIds = React.useMemo<string[]>(
157
+ () => Object.keys(selection).filter((id) => selection[id]),
158
+ [selection],
159
+ );
160
+
161
+ // Refresh local data when server data changes
162
+ React.useEffect(() => {
163
+ setData(initialData);
164
+ setIsInitialLoad(false);
165
+ }, [initialData]);
166
+
167
+ // Show skeleton when paginating (not on first paint)
168
+ React.useEffect(() => {
169
+ const changed = pagination.pageIndex !== prevPageIndex.current;
170
+ if (!isInitialLoad && changed) {
171
+ setIsTableLoading(true);
172
+ prevPageIndex.current = pagination.pageIndex;
173
+ }
174
+ }, [pagination.pageIndex, isInitialLoad]);
175
+
176
+ React.useEffect(() => {
177
+ if (!isTableLoading) return;
178
+ const t = setTimeout(() => setIsTableLoading(false), 400);
179
+ return () => clearTimeout(t);
180
+ }, [isTableLoading]);
181
+
182
+ // Bulk delete
183
+ const handleDeleteMany = React.useCallback(async () => {
184
+ if (!onDeleteMany || selectedIds.length === 0) return;
185
+
186
+ toast.promise(
187
+ onDeleteMany(selectedIds).then(() => {
188
+ table.resetRowSelection();
189
+ setIsDialogOpen(false);
190
+ }),
191
+ {
192
+ loading: "Deleting selected items...",
193
+ success: () => {
194
+ {{#if framework == "nextjs"}}
195
+ router.refresh();
196
+ {{/if}}
197
+ return `Successfully deleted ${selectedIds.length} item${
198
+ selectedIds.length > 1 ? "s" : ""
199
+ }`;
200
+ },
201
+ error: (err) => (err?.message as string) || "Failed to delete items",
202
+ },
203
+ );
204
+ {{#if framework != "nextjs"}}
205
+ }, [onDeleteMany, selectedIds, table]);
206
+ {{else}}
207
+ }, [onDeleteMany, selectedIds, table, router]);
208
+ {{/if}}
209
+
210
+ return (
211
+ <Tabs
212
+ defaultValue="outline"
213
+ className="w-full flex-col justify-start gap-6"
214
+ >
215
+ {/* Toolbar */}
216
+ <div
217
+ className={cn(
218
+ "grid items-center gap-3 sm:grid-cols-[1fr_auto]",
219
+ hasHeaderFooter ? "" : "hidden",
220
+ )}
221
+ >
222
+ {/* LEFT */}
223
+ <div className="flex flex-wrap items-center gap-2">
224
+ <DataTableColumnSelector table={table} />
225
+
226
+ {actionModal && (
227
+ <Dialog>
228
+ <DialogTrigger>
229
+ <Button variant="outline" className="h-8">
230
+ <IconPlus size={16} />
231
+ <span>{actionModal.label}</span>
232
+ </Button>
233
+ </DialogTrigger>
234
+ <DialogContent className="max-h-[80vh] max-w-150 overflow-y-auto">
235
+ {actionModal.title && (
236
+ <DialogHeader>
237
+ <DialogTitle className="text-center text-2xl">
238
+ {actionModal.title}
239
+ </DialogTitle>
240
+ </DialogHeader>
241
+ )}
242
+ {actionModal.form}
243
+ </DialogContent>
244
+ </Dialog>
245
+ )}
246
+
247
+ {/* Bulk Delete */}
248
+ {onDeleteMany && selectedIds.length > 0 && (
249
+ <Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
250
+ <DialogTrigger>
251
+ <Button variant="destructive" size="sm" className="h-8">
252
+ Delete ({selectedIds.length})
253
+ </Button>
254
+ </DialogTrigger>
255
+ <DialogContent>
256
+ <DialogHeader>
257
+ <DialogTitle>Delete Selected Items</DialogTitle>
258
+ <DialogDescription>
259
+ Are you sure you want to delete {selectedIds.length}{" "}
260
+ selected item{selectedIds.length > 1 ? "s" : ""}? This
261
+ action cannot be undone.
262
+ </DialogDescription>
263
+ </DialogHeader>
264
+ <DialogFooter>
265
+ <Button
266
+ variant="outline"
267
+ onClick={() => setIsDialogOpen(false)}
268
+ >
269
+ Cancel
270
+ </Button>
271
+ <Button variant="destructive" onClick={handleDeleteMany}>
272
+ Yes, Delete {selectedIds.length > 1 ? "All" : "It"}
273
+ </Button>
274
+ </DialogFooter>
275
+ </DialogContent>
276
+ </Dialog>
277
+ )}
278
+ </div>
279
+
280
+ {/* RIGHT */}
281
+ <div className="flex items-center justify-end gap-2">
282
+ {actionLink && (
283
+ <Button variant="outline">
284
+ {{#if framework == "nextjs"}}
285
+ <Link href={actionLink.href} className="flex items-center gap-2">
286
+ {{else}}
287
+ <Link to={actionLink.href} className="flex items-center gap-2">
288
+ {{/if}}
289
+ <IconPlus size={16} />
290
+ <span>{actionLink.label}</span>
291
+ </Link>
292
+ </Button>
293
+ )}
294
+ {rightSite && rightSite}
295
+ </div>
296
+ </div>
297
+
298
+ {/* Table */}
299
+ <TabsContent
300
+ value="outline"
301
+ className="relative flex flex-col gap-4 overflow-auto rounded-lg"
302
+ >
303
+ <div className="bg-light overflow-hidden rounded-lg border dark:bg-transparent">
304
+ <Table>
305
+ <TableHeader className="bg-light-bg/50 dark:bg-dark-hover sticky top-0 z-10">
306
+ {table.getHeaderGroups().map((hg) => (
307
+ <TableRow key={hg.id}>
308
+ {hg.headers.map((header) => (
309
+ <TableHead key={header.id} colSpan={header.colSpan}>
310
+ {header.isPlaceholder
311
+ ? null
312
+ : flexRender(
313
+ header.column.columnDef.header,
314
+ header.getContext(),
315
+ )}
316
+ </TableHead>
317
+ ))}
318
+ </TableRow>
319
+ ))}
320
+ </TableHeader>
321
+
322
+ <TableBody className="**:data-[slot=table-cell]:first:w-8">
323
+ {isTableLoading ? (
324
+ Array.from({ length: pagination.pageSize }).map((_, i) => (
325
+ <TableRow key={i as number}>
326
+ <TableCell colSpan={columns.length}>
327
+ <Skeleton className="h-5 w-full" />
328
+ </TableCell>
329
+ </TableRow>
330
+ ))
331
+ ) : data.length > 0 ? (
332
+ table.getRowModel().rows.map((row) => (
333
+ <TableRow
334
+ key={row.id}
335
+ data-state={row.getIsSelected() && "selected"}
336
+ className="relative"
337
+ >
338
+ {row.getVisibleCells().map((cell) => (
339
+ <TableCell key={cell.id}>
340
+ {flexRender(
341
+ cell.column.columnDef.cell,
342
+ cell.getContext(),
343
+ )}
344
+ </TableCell>
345
+ ))}
346
+ </TableRow>
347
+ ))
348
+ ) : (
349
+ <TableRow>
350
+ <TableCell
351
+ colSpan={columns.length}
352
+ className="h-24 text-center"
353
+ >
354
+ No results.
355
+ </TableCell>
356
+ </TableRow>
357
+ )}
358
+ </TableBody>
359
+ </Table>
360
+ </div>
361
+
362
+ <div className={cn(hasHeaderFooter ? "" : "hidden")}>
363
+ <DataTableFooter
364
+ table={table}
365
+ pageSize={pagination.pageSize}
366
+ total={total}
367
+ />
368
+ </div>
369
+ </TabsContent>
370
+ </Tabs>
371
+ );
372
+ }
373
+
374
+ /** Selection checkbox column */
375
+ // eslint-disable-next-line react-refresh/only-export-components
376
+ export function createSelectionColumn<T extends BaseRecord>() {
377
+ return {
378
+ id: "select",
379
+ header: ({ table }: { table: TableInstance<T> }) => (
380
+ <div className="flex items-center justify-center">
381
+ <Checkbox
382
+ checked={table.getIsAllPageRowsSelected()}
383
+ indeterminate={
384
+ table.getIsSomePageRowsSelected() &&
385
+ !table.getIsAllPageRowsSelected()
386
+ }
387
+ onCheckedChange={(v) => table.toggleAllPageRowsSelected(!!v)}
388
+ aria-label="Select all"
389
+ className="border-border"
390
+ />
391
+ </div>
392
+ ),
393
+ cell: ({ row }: { row: Row<T> }) => (
394
+ <div className="flex items-center justify-center">
395
+ <Checkbox
396
+ checked={row.getIsSelected()}
397
+ onCheckedChange={(v) => row.toggleSelected(!!v)}
398
+ aria-label="Select row"
399
+ />
400
+ </div>
401
+ ),
402
+ enableSorting: false,
403
+ enableHiding: false,
404
+ } as ColumnDef<T>;
405
+ }