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.
- package/README.md +50 -42
- package/dist/cli/add.js +122 -56
- package/dist/cli/create.d.ts +2 -0
- package/dist/cli/create.js +271 -95
- package/dist/cli/doctor.js +1 -0
- package/dist/cli/list.d.ts +1 -1
- package/dist/cli/list.js +6 -4
- package/dist/index.js +234 -191
- package/dist/lib/constants.d.ts +4 -0
- package/dist/lib/constants.js +4 -0
- package/dist/lib/discovery/module-discovery.d.ts +4 -0
- package/dist/lib/discovery/module-discovery.js +56 -0
- package/dist/lib/generation/code-generator.d.ts +11 -2
- package/dist/lib/generation/code-generator.js +42 -3
- package/dist/lib/generation/generator-utils.js +3 -1
- package/dist/lib/pm/package-manager.js +16 -13
- package/dist/lib/ui/logger.js +3 -2
- package/dist/lib/utils/path-resolver.d.ts +2 -0
- package/dist/lib/utils/path-resolver.js +8 -0
- package/dist/meta.json +8312 -0
- package/modules/auth/better-auth/files/{shared → express}/config/env.ts +48 -50
- package/modules/auth/better-auth/files/express/middlewares/authorize.ts +20 -1
- package/modules/auth/better-auth/files/express/modules/auth.controller.ts +349 -0
- package/modules/auth/better-auth/files/express/modules/{auth/auth.route.ts → auth.route.ts} +9 -4
- package/modules/auth/better-auth/files/express/modules/auth.service.ts +664 -0
- package/modules/auth/better-auth/files/express/modules/{auth/auth.type.ts → auth.type.ts} +22 -9
- package/modules/auth/better-auth/files/{shared/mongoose/auth/helper.ts → express/mongo-modules/auth.helper.ts} +11 -1
- package/modules/auth/better-auth/files/express/types/express.d.ts +11 -0
- package/modules/auth/better-auth/files/nextjs/api-route.ts +74 -0
- package/modules/auth/better-auth/files/nextjs/dashboard/pages/(user)/page.tsx +6 -0
- package/modules/auth/better-auth/files/nextjs/dashboard/pages/admin/page.tsx +6 -0
- package/modules/auth/better-auth/files/nextjs/dashboard/pages/layout.tsx +48 -0
- package/modules/auth/better-auth/files/nextjs/dashboard/pages/my-profile/page.tsx +5 -0
- package/modules/auth/better-auth/files/nextjs/features/services/auth.service.ts +102 -0
- package/modules/auth/better-auth/files/nextjs/layout/layout.tsx +13 -0
- package/modules/auth/better-auth/files/nextjs/lib/axios/http.ts +158 -0
- package/modules/auth/better-auth/files/nextjs/lib/env.ts +35 -0
- package/modules/auth/better-auth/files/nextjs/lib/utils/auth.ts +75 -0
- package/modules/auth/better-auth/files/nextjs/lib/utils/cookie.ts +29 -0
- package/modules/auth/better-auth/files/nextjs/lib/utils/jwt.ts +28 -0
- package/modules/auth/better-auth/files/nextjs/lib/utils/token.ts +49 -0
- package/modules/auth/better-auth/files/nextjs/pages/forgot-password/page.tsx +5 -0
- package/modules/auth/better-auth/files/nextjs/pages/layout.tsx +11 -0
- package/modules/auth/better-auth/files/nextjs/pages/login/page.tsx +9 -0
- package/modules/auth/better-auth/files/nextjs/pages/register/page.tsx +5 -0
- package/modules/auth/better-auth/files/nextjs/pages/reset-password/page.tsx +10 -0
- package/modules/auth/better-auth/files/nextjs/pages/verify-email/page.tsx +10 -0
- package/modules/auth/better-auth/files/nextjs/proxy.ts +154 -42
- package/modules/auth/better-auth/files/nextjs/theme/providers/theme-provider.tsx +11 -0
- package/modules/auth/better-auth/files/nextjs/types/api.types.ts +18 -0
- package/modules/auth/better-auth/files/react/components/protected-route.tsx +39 -0
- package/modules/auth/better-auth/files/react/components/route-guards.tsx +13 -0
- package/modules/auth/better-auth/files/react/dashboard/admin/pages/overview.tsx +3 -0
- package/modules/auth/better-auth/files/react/dashboard/pages/overview.tsx +3 -0
- package/modules/auth/better-auth/files/react/features/pages/forgot-password.tsx +5 -0
- package/modules/auth/better-auth/files/react/features/pages/login.tsx +5 -0
- package/modules/auth/better-auth/files/react/features/pages/my-profile.tsx +5 -0
- package/modules/auth/better-auth/files/react/features/pages/oauth-callback.tsx +59 -0
- package/modules/auth/better-auth/files/react/features/pages/register.tsx +5 -0
- package/modules/auth/better-auth/files/react/features/pages/reset-password.tsx +10 -0
- package/modules/auth/better-auth/files/react/features/pages/verify-email.tsx +10 -0
- package/modules/auth/better-auth/files/react/layout/dashboard-layout.tsx +54 -0
- package/modules/auth/better-auth/files/react/lib/axios/http.ts +68 -0
- package/modules/auth/better-auth/files/react/lib/env.ts +25 -0
- package/modules/auth/better-auth/files/react/router.tsx +73 -0
- package/modules/auth/better-auth/files/react/theme/components/providers/theme-provider-context.ts +13 -0
- package/modules/auth/better-auth/files/react/theme/components/providers/theme-provider.tsx +51 -0
- package/modules/auth/better-auth/files/react/theme/hooks/use-theme.ts +8 -0
- package/modules/auth/better-auth/files/shared/features/components/change-password-dialog.tsx +113 -0
- package/modules/auth/better-auth/files/shared/features/components/forgot-password-form.tsx +84 -0
- package/modules/auth/better-auth/files/shared/features/components/login-form.tsx +134 -0
- package/modules/auth/better-auth/files/shared/features/components/my-profile.tsx +147 -0
- package/modules/auth/better-auth/files/shared/features/components/profile-form.tsx +205 -0
- package/modules/auth/better-auth/files/shared/features/components/register-form.tsx +100 -0
- package/modules/auth/better-auth/files/shared/features/components/reset-password-form.tsx +111 -0
- package/modules/auth/better-auth/files/shared/features/components/social-login-buttons.tsx +47 -0
- package/modules/auth/better-auth/files/shared/features/components/user-profile-menu.tsx +106 -0
- package/modules/auth/better-auth/files/shared/features/components/verify-email-form.tsx +110 -0
- package/modules/auth/better-auth/files/shared/features/queries/auth.mutations.tsx +312 -0
- package/modules/auth/better-auth/files/shared/features/queries/auth.querie.ts +19 -0
- package/modules/auth/better-auth/files/shared/features/services/auth.api.ts +81 -0
- package/modules/auth/better-auth/files/shared/features/types/auth.type.ts +47 -0
- package/modules/auth/better-auth/files/shared/features/validators/change-password.validator.ts +18 -0
- package/modules/auth/better-auth/files/shared/features/validators/forgot.validator.ts +7 -0
- package/modules/auth/better-auth/files/shared/features/validators/login.validator.ts +14 -0
- package/modules/auth/better-auth/files/shared/features/validators/profile.validator.ts +8 -0
- package/modules/auth/better-auth/files/shared/features/validators/register.validator.ts +9 -0
- package/modules/auth/better-auth/files/shared/features/validators/reset.validator.ts +9 -0
- package/modules/auth/better-auth/files/shared/features/validators/verify.validator.ts +8 -0
- package/modules/auth/better-auth/files/shared/lib/auth-client.ts +2 -1
- package/modules/auth/better-auth/files/shared/lib/auth.ts +5 -19
- package/modules/auth/better-auth/files/shared/lib/constant/dashboard.ts +90 -0
- package/modules/auth/better-auth/files/shared/theme/mode-toggle.tsx +30 -0
- package/modules/auth/better-auth/files/shared/ui/shadcn/components/dashboard/dashboard-header.tsx +94 -0
- package/modules/auth/better-auth/files/shared/ui/shadcn/components/dashboard/dashboard-sidebar.tsx +255 -0
- package/modules/auth/better-auth/files/shared/ui/shadcn/components/footer.tsx +35 -0
- package/modules/auth/better-auth/files/shared/ui/shadcn/components/navbar.tsx +145 -0
- package/modules/auth/better-auth/files/shared/ui/shadcn/form-field/input-field.tsx +440 -0
- package/modules/auth/better-auth/files/shared/utils/email.ts +2 -17
- package/modules/auth/better-auth/generator.json +172 -51
- package/modules/auth/better-auth/module.json +2 -2
- package/modules/components/files/shared/hooks/use-file-upload.ts +412 -0
- package/modules/components/files/shared/lib/utils/url-helpers.ts +110 -0
- package/modules/components/files/shared/shadcn/dashboard/data-table-column-selector.tsx +52 -0
- package/modules/components/files/shared/shadcn/dashboard/data-table-footer.tsx +156 -0
- package/modules/components/files/shared/shadcn/dashboard/data-table.tsx +405 -0
- package/modules/components/files/shared/shadcn/global/form-field/input-field.tsx +440 -0
- package/modules/components/files/shared/shadcn/global/form-field/media-uploader-field.tsx +745 -0
- package/modules/components/files/shared/shadcn/global/form-field/multi-select-field.tsx +207 -0
- package/modules/components/files/shared/shadcn/global/form-field/select-field.tsx +247 -0
- package/modules/components/files/shared/shadcn/global/form-field/textarea-field.tsx +277 -0
- package/modules/components/files/shared/shadcn/global/form-field/tiptap-editor-field.tsx +35 -0
- package/modules/components/files/shared/shadcn/global/no-results.tsx +41 -0
- package/modules/components/files/shared/shadcn/tiptap-editor/editor-menu-bar.tsx +217 -0
- package/modules/components/files/shared/shadcn/tiptap-editor/tiptap-editor.tsx +104 -0
- package/modules/components/files/shared/url/load-more.tsx +93 -0
- package/modules/components/files/shared/url/search-bar.tsx +131 -0
- package/modules/components/files/shared/url/sort-select.tsx +118 -0
- package/modules/components/files/shared/url/url-tabs.tsx +77 -0
- package/modules/components/generator.json +109 -0
- package/modules/components/module.json +11 -0
- package/modules/database/mongoose/generator.json +3 -14
- package/modules/database/mongoose/module.json +2 -2
- package/modules/database/prisma/generator.json +6 -12
- package/modules/database/prisma/module.json +2 -2
- package/modules/storage/cloudinary/files/express/config/env.ts +65 -0
- package/modules/storage/cloudinary/files/express/config/media.ts +103 -0
- package/modules/storage/cloudinary/files/express/modules/media/media.controller.ts +59 -0
- package/modules/storage/cloudinary/files/express/modules/media/media.route.ts +29 -0
- package/modules/storage/cloudinary/files/express/modules/media/media.service.ts +113 -0
- package/modules/storage/cloudinary/files/express/modules/media/media.type.ts +32 -0
- package/modules/storage/cloudinary/generator.json +34 -0
- package/modules/storage/cloudinary/module.json +11 -0
- package/modules/ui/shadcn/generator.json +21 -0
- package/modules/ui/shadcn/module.json +11 -0
- package/package.json +24 -26
- package/templates/express/README.md +11 -16
- package/templates/express/src/config/env.ts +7 -5
- package/templates/nextjs/README.md +13 -18
- package/templates/nextjs/app/favicon.ico +0 -0
- package/templates/nextjs/app/layout.tsx +6 -4
- package/templates/nextjs/components/providers/query-provider.tsx +3 -0
- package/templates/nextjs/env.example +3 -1
- package/templates/nextjs/lib/axios/http.ts +23 -0
- package/templates/nextjs/lib/env.ts +7 -5
- package/templates/nextjs/package.json +2 -1
- package/templates/nextjs/template.json +1 -2
- package/templates/react/README.md +9 -14
- package/templates/react/index.html +1 -1
- package/templates/react/package.json +1 -1
- package/templates/react/src/assets/favicon.ico +0 -0
- package/templates/react/src/components/providers/query-provider.tsx +38 -0
- package/templates/react/src/{shared/components → components}/seo.tsx +4 -8
- package/templates/react/src/lib/axios/http.ts +24 -0
- package/templates/react/src/main.tsx +8 -11
- package/templates/react/src/{features/about/pages → pages}/about.tsx +1 -1
- package/templates/react/src/{features/home/pages → pages}/home.tsx +1 -1
- package/templates/react/src/router.tsx +6 -6
- package/templates/react/src/vite-env.d.ts +2 -1
- package/templates/react/template.json +0 -1
- package/templates/react/tsconfig.app.json +6 -0
- package/templates/react/tsconfig.json +7 -1
- package/templates/react/vite.config.ts +12 -0
- package/modules/auth/authjs/files/nextjs/api/auth/[...nextauth]/route.ts +0 -3
- package/modules/auth/authjs/files/nextjs/proxy.ts +0 -1
- package/modules/auth/authjs/files/shared/lib/auth.ts +0 -119
- package/modules/auth/authjs/files/shared/prisma/schema.prisma +0 -61
- package/modules/auth/authjs/generator.json +0 -64
- package/modules/auth/authjs/module.json +0 -13
- package/modules/auth/better-auth/files/express/modules/auth/auth.controller.ts +0 -264
- package/modules/auth/better-auth/files/express/modules/auth/auth.service.ts +0 -549
- package/modules/auth/better-auth/files/express/templates/google-redirect.ejs +0 -24
- package/modules/auth/better-auth/files/nextjs/api/auth/[...all]/route.ts +0 -4
- package/modules/auth/better-auth/files/nextjs/lib/auth/auth-guards.ts +0 -31
- package/modules/auth/better-auth/files/nextjs/templates/email-otp.tsx +0 -74
- package/templates/nextjs/lib/api/http.ts +0 -40
- package/templates/react/public/vite.svg +0 -1
- package/templates/react/src/app/layouts/dashboard-layout.tsx +0 -8
- package/templates/react/src/app/layouts/public-layout.tsx +0 -5
- package/templates/react/src/app/providers.tsx +0 -20
- package/templates/react/src/app/router.tsx +0 -21
- package/templates/react/src/assets/react.svg +0 -1
- package/templates/react/src/shared/api/http.ts +0 -39
- package/templates/react/src/shared/components/loading.tsx +0 -8
- package/templates/react/src/shared/lib/query-client.ts +0 -12
- package/templates/react/src/utils/storage.ts +0 -35
- package/templates/react/src/utils/utils.ts +0 -3
- /package/modules/auth/better-auth/files/{shared/mongoose/auth/constants.ts → express/mongo-modules/auth.constants.ts} +0 -0
- /package/templates/nextjs/app/{page.tsx → (public)/(root)/page.tsx} +0 -0
- /package/templates/react/src/{shared/components → components}/error-boundary.tsx +0 -0
- /package/templates/react/src/{shared/components → components}/layout.tsx +0 -0
- /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
|
+
}
|