stackkit 0.3.4 → 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 (203) 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 -52
  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} +12 -7
  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/constants.ts → express/mongo-modules/auth.constants.ts} +0 -1
  28. package/modules/auth/better-auth/files/{shared/mongoose/auth/helper.ts → express/mongo-modules/auth.helper.ts} +11 -1
  29. package/modules/auth/better-auth/files/express/types/express.d.ts +11 -0
  30. package/modules/auth/better-auth/files/nextjs/api-route.ts +74 -0
  31. package/modules/auth/better-auth/files/nextjs/dashboard/pages/(user)/page.tsx +6 -0
  32. package/modules/auth/better-auth/files/nextjs/dashboard/pages/admin/page.tsx +6 -0
  33. package/modules/auth/better-auth/files/nextjs/dashboard/pages/layout.tsx +48 -0
  34. package/modules/auth/better-auth/files/nextjs/dashboard/pages/my-profile/page.tsx +5 -0
  35. package/modules/auth/better-auth/files/nextjs/features/services/auth.service.ts +102 -0
  36. package/modules/auth/better-auth/files/nextjs/layout/layout.tsx +13 -0
  37. package/modules/auth/better-auth/files/nextjs/lib/axios/http.ts +158 -0
  38. package/modules/auth/better-auth/files/nextjs/lib/env.ts +35 -0
  39. package/modules/auth/better-auth/files/nextjs/lib/utils/auth.ts +75 -0
  40. package/modules/auth/better-auth/files/nextjs/lib/utils/cookie.ts +29 -0
  41. package/modules/auth/better-auth/files/nextjs/lib/utils/jwt.ts +28 -0
  42. package/modules/auth/better-auth/files/nextjs/lib/utils/token.ts +49 -0
  43. package/modules/auth/better-auth/files/nextjs/pages/forgot-password/page.tsx +5 -0
  44. package/modules/auth/better-auth/files/nextjs/pages/layout.tsx +11 -0
  45. package/modules/auth/better-auth/files/nextjs/pages/login/page.tsx +9 -0
  46. package/modules/auth/better-auth/files/nextjs/pages/register/page.tsx +5 -0
  47. package/modules/auth/better-auth/files/nextjs/pages/reset-password/page.tsx +10 -0
  48. package/modules/auth/better-auth/files/nextjs/pages/verify-email/page.tsx +10 -0
  49. package/modules/auth/better-auth/files/nextjs/proxy.ts +157 -22
  50. package/modules/auth/better-auth/files/nextjs/theme/providers/theme-provider.tsx +11 -0
  51. package/modules/auth/better-auth/files/nextjs/types/api.types.ts +18 -0
  52. package/modules/auth/better-auth/files/react/components/protected-route.tsx +39 -0
  53. package/modules/auth/better-auth/files/react/components/route-guards.tsx +13 -0
  54. package/modules/auth/better-auth/files/react/dashboard/admin/pages/overview.tsx +3 -0
  55. package/modules/auth/better-auth/files/react/dashboard/pages/overview.tsx +3 -0
  56. package/modules/auth/better-auth/files/react/features/pages/forgot-password.tsx +5 -0
  57. package/modules/auth/better-auth/files/react/features/pages/login.tsx +5 -0
  58. package/modules/auth/better-auth/files/react/features/pages/my-profile.tsx +5 -0
  59. package/modules/auth/better-auth/files/react/features/pages/oauth-callback.tsx +59 -0
  60. package/modules/auth/better-auth/files/react/features/pages/register.tsx +5 -0
  61. package/modules/auth/better-auth/files/react/features/pages/reset-password.tsx +10 -0
  62. package/modules/auth/better-auth/files/react/features/pages/verify-email.tsx +10 -0
  63. package/modules/auth/better-auth/files/react/layout/dashboard-layout.tsx +54 -0
  64. package/modules/auth/better-auth/files/react/lib/axios/http.ts +68 -0
  65. package/modules/auth/better-auth/files/react/lib/env.ts +25 -0
  66. package/modules/auth/better-auth/files/react/router.tsx +73 -0
  67. package/modules/auth/better-auth/files/react/theme/components/providers/theme-provider-context.ts +13 -0
  68. package/modules/auth/better-auth/files/react/theme/components/providers/theme-provider.tsx +51 -0
  69. package/modules/auth/better-auth/files/react/theme/hooks/use-theme.ts +8 -0
  70. package/modules/auth/better-auth/files/shared/features/components/change-password-dialog.tsx +113 -0
  71. package/modules/auth/better-auth/files/shared/features/components/forgot-password-form.tsx +84 -0
  72. package/modules/auth/better-auth/files/shared/features/components/login-form.tsx +134 -0
  73. package/modules/auth/better-auth/files/shared/features/components/my-profile.tsx +147 -0
  74. package/modules/auth/better-auth/files/shared/features/components/profile-form.tsx +205 -0
  75. package/modules/auth/better-auth/files/shared/features/components/register-form.tsx +100 -0
  76. package/modules/auth/better-auth/files/shared/features/components/reset-password-form.tsx +111 -0
  77. package/modules/auth/better-auth/files/shared/features/components/social-login-buttons.tsx +47 -0
  78. package/modules/auth/better-auth/files/shared/features/components/user-profile-menu.tsx +106 -0
  79. package/modules/auth/better-auth/files/shared/features/components/verify-email-form.tsx +110 -0
  80. package/modules/auth/better-auth/files/shared/features/queries/auth.mutations.tsx +312 -0
  81. package/modules/auth/better-auth/files/shared/features/queries/auth.querie.ts +19 -0
  82. package/modules/auth/better-auth/files/shared/features/services/auth.api.ts +81 -0
  83. package/modules/auth/better-auth/files/shared/features/types/auth.type.ts +47 -0
  84. package/modules/auth/better-auth/files/shared/features/validators/change-password.validator.ts +18 -0
  85. package/modules/auth/better-auth/files/shared/features/validators/forgot.validator.ts +7 -0
  86. package/modules/auth/better-auth/files/shared/features/validators/login.validator.ts +14 -0
  87. package/modules/auth/better-auth/files/shared/features/validators/profile.validator.ts +8 -0
  88. package/modules/auth/better-auth/files/shared/features/validators/register.validator.ts +9 -0
  89. package/modules/auth/better-auth/files/shared/features/validators/reset.validator.ts +9 -0
  90. package/modules/auth/better-auth/files/shared/features/validators/verify.validator.ts +8 -0
  91. package/modules/auth/better-auth/files/shared/lib/auth-client.ts +2 -1
  92. package/modules/auth/better-auth/files/shared/lib/auth.ts +10 -29
  93. package/modules/auth/better-auth/files/shared/lib/constant/dashboard.ts +90 -0
  94. package/modules/auth/better-auth/files/shared/prisma/enums.prisma +0 -1
  95. package/modules/auth/better-auth/files/shared/theme/mode-toggle.tsx +30 -0
  96. package/modules/auth/better-auth/files/shared/ui/shadcn/components/dashboard/dashboard-header.tsx +94 -0
  97. package/modules/auth/better-auth/files/shared/ui/shadcn/components/dashboard/dashboard-sidebar.tsx +255 -0
  98. package/modules/auth/better-auth/files/shared/ui/shadcn/components/footer.tsx +35 -0
  99. package/modules/auth/better-auth/files/shared/ui/shadcn/components/navbar.tsx +145 -0
  100. package/modules/auth/better-auth/files/shared/ui/shadcn/form-field/input-field.tsx +440 -0
  101. package/modules/auth/better-auth/files/shared/utils/email.ts +20 -18
  102. package/modules/auth/better-auth/generator.json +174 -53
  103. package/modules/auth/better-auth/module.json +2 -2
  104. package/modules/components/files/shared/hooks/use-file-upload.ts +412 -0
  105. package/modules/components/files/shared/lib/utils/url-helpers.ts +110 -0
  106. package/modules/components/files/shared/shadcn/dashboard/data-table-column-selector.tsx +52 -0
  107. package/modules/components/files/shared/shadcn/dashboard/data-table-footer.tsx +156 -0
  108. package/modules/components/files/shared/shadcn/dashboard/data-table.tsx +405 -0
  109. package/modules/components/files/shared/shadcn/global/form-field/input-field.tsx +440 -0
  110. package/modules/components/files/shared/shadcn/global/form-field/media-uploader-field.tsx +745 -0
  111. package/modules/components/files/shared/shadcn/global/form-field/multi-select-field.tsx +207 -0
  112. package/modules/components/files/shared/shadcn/global/form-field/select-field.tsx +247 -0
  113. package/modules/components/files/shared/shadcn/global/form-field/textarea-field.tsx +277 -0
  114. package/modules/components/files/shared/shadcn/global/form-field/tiptap-editor-field.tsx +35 -0
  115. package/modules/components/files/shared/shadcn/global/no-results.tsx +41 -0
  116. package/modules/components/files/shared/shadcn/tiptap-editor/editor-menu-bar.tsx +217 -0
  117. package/modules/components/files/shared/shadcn/tiptap-editor/tiptap-editor.tsx +104 -0
  118. package/modules/components/files/shared/url/load-more.tsx +93 -0
  119. package/modules/components/files/shared/url/search-bar.tsx +131 -0
  120. package/modules/components/files/shared/url/sort-select.tsx +118 -0
  121. package/modules/components/files/shared/url/url-tabs.tsx +77 -0
  122. package/modules/components/generator.json +109 -0
  123. package/modules/components/module.json +11 -0
  124. package/modules/database/mongoose/generator.json +3 -14
  125. package/modules/database/mongoose/module.json +2 -2
  126. package/modules/database/prisma/generator.json +6 -12
  127. package/modules/database/prisma/module.json +2 -2
  128. package/modules/storage/cloudinary/files/express/config/env.ts +65 -0
  129. package/modules/storage/cloudinary/files/express/config/media.ts +103 -0
  130. package/modules/storage/cloudinary/files/express/modules/media/media.controller.ts +59 -0
  131. package/modules/storage/cloudinary/files/express/modules/media/media.route.ts +29 -0
  132. package/modules/storage/cloudinary/files/express/modules/media/media.service.ts +113 -0
  133. package/modules/storage/cloudinary/files/express/modules/media/media.type.ts +32 -0
  134. package/modules/storage/cloudinary/generator.json +34 -0
  135. package/modules/storage/cloudinary/module.json +11 -0
  136. package/modules/ui/shadcn/generator.json +21 -0
  137. package/modules/ui/shadcn/module.json +11 -0
  138. package/package.json +24 -26
  139. package/templates/express/README.md +11 -16
  140. package/templates/express/src/config/env.ts +7 -5
  141. package/templates/nextjs/README.md +13 -18
  142. package/templates/nextjs/app/favicon.ico +0 -0
  143. package/templates/nextjs/app/layout.tsx +6 -4
  144. package/templates/nextjs/components/providers/query-provider.tsx +3 -0
  145. package/templates/nextjs/env.example +3 -1
  146. package/templates/nextjs/lib/axios/http.ts +23 -0
  147. package/templates/nextjs/lib/env.ts +7 -5
  148. package/templates/nextjs/package.json +2 -1
  149. package/templates/nextjs/template.json +1 -2
  150. package/templates/react/README.md +9 -14
  151. package/templates/react/index.html +1 -1
  152. package/templates/react/package.json +1 -1
  153. package/templates/react/src/assets/favicon.ico +0 -0
  154. package/templates/react/src/components/providers/query-provider.tsx +38 -0
  155. package/templates/react/src/{shared/components → components}/seo.tsx +4 -8
  156. package/templates/react/src/lib/axios/http.ts +24 -0
  157. package/templates/react/src/main.tsx +8 -11
  158. package/templates/react/src/{features/about/pages → pages}/about.tsx +1 -1
  159. package/templates/react/src/{features/home/pages → pages}/home.tsx +1 -1
  160. package/templates/react/src/router.tsx +6 -6
  161. package/templates/react/src/vite-env.d.ts +2 -1
  162. package/templates/react/template.json +0 -1
  163. package/templates/react/tsconfig.app.json +6 -0
  164. package/templates/react/tsconfig.json +7 -1
  165. package/templates/react/vite.config.ts +12 -0
  166. package/modules/auth/authjs/files/nextjs/api/auth/[...nextauth]/route.ts +0 -3
  167. package/modules/auth/authjs/files/nextjs/proxy.ts +0 -1
  168. package/modules/auth/authjs/files/shared/lib/auth.ts +0 -119
  169. package/modules/auth/authjs/files/shared/prisma/schema.prisma +0 -61
  170. package/modules/auth/authjs/generator.json +0 -64
  171. package/modules/auth/authjs/module.json +0 -13
  172. package/modules/auth/better-auth/files/express/modules/auth/auth.controller.ts +0 -264
  173. package/modules/auth/better-auth/files/express/modules/auth/auth.service.ts +0 -537
  174. package/modules/auth/better-auth/files/express/templates/google-redirect.ejs +0 -24
  175. package/modules/auth/better-auth/files/nextjs/api/auth/[...all]/route.ts +0 -4
  176. package/modules/auth/better-auth/files/nextjs/lib/auth/auth-guards.ts +0 -41
  177. package/modules/auth/better-auth/files/nextjs/templates/email-otp.tsx +0 -74
  178. package/templates/express/node_modules/.bin/acorn +0 -17
  179. package/templates/express/node_modules/.bin/eslint +0 -17
  180. package/templates/express/node_modules/.bin/tsc +0 -17
  181. package/templates/express/node_modules/.bin/tsserver +0 -17
  182. package/templates/express/node_modules/.bin/tsx +0 -17
  183. package/templates/nextjs/lib/api/http.ts +0 -40
  184. package/templates/nextjs/next-env.d.ts +0 -6
  185. package/templates/react/dist/assets/index-D4AHT4dU.js +0 -193
  186. package/templates/react/dist/assets/index-rpwj5ZOX.css +0 -1
  187. package/templates/react/dist/index.html +0 -14
  188. package/templates/react/dist/vite.svg +0 -1
  189. package/templates/react/public/vite.svg +0 -1
  190. package/templates/react/src/app/layouts/dashboard-layout.tsx +0 -8
  191. package/templates/react/src/app/layouts/public-layout.tsx +0 -5
  192. package/templates/react/src/app/providers.tsx +0 -20
  193. package/templates/react/src/app/router.tsx +0 -21
  194. package/templates/react/src/assets/react.svg +0 -1
  195. package/templates/react/src/shared/api/http.ts +0 -39
  196. package/templates/react/src/shared/components/loading.tsx +0 -8
  197. package/templates/react/src/shared/lib/query-client.ts +0 -12
  198. package/templates/react/src/utils/storage.ts +0 -35
  199. package/templates/react/src/utils/utils.ts +0 -3
  200. /package/templates/nextjs/app/{page.tsx → (public)/(root)/page.tsx} +0 -0
  201. /package/templates/react/src/{shared/components → components}/error-boundary.tsx +0 -0
  202. /package/templates/react/src/{shared/components → components}/layout.tsx +0 -0
  203. /package/templates/react/src/{shared/pages → pages}/not-found.tsx +0 -0
@@ -0,0 +1,412 @@
1
+ "use client";
2
+
3
+ import type React from "react";
4
+ import {
5
+ type ChangeEvent,
6
+ type DragEvent,
7
+ type InputHTMLAttributes,
8
+ useCallback,
9
+ useRef,
10
+ useState,
11
+ } from "react";
12
+
13
+ export type FileMetadata = {
14
+ name: string;
15
+ size: number;
16
+ type: string;
17
+ url: string;
18
+ id: string;
19
+ };
20
+
21
+ export type FileWithPreview = {
22
+ file: File | FileMetadata;
23
+ id: string;
24
+ preview?: string;
25
+ };
26
+
27
+ export type FileUploadOptions = {
28
+ maxFiles?: number; // Only used when multiple is true, defaults to Infinity
29
+ maxSize?: number; // in bytes
30
+ accept?: string;
31
+ multiple?: boolean; // Defaults to false
32
+ initialFiles?: FileMetadata[];
33
+ onFilesChange?: (files: FileWithPreview[]) => void; // Callback when files change
34
+ onFilesAdded?: (addedFiles: FileWithPreview[]) => void; // Callback when new files are added
35
+ };
36
+
37
+ export type FileUploadState = {
38
+ files: FileWithPreview[];
39
+ isDragging: boolean;
40
+ errors: string[];
41
+ };
42
+
43
+ export type FileUploadActions = {
44
+ addFiles: (files: FileList | File[]) => void;
45
+ removeFile: (id: string) => void;
46
+ clearFiles: () => void;
47
+ clearErrors: () => void;
48
+ handleDragEnter: (e: DragEvent<HTMLElement>) => void;
49
+ handleDragLeave: (e: DragEvent<HTMLElement>) => void;
50
+ handleDragOver: (e: DragEvent<HTMLElement>) => void;
51
+ handleDrop: (e: DragEvent<HTMLElement>) => void;
52
+ handleFileChange: (e: ChangeEvent<HTMLInputElement>) => void;
53
+ openFileDialog: () => void;
54
+ getInputProps: (
55
+ props?: InputHTMLAttributes<HTMLInputElement>,
56
+ ) => InputHTMLAttributes<HTMLInputElement> & {
57
+ ref: React.Ref<HTMLInputElement>;
58
+ };
59
+ };
60
+
61
+ export const useFileUpload = (
62
+ options: FileUploadOptions = {},
63
+ ): [FileUploadState, FileUploadActions] => {
64
+ const {
65
+ maxFiles = Infinity,
66
+ maxSize = Infinity,
67
+ accept = "*",
68
+ multiple = false,
69
+ initialFiles = [],
70
+ onFilesChange,
71
+ onFilesAdded,
72
+ } = options;
73
+
74
+ const [state, setState] = useState<FileUploadState>({
75
+ files: initialFiles.map((file) => ({
76
+ file,
77
+ id: file.id,
78
+ preview: file.url,
79
+ })),
80
+ isDragging: false,
81
+ errors: [],
82
+ });
83
+
84
+ const inputRef = useRef<HTMLInputElement>(null);
85
+
86
+ const validateFile = useCallback(
87
+ (file: File | FileMetadata): string | null => {
88
+ if (file instanceof File) {
89
+ if (file.size > maxSize) {
90
+ return `File "${file.name}" exceeds the maximum size of ${formatBytes(maxSize)}.`;
91
+ }
92
+ } else {
93
+ if (file.size > maxSize) {
94
+ return `File "${file.name}" exceeds the maximum size of ${formatBytes(maxSize)}.`;
95
+ }
96
+ }
97
+
98
+ if (accept !== "*") {
99
+ const acceptedTypes = accept.split(",").map((type) => type.trim());
100
+ const fileType = file instanceof File ? file.type || "" : file.type;
101
+ const fileExtension = `.${file instanceof File ? file.name.split(".").pop() : file.name.split(".").pop()}`;
102
+
103
+ const isAccepted = acceptedTypes.some((type) => {
104
+ if (type.startsWith(".")) {
105
+ return fileExtension.toLowerCase() === type.toLowerCase();
106
+ }
107
+ if (type.endsWith("/*")) {
108
+ const baseType = type.split("/")[0];
109
+ return fileType.startsWith(`${baseType}/`);
110
+ }
111
+ return fileType === type;
112
+ });
113
+
114
+ if (!isAccepted) {
115
+ return `File "${file instanceof File ? file.name : file.name}" is not an accepted file type.`;
116
+ }
117
+ }
118
+
119
+ return null;
120
+ },
121
+ [accept, maxSize],
122
+ );
123
+
124
+ const createPreview = useCallback(
125
+ (file: File | FileMetadata): string | undefined => {
126
+ if (file instanceof File) {
127
+ return URL.createObjectURL(file);
128
+ }
129
+ return file.url;
130
+ },
131
+ [],
132
+ );
133
+
134
+ const generateUniqueId = useCallback((file: File | FileMetadata): string => {
135
+ if (file instanceof File) {
136
+ return `${file.name}-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
137
+ }
138
+ return file.id;
139
+ }, []);
140
+
141
+ const clearFiles = useCallback(() => {
142
+ setState((prev) => {
143
+ // Clean up object URLs
144
+ prev.files.forEach((file) => {
145
+ if (
146
+ file.preview &&
147
+ file.file instanceof File &&
148
+ file.file.type.startsWith("image/")
149
+ ) {
150
+ URL.revokeObjectURL(file.preview);
151
+ }
152
+ });
153
+
154
+ if (inputRef.current) {
155
+ inputRef.current.value = "";
156
+ }
157
+
158
+ const newState = {
159
+ ...prev,
160
+ files: [],
161
+ errors: [],
162
+ };
163
+
164
+ onFilesChange?.(newState.files);
165
+ return newState;
166
+ });
167
+ }, [onFilesChange]);
168
+
169
+ const addFiles = useCallback(
170
+ (newFiles: FileList | File[]) => {
171
+ if (!newFiles || newFiles.length === 0) return;
172
+
173
+ const newFilesArray = Array.from(newFiles);
174
+ const errors: string[] = [];
175
+
176
+ // Clear existing errors when new files are uploaded
177
+ setState((prev) => ({ ...prev, errors: [] }));
178
+
179
+ // In single file mode, clear existing files first
180
+ if (!multiple) {
181
+ clearFiles();
182
+ }
183
+
184
+ // Check if adding these files would exceed maxFiles (only in multiple mode)
185
+ if (
186
+ multiple &&
187
+ maxFiles !== Infinity &&
188
+ state.files.length + newFilesArray.length > maxFiles
189
+ ) {
190
+ errors.push(`You can only upload a maximum of ${maxFiles} files.`);
191
+ setState((prev) => ({ ...prev, errors }));
192
+ return;
193
+ }
194
+
195
+ const validFiles: FileWithPreview[] = [];
196
+
197
+ newFilesArray.forEach((file) => {
198
+ // Only check for duplicates if multiple files are allowed
199
+ if (multiple) {
200
+ const isDuplicate = state.files.some(
201
+ (existingFile) =>
202
+ existingFile.file.name === file.name &&
203
+ existingFile.file.size === file.size,
204
+ );
205
+
206
+ // Skip duplicate files silently
207
+ if (isDuplicate) {
208
+ return;
209
+ }
210
+ }
211
+
212
+ // Check file size
213
+ if (file.size > maxSize) {
214
+ errors.push(
215
+ multiple
216
+ ? `Some files exceed the maximum size of ${formatBytes(maxSize)}.`
217
+ : `File exceeds the maximum size of ${formatBytes(maxSize)}.`,
218
+ );
219
+ return;
220
+ }
221
+
222
+ const error = validateFile(file);
223
+ if (error) {
224
+ errors.push(error);
225
+ } else {
226
+ validFiles.push({
227
+ file,
228
+ id: generateUniqueId(file),
229
+ preview: createPreview(file),
230
+ });
231
+ }
232
+ });
233
+
234
+ // Only update state if we have valid files to add
235
+ if (validFiles.length > 0) {
236
+ // Call the onFilesAdded callback with the newly added valid files
237
+ onFilesAdded?.(validFiles);
238
+
239
+ setState((prev) => {
240
+ const newFiles = !multiple
241
+ ? validFiles
242
+ : [...prev.files, ...validFiles];
243
+ onFilesChange?.(newFiles);
244
+ return {
245
+ ...prev,
246
+ files: newFiles,
247
+ errors,
248
+ };
249
+ });
250
+ } else if (errors.length > 0) {
251
+ setState((prev) => ({
252
+ ...prev,
253
+ errors,
254
+ }));
255
+ }
256
+
257
+ // Reset input value after handling files
258
+ if (inputRef.current) {
259
+ inputRef.current.value = "";
260
+ }
261
+ },
262
+ [
263
+ state.files,
264
+ maxFiles,
265
+ multiple,
266
+ maxSize,
267
+ validateFile,
268
+ createPreview,
269
+ generateUniqueId,
270
+ clearFiles,
271
+ onFilesChange,
272
+ onFilesAdded,
273
+ ],
274
+ );
275
+
276
+ const removeFile = useCallback(
277
+ (id: string) => {
278
+ setState((prev) => {
279
+ const fileToRemove = prev.files.find((file) => file.id === id);
280
+ if (
281
+ fileToRemove?.preview &&
282
+ fileToRemove.file instanceof File &&
283
+ fileToRemove.file.type.startsWith("image/")
284
+ ) {
285
+ URL.revokeObjectURL(fileToRemove.preview);
286
+ }
287
+
288
+ const newFiles = prev.files.filter((file) => file.id !== id);
289
+ onFilesChange?.(newFiles);
290
+
291
+ return {
292
+ ...prev,
293
+ files: newFiles,
294
+ errors: [],
295
+ };
296
+ });
297
+ },
298
+ [onFilesChange],
299
+ );
300
+
301
+ const clearErrors = useCallback(() => {
302
+ setState((prev) => ({
303
+ ...prev,
304
+ errors: [],
305
+ }));
306
+ }, []);
307
+
308
+ const handleDragEnter = useCallback((e: DragEvent<HTMLElement>) => {
309
+ e.preventDefault();
310
+ e.stopPropagation();
311
+ setState((prev) => ({ ...prev, isDragging: true }));
312
+ }, []);
313
+
314
+ const handleDragLeave = useCallback((e: DragEvent<HTMLElement>) => {
315
+ e.preventDefault();
316
+ e.stopPropagation();
317
+
318
+ if (e.currentTarget.contains(e.relatedTarget as Node)) {
319
+ return;
320
+ }
321
+
322
+ setState((prev) => ({ ...prev, isDragging: false }));
323
+ }, []);
324
+
325
+ const handleDragOver = useCallback((e: DragEvent<HTMLElement>) => {
326
+ e.preventDefault();
327
+ e.stopPropagation();
328
+ }, []);
329
+
330
+ const handleDrop = useCallback(
331
+ (e: DragEvent<HTMLElement>) => {
332
+ e.preventDefault();
333
+ e.stopPropagation();
334
+ setState((prev) => ({ ...prev, isDragging: false }));
335
+
336
+ // Don't process files if the input is disabled
337
+ if (inputRef.current?.disabled) {
338
+ return;
339
+ }
340
+
341
+ if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
342
+ // In single file mode, only use the first file
343
+ if (!multiple) {
344
+ const file = e.dataTransfer.files[0];
345
+ addFiles([file]);
346
+ } else {
347
+ addFiles(e.dataTransfer.files);
348
+ }
349
+ }
350
+ },
351
+ [addFiles, multiple],
352
+ );
353
+
354
+ const handleFileChange = useCallback(
355
+ (e: ChangeEvent<HTMLInputElement>) => {
356
+ if (e.target.files && e.target.files.length > 0) {
357
+ addFiles(e.target.files);
358
+ }
359
+ },
360
+ [addFiles],
361
+ );
362
+
363
+ const openFileDialog = useCallback(() => {
364
+ if (inputRef.current) {
365
+ inputRef.current.click();
366
+ }
367
+ }, []);
368
+
369
+ const getInputProps = useCallback(
370
+ (props: InputHTMLAttributes<HTMLInputElement> = {}) => {
371
+ return {
372
+ ...props,
373
+ type: "file" as const,
374
+ onChange: handleFileChange,
375
+ accept: props.accept || accept,
376
+ multiple: props.multiple !== undefined ? props.multiple : multiple,
377
+ ref: inputRef,
378
+ };
379
+ },
380
+ [accept, multiple, handleFileChange],
381
+ );
382
+
383
+ return [
384
+ state,
385
+ {
386
+ addFiles,
387
+ removeFile,
388
+ clearFiles,
389
+ clearErrors,
390
+ handleDragEnter,
391
+ handleDragLeave,
392
+ handleDragOver,
393
+ handleDrop,
394
+ handleFileChange,
395
+ openFileDialog,
396
+ getInputProps,
397
+ },
398
+ ];
399
+ };
400
+
401
+ // Helper function to format bytes to human-readable format
402
+ export const formatBytes = (bytes: number, decimals = 2): string => {
403
+ if (bytes === 0) return "0 Bytes";
404
+
405
+ const k = 1024;
406
+ const dm = decimals < 0 ? 0 : decimals;
407
+ const sizes = ["Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
408
+
409
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
410
+
411
+ return Number.parseFloat((bytes / k ** i).toFixed(dm)) + sizes[i];
412
+ };
@@ -0,0 +1,110 @@
1
+ type QSValue = string | undefined;
2
+
3
+ function normalizeQS(qs: string): string {
4
+ const s = new URLSearchParams(qs || "");
5
+ const entries = Array.from(s.entries()).sort(([a], [b]) =>
6
+ a.localeCompare(b),
7
+ );
8
+ const sorted = new URLSearchParams(entries);
9
+ return sorted.toString();
10
+ }
11
+
12
+ export function setQuery(
13
+ prevQS: string,
14
+ pairs: Record<string, QSValue>,
15
+ ): string {
16
+ const s = new URLSearchParams(prevQS || "");
17
+ Object.entries(pairs).forEach(([k, v]) => {
18
+ if (v === undefined || v === "") s.delete(k);
19
+ else s.set(k, v);
20
+ });
21
+ return s.toString();
22
+ }
23
+
24
+ export function clearQuery(prevQS: string, keys: string[]): string {
25
+ const s = new URLSearchParams(prevQS || "");
26
+ keys.forEach((k) => {
27
+ s.delete(k);
28
+ });
29
+ return s.toString();
30
+ }
31
+
32
+ export function isSameQuery(a: string, b: string): boolean {
33
+ return normalizeQS(a) === normalizeQS(b);
34
+ }
35
+
36
+ export function withPageReset(
37
+ pairs: Record<string, QSValue>,
38
+ ): Record<string, QSValue> {
39
+ return { ...pairs };
40
+ }
41
+
42
+ export function formUrlQuery({
43
+ params,
44
+ key,
45
+ value,
46
+ }: {
47
+ params: string;
48
+ key: string;
49
+ value: string | null;
50
+ }): string {
51
+ const s = new URLSearchParams(params || "");
52
+ if (value === null || value === "") s.delete(key);
53
+ else s.set(key, value);
54
+ const base = typeof window !== "undefined" ? window.location.pathname : "";
55
+ const qs = s.toString();
56
+ return qs ? `${base}?${qs}` : base;
57
+ }
58
+
59
+ export function removeKeysFromQuery({
60
+ params,
61
+ keysToRemove,
62
+ }: {
63
+ params: string;
64
+ keysToRemove: string[];
65
+ }): string {
66
+ const s = new URLSearchParams(params || "");
67
+ keysToRemove.forEach((k) => {
68
+ s.delete(k);
69
+ });
70
+ const base = typeof window !== "undefined" ? window.location.pathname : "";
71
+ const qs = s.toString();
72
+ return qs ? `${base}?${qs}` : base;
73
+ }
74
+
75
+ export const toYMD = (d?: Date) =>
76
+ d ? new Date(d).toISOString().slice(0, 10) : undefined;
77
+
78
+ export function getCsvParam(sp: URLSearchParams | null, key: string): string[] {
79
+ if (!sp) return [];
80
+ const v = sp.get(key);
81
+ return v
82
+ ? v
83
+ .split(",")
84
+ .map((s) => s.trim())
85
+ .filter(Boolean)
86
+ : [];
87
+ }
88
+
89
+ export function setCsvParam(
90
+ prevQS: string,
91
+ key: string,
92
+ values: string[],
93
+ resetPage = true,
94
+ ): string {
95
+ const pairs: Record<string, string | undefined> = {
96
+ [key]: values.length ? values.join(",") : undefined,
97
+ };
98
+ return resetPage
99
+ ? setQuery(prevQS, withPageReset(pairs))
100
+ : setQuery(prevQS, pairs);
101
+ }
102
+
103
+ export function arraysShallowEqual(
104
+ a: readonly string[],
105
+ b: readonly string[],
106
+ ): boolean {
107
+ if (a.length !== b.length) return false;
108
+ for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
109
+ return true;
110
+ }
@@ -0,0 +1,52 @@
1
+ import { IconChevronDown } from "@tabler/icons-react";
2
+ import type { Table } from "@tanstack/react-table";
3
+ import { Columns3Icon } from "lucide-react";
4
+ import { Button } from "../ui/button";
5
+ import {
6
+ DropdownMenu,
7
+ DropdownMenuCheckboxItem,
8
+ DropdownMenuContent,
9
+ DropdownMenuTrigger,
10
+ } from "../ui/dropdown-menu";
11
+ import type { BaseRecord } from "./data-table";
12
+
13
+ interface DataTableColumnSelectorProps<TData extends BaseRecord> {
14
+ table: Table<TData>;
15
+ }
16
+
17
+ export default function DataTableColumnSelector<TData extends BaseRecord>({
18
+ table,
19
+ }: DataTableColumnSelectorProps<TData>) {
20
+ return (
21
+ <DropdownMenu>
22
+ <DropdownMenuTrigger>
23
+ <Button variant="outline">
24
+ <Columns3Icon />
25
+ <span className="hidden lg:inline">View</span>
26
+ <span className="lg:hidden">Columns</span>
27
+ <IconChevronDown />
28
+ </Button>
29
+ </DropdownMenuTrigger>
30
+ <DropdownMenuContent align="end" className="w-56">
31
+ {table
32
+ .getAllColumns()
33
+ .filter(
34
+ (column) =>
35
+ typeof column.accessorFn !== "undefined" && column.getCanHide(),
36
+ )
37
+ .map((column) => {
38
+ return (
39
+ <DropdownMenuCheckboxItem
40
+ key={column.id}
41
+ className="capitalize"
42
+ checked={column.getIsVisible()}
43
+ onCheckedChange={(value) => column.toggleVisibility(!!value)}
44
+ >
45
+ {column.id}
46
+ </DropdownMenuCheckboxItem>
47
+ );
48
+ })}
49
+ </DropdownMenuContent>
50
+ </DropdownMenu>
51
+ );
52
+ }