proje-react-panel 1.0.14 → 1.0.16

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 (168) hide show
  1. package/.cursor/rules.md +122 -0
  2. package/.cursor/settings.json +57 -0
  3. package/.eslintrc.js +5 -0
  4. package/.eslintrc.json +26 -0
  5. package/.prettierrc +10 -0
  6. package/.vscode/launch.json +27 -0
  7. package/.vscode/settings.json +8 -0
  8. package/PTD.md +234 -0
  9. package/README.md +62 -28
  10. package/dist/api/CrudApi.d.ts +12 -0
  11. package/dist/components/Panel.d.ts +2 -2
  12. package/dist/components/components/Checkbox.d.ts +6 -0
  13. package/dist/components/components/Counter.d.ts +9 -0
  14. package/dist/components/components/FormField.d.ts +13 -0
  15. package/dist/components/components/ImageUploader.d.ts +15 -0
  16. package/dist/components/components/InnerForm.d.ts +12 -0
  17. package/dist/components/components/LoadingScreen.d.ts +2 -0
  18. package/dist/components/components/index.d.ts +8 -0
  19. package/dist/components/components/list/Datagrid.d.ts +13 -0
  20. package/dist/components/components/list/EmptyList.d.ts +2 -0
  21. package/dist/components/components/list/FilterPopup.d.ts +11 -0
  22. package/dist/components/components/list/ListPage.d.ts +22 -0
  23. package/dist/components/components/list/Pagination.d.ts +11 -0
  24. package/dist/components/components/list/index.d.ts +0 -0
  25. package/dist/components/layout/Layout.d.ts +2 -1
  26. package/dist/components/layout/SideBar.d.ts +4 -3
  27. package/dist/components/layout/index.d.ts +2 -0
  28. package/dist/components/list/Datagrid.d.ts +12 -0
  29. package/dist/components/list/EmptyList.d.ts +2 -0
  30. package/dist/components/list/FilterPopup.d.ts +10 -0
  31. package/dist/components/list/Pagination.d.ts +11 -0
  32. package/dist/components/list/index.d.ts +0 -0
  33. package/dist/{src/screens → components/pages}/ControllerDetails.d.ts +1 -1
  34. package/dist/components/pages/FormPage.d.ts +12 -0
  35. package/dist/components/pages/ListPage.d.ts +18 -0
  36. package/dist/components/pages/Login.d.ts +13 -0
  37. package/dist/decorators/form/Form.d.ts +6 -0
  38. package/dist/decorators/form/FormOptions.d.ts +7 -0
  39. package/dist/decorators/form/Input.d.ts +17 -0
  40. package/dist/decorators/form/getFormFields.d.ts +3 -0
  41. package/dist/decorators/list/Cell.d.ts +21 -0
  42. package/dist/decorators/list/GetCellFields.d.ts +2 -0
  43. package/dist/decorators/list/ImageCell.d.ts +6 -0
  44. package/dist/decorators/list/List.d.ts +28 -0
  45. package/dist/decorators/list/ListData.d.ts +6 -0
  46. package/dist/decorators/list/getListFields.d.ts +2 -0
  47. package/dist/index.cjs.js +1 -1
  48. package/dist/index.d.ts +20 -10
  49. package/dist/index.esm.js +1 -1
  50. package/dist/initPanel.d.ts +2 -2
  51. package/dist/store/store.d.ts +1 -5
  52. package/dist/types/AnyClass.d.ts +1 -0
  53. package/dist/types/ScreenCreatorData.d.ts +5 -3
  54. package/dist/types/initPanelOptions.d.ts +0 -6
  55. package/dist/utils/format.d.ts +1 -0
  56. package/dist/utils/getFields.d.ts +2 -1
  57. package/package.json +13 -6
  58. package/src/api/CrudApi.ts +30 -11
  59. package/src/assets/icons/svg/create.svg +9 -0
  60. package/src/assets/icons/svg/filter.svg +3 -0
  61. package/src/assets/icons/svg/pencil.svg +8 -0
  62. package/src/assets/icons/svg/search.svg +8 -0
  63. package/src/assets/icons/svg/trash.svg +8 -0
  64. package/src/components/Panel.tsx +11 -11
  65. package/src/components/components/Checkbox.tsx +9 -0
  66. package/src/components/components/Counter.tsx +51 -0
  67. package/src/components/components/FormField.tsx +94 -0
  68. package/src/components/components/ImageUploader.tsx +301 -0
  69. package/src/components/components/InnerForm.tsx +74 -0
  70. package/src/components/components/LoadingScreen.tsx +12 -0
  71. package/src/components/components/index.ts +8 -0
  72. package/src/components/components/list/Datagrid.tsx +121 -0
  73. package/src/components/components/list/EmptyList.tsx +26 -0
  74. package/src/components/components/list/FilterPopup.tsx +202 -0
  75. package/src/components/components/list/ListPage.tsx +178 -0
  76. package/src/components/components/list/Pagination.tsx +110 -0
  77. package/src/components/components/list/index.ts +1 -0
  78. package/src/components/layout/Layout.tsx +8 -1
  79. package/src/components/layout/SideBar.tsx +103 -31
  80. package/src/components/layout/index.ts +2 -0
  81. package/src/components/pages/ControllerDetails.tsx +37 -0
  82. package/src/components/pages/FormPage.tsx +34 -0
  83. package/src/components/pages/Login.tsx +79 -0
  84. package/src/decorators/form/Form.ts +18 -0
  85. package/src/decorators/form/FormOptions.ts +8 -0
  86. package/src/decorators/form/Input.ts +53 -0
  87. package/src/decorators/form/getFormFields.ts +13 -0
  88. package/src/decorators/list/Cell.ts +32 -0
  89. package/src/decorators/list/GetCellFields.ts +13 -0
  90. package/src/decorators/list/ImageCell.ts +13 -0
  91. package/src/decorators/list/List.ts +31 -0
  92. package/src/decorators/list/ListData.ts +7 -0
  93. package/src/decorators/list/getListFields.ts +10 -0
  94. package/src/index.ts +28 -10
  95. package/src/initPanel.ts +4 -12
  96. package/src/store/store.ts +23 -28
  97. package/src/styles/counter.scss +42 -0
  98. package/src/styles/filter-popup.scss +134 -0
  99. package/src/styles/image-uploader.scss +94 -0
  100. package/src/styles/index.scss +26 -7
  101. package/src/styles/layout.scss +1 -6
  102. package/src/styles/list.scss +175 -7
  103. package/src/styles/loading-screen.scss +42 -0
  104. package/src/styles/pagination.scss +66 -0
  105. package/src/styles/sidebar.scss +64 -0
  106. package/src/styles/utils/scrollbar.scss +19 -0
  107. package/src/types/AnyClass.ts +1 -0
  108. package/src/types/ScreenCreatorData.ts +5 -3
  109. package/src/types/initPanelOptions.ts +1 -7
  110. package/src/types/svg.d.ts +5 -0
  111. package/src/utils/format.ts +7 -0
  112. package/src/utils/getFields.ts +11 -9
  113. package/dist/api/crudApi.d.ts +0 -17
  114. package/dist/components/Form.d.ts +0 -6
  115. package/dist/components/FormField.d.ts +0 -13
  116. package/dist/components/list/List.d.ts +0 -10
  117. package/dist/components/screens/ControllerCreate.d.ts +0 -5
  118. package/dist/components/screens/ControllerDetails.d.ts +0 -5
  119. package/dist/components/screens/ControllerEdit.d.ts +0 -5
  120. package/dist/components/screens/ControllerList.d.ts +0 -5
  121. package/dist/components/screens/Login.d.ts +0 -2
  122. package/dist/decorators/Cell.d.ts +0 -9
  123. package/dist/decorators/Input.d.ts +0 -13
  124. package/dist/hooks/useScreens.d.ts +0 -2
  125. package/dist/initPanelOptions.d.ts +0 -8
  126. package/dist/screens/ControllerCreate.d.ts +0 -5
  127. package/dist/screens/ControllerDetails.d.ts +0 -5
  128. package/dist/screens/ControllerEdit.d.ts +0 -5
  129. package/dist/screens/ControllerList.d.ts +0 -5
  130. package/dist/screens/Form.d.ts +0 -6
  131. package/dist/src/api/crudApi.d.ts +0 -6
  132. package/dist/src/components/Panel.d.ts +0 -9
  133. package/dist/src/components/layout/Layout.d.ts +0 -11
  134. package/dist/src/components/layout/SideBar.d.ts +0 -10
  135. package/dist/src/components/list/List.d.ts +0 -10
  136. package/dist/src/decorators/Cell.d.ts +0 -10
  137. package/dist/src/decorators/Crud.d.ts +0 -6
  138. package/dist/src/index.d.ts +0 -8
  139. package/dist/src/screens/ControllerCreate.d.ts +0 -5
  140. package/dist/src/screens/ControllerEdit.d.ts +0 -5
  141. package/dist/src/screens/ControllerList.d.ts +0 -5
  142. package/dist/src/screens/Form.d.ts +0 -6
  143. package/dist/src/store/store.d.ts +0 -19
  144. package/dist/src/types/Screen.d.ts +0 -4
  145. package/dist/src/types/ScreenCreatorData.d.ts +0 -8
  146. package/dist/src/utils/createScreens.d.ts +0 -1
  147. package/dist/src/utils/getFields.d.ts +0 -2
  148. package/dist/src/utils/getScreens.d.ts +0 -2
  149. package/dist/utils/crudScreens.d.ts +0 -2
  150. package/dist/utils/getScreens.d.ts +0 -2
  151. package/src/api/AuthApi.ts +0 -14
  152. package/src/components/Form.tsx +0 -70
  153. package/src/components/FormField.tsx +0 -60
  154. package/src/components/list/List.tsx +0 -81
  155. package/src/components/screens/ControllerCreate.tsx +0 -7
  156. package/src/components/screens/ControllerDetails.tsx +0 -40
  157. package/src/components/screens/ControllerEdit.tsx +0 -35
  158. package/src/components/screens/ControllerList.tsx +0 -45
  159. package/src/components/screens/Login.tsx +0 -68
  160. package/src/decorators/Cell.ts +0 -34
  161. package/src/decorators/Input.ts +0 -50
  162. package/src/hooks/useScreens.tsx +0 -36
  163. /package/dist/components/{ErrorBoundary.d.ts → components/ErrorBoundary.d.ts} +0 -0
  164. /package/dist/components/{ErrorComponent.d.ts → components/ErrorComponent.d.ts} +0 -0
  165. /package/dist/components/{Label.d.ts → components/Label.d.ts} +0 -0
  166. /package/src/components/{ErrorBoundary.tsx → components/ErrorBoundary.tsx} +0 -0
  167. /package/src/components/{ErrorComponent.tsx → components/ErrorComponent.tsx} +0 -0
  168. /package/src/components/{Label.tsx → components/Label.tsx} +0 -0
@@ -0,0 +1,51 @@
1
+ import React, { useState, useEffect } from 'react';
2
+
3
+ interface CounterProps {
4
+ image: React.ReactNode;
5
+ text: string;
6
+ targetNumber: number;
7
+ duration?: number;
8
+ }
9
+
10
+ export function Counter({ image, text, targetNumber, duration = 2000 }: CounterProps) {
11
+ const [count, setCount] = useState<number>(0);
12
+
13
+ useEffect(() => {
14
+ let startTime: number;
15
+ let animationFrameId: number;
16
+
17
+ const animate = (timestamp: number) => {
18
+ if (!startTime) startTime = timestamp;
19
+ const progress = timestamp - startTime;
20
+ const percentage = Math.min(progress / duration, 1);
21
+
22
+ setCount(Math.floor(percentage * targetNumber));
23
+
24
+ if (percentage < 1) {
25
+ animationFrameId = requestAnimationFrame(animate);
26
+ }
27
+ };
28
+
29
+ animationFrameId = requestAnimationFrame(animate);
30
+
31
+ return () => {
32
+ if (animationFrameId) {
33
+ cancelAnimationFrame(animationFrameId);
34
+ }
35
+ };
36
+ }, [targetNumber, duration]);
37
+
38
+ return (
39
+ <div className="counter-container">
40
+ <div className="counter-image">
41
+ {image}
42
+ </div>
43
+ <div className="counter-text">
44
+ {text}
45
+ </div>
46
+ <div className="counter-value">
47
+ {count}
48
+ </div>
49
+ </div>
50
+ );
51
+ }
@@ -0,0 +1,94 @@
1
+ import React from 'react';
2
+ import { InputOptions } from '../../decorators/form/Input';
3
+ import { Label } from './Label';
4
+ import { useFormContext, UseFormRegister } from 'react-hook-form';
5
+ import { ImageUploader } from './ImageUploader';
6
+ import { Checkbox } from './Checkbox';
7
+
8
+ interface FormFieldProps {
9
+ input: InputOptions;
10
+ register: UseFormRegister<any>;
11
+ error?: { message?: string };
12
+ baseName?: string;
13
+ }
14
+
15
+ interface NestedFormFieldsProps {
16
+ input: InputOptions;
17
+ register: UseFormRegister<any>;
18
+ }
19
+
20
+ function NestedFormFields({ input, register }: NestedFormFieldsProps) {
21
+ const form = useFormContext();
22
+ //TODO: inputOptions İnputResult seperate
23
+ const data = form.getValues(input.name!);
24
+ return (
25
+ <div>
26
+ {data?.map((value: any, index: number) => (
27
+ <div key={index}>
28
+ {input.nestedFields?.map((nestedInput: InputOptions) => (
29
+ <FormField
30
+ key={nestedInput.name?.toString() ?? ''}
31
+ baseName={input.name + '[' + index + ']'}
32
+ input={nestedInput}
33
+ register={register}
34
+ error={
35
+ input.name
36
+ ? { message: (form.formState.errors[input.name] as any)?.message }
37
+ : undefined
38
+ }
39
+ />
40
+ ))}
41
+ </div>
42
+ ))}
43
+ </div>
44
+ );
45
+ }
46
+
47
+ export function FormField({ input, register, error, baseName }: FormFieldProps) {
48
+ const fieldName = (baseName ? baseName.toString() + '.' : '') + input.name || '';
49
+ const renderField = () => {
50
+ switch (input.type) {
51
+ case 'textarea':
52
+ return <textarea {...register(fieldName)} placeholder={input.placeholder} id={fieldName} />;
53
+ case 'select':
54
+ return (
55
+ <select {...register(fieldName)} id={fieldName}>
56
+ <option value="">Select {fieldName}</option>
57
+ {input.options?.map(option => (
58
+ <option key={option.value} value={option.value}>
59
+ {option.label}
60
+ </option>
61
+ ))}
62
+ </select>
63
+ );
64
+ case 'input': {
65
+ return (
66
+ <input
67
+ type={input.inputType}
68
+ {...register(fieldName)}
69
+ placeholder={input.placeholder}
70
+ id={fieldName}
71
+ />
72
+ );
73
+ }
74
+ case 'file-upload':
75
+ return <ImageUploader />;
76
+ case 'checkbox':
77
+ return <Checkbox {...register(fieldName)} id={fieldName} />;
78
+ case 'hidden':
79
+ return <input type="hidden" {...register(fieldName)} id={fieldName} />;
80
+ case 'nested':
81
+ return <NestedFormFields input={input} register={register} />;
82
+ default:
83
+ null;
84
+ }
85
+ };
86
+
87
+ return (
88
+ <div className="form-field">
89
+ <Label htmlFor={fieldName} label={input.label} fieldName={fieldName} />
90
+ {renderField()}
91
+ {error && <span className="error-message">{error.message}</span>}
92
+ </div>
93
+ );
94
+ }
@@ -0,0 +1,301 @@
1
+ import React, { useEffect } from "react";
2
+ import { useFormContext } from "react-hook-form";
3
+ import { bytesToSize } from "../../utils/format";
4
+
5
+ interface ThumbnailImageProps {
6
+ name: string;
7
+ src: string;
8
+ size: number;
9
+ style?: React.CSSProperties;
10
+ }
11
+
12
+ interface MultipleImageUploaderProps {
13
+ value?: Array<{ file: File; image: string; remove?: boolean }>;
14
+ onError?: (error: string | null) => void;
15
+ onClear?: () => void;
16
+ reset?: any;
17
+ onFilesChange?: (files: File[]) => void;
18
+ }
19
+
20
+ interface FileWithPreview {
21
+ file: File;
22
+ image: string;
23
+ }
24
+
25
+ const uploadState = Object.freeze({
26
+ BEFORE: "before",
27
+ HOVER: "hover",
28
+ AFTER: "after",
29
+ } as const);
30
+
31
+ type UploadStateType = (typeof uploadState)[keyof typeof uploadState];
32
+
33
+ function ThumbnailImage(props: ThumbnailImageProps) {
34
+ return (
35
+ <div>
36
+ <img {...props} style={{ width: 100 }} />
37
+ <p>
38
+ {props.name} <span style={{ whiteSpace: "none" }}>({bytesToSize(props.size)})</span>
39
+ </p>
40
+ </div>
41
+ );
42
+ }
43
+
44
+ export function ImageUploader() {
45
+ const {
46
+ register,
47
+ formState: { errors },
48
+ watch,
49
+ setValue,
50
+ clearErrors,
51
+ setError,
52
+ } = useFormContext();
53
+ const up = watch("uploader");
54
+
55
+ useEffect(() => {
56
+ register("uploader", { required: true });
57
+ }, [register]);
58
+
59
+ return (
60
+ <div>
61
+ <span className="form-error" style={{ bottom: 2, top: "unset" }}>
62
+ {errors.uploader?.type === "required" && "At least 1 image is required!"}
63
+ {errors.uploader?.type === "custom" && errors.uploader.message?.toString()}
64
+ </span>
65
+ <MultipleImageUploader
66
+ reset={up}
67
+ onError={(data: string | null) => {
68
+ if (!data) {
69
+ setValue("uploader", { files: [] });
70
+ clearErrors("uploader");
71
+ } else {
72
+ setError("uploader", {
73
+ type: "custom",
74
+ message: data,
75
+ });
76
+ }
77
+ }}
78
+ onClear={() => {
79
+ setValue("uploader", { files: [] });
80
+ clearErrors("uploader");
81
+ }}
82
+ onFilesChange={(files) => {
83
+ setValue("uploader", { files });
84
+ }}
85
+ />
86
+ </div>
87
+ );
88
+ }
89
+
90
+ export function MultipleImageUploader(props: MultipleImageUploaderProps) {
91
+ const [currentUploadState, setUploadState] = React.useState<UploadStateType>(uploadState.BEFORE);
92
+ const [images, setImages] = React.useState<Array<{ file: File; image: string; remove?: boolean }>>(
93
+ props.value || []
94
+ );
95
+ const [files, setFiles] = React.useState<FileWithPreview[]>([]);
96
+ const [counter, setCounter] = React.useState(0);
97
+ console.log("files", files);
98
+ const dropzoneElement = React.useRef<HTMLDivElement>(null);
99
+ const imageInputRef = React.useRef<HTMLInputElement>(null);
100
+
101
+ React.useEffect(() => {
102
+ const element = dropzoneElement.current;
103
+ if (!element) return;
104
+
105
+ const handleDragEnter = (e: DragEvent) => {
106
+ e.preventDefault();
107
+ e.stopPropagation();
108
+ dragEnter();
109
+ };
110
+
111
+ const handleDragLeave = (e: DragEvent) => {
112
+ e.preventDefault();
113
+ e.stopPropagation();
114
+ dragLeave();
115
+ };
116
+
117
+ const handleDragOver = (e: DragEvent) => {
118
+ e.preventDefault();
119
+ e.stopPropagation();
120
+ };
121
+
122
+ const handleDrop = (e: DragEvent) => {
123
+ e.preventDefault();
124
+ e.stopPropagation();
125
+ setCounter(0);
126
+ const droppedFiles = e.dataTransfer?.files;
127
+ if (!droppedFiles) return;
128
+
129
+ setFiles([]);
130
+ setUploadState(uploadState.AFTER);
131
+
132
+ const newFiles: File[] = [];
133
+ for (let i = 0; i < droppedFiles.length; i++) {
134
+ const reader = new FileReader();
135
+ reader.onload = (event) => {
136
+ if (!event.target) return;
137
+ const check = onFileChange([
138
+ ...files,
139
+ { file: droppedFiles[i], image: event.target.result as string },
140
+ ]);
141
+ if (check) {
142
+ newFiles.push(droppedFiles[i]);
143
+ if (imageInputRef.current) {
144
+ imageInputRef.current.files = droppedFiles;
145
+ }
146
+ setFiles([]);
147
+ }
148
+ // Notify parent of file changes
149
+ if (props.onFilesChange) {
150
+ props.onFilesChange(newFiles);
151
+ }
152
+ };
153
+ reader.readAsDataURL(droppedFiles[i]);
154
+ }
155
+ if (imageInputRef.current) {
156
+ imageInputRef.current.files = droppedFiles;
157
+ }
158
+ };
159
+
160
+ element.addEventListener("dragenter", handleDragEnter, false);
161
+ element.addEventListener("dragleave", handleDragLeave, false);
162
+ element.addEventListener("dragover", handleDragOver, false);
163
+ element.addEventListener("drop", handleDrop, false);
164
+
165
+ return () => {
166
+ element.removeEventListener("dragenter", handleDragEnter);
167
+ element.removeEventListener("dragleave", handleDragLeave);
168
+ element.removeEventListener("dragover", handleDragOver);
169
+ element.removeEventListener("drop", handleDrop);
170
+ };
171
+ }, [files]);
172
+
173
+ const dragEnter = () => {
174
+ setCounter((prev) => prev + 1);
175
+ setUploadState(uploadState.HOVER);
176
+ };
177
+
178
+ const dragLeave = () => {
179
+ setCounter((prev) => {
180
+ if (prev - 1 === 0) {
181
+ setUploadState(uploadState.BEFORE);
182
+ return 0;
183
+ }
184
+ return prev - 1;
185
+ });
186
+ };
187
+
188
+ const clickRemoveImage = (i: number) => {
189
+ const sources = [...images];
190
+ sources[i].remove = !sources[i].remove;
191
+ setImages(sources);
192
+ };
193
+
194
+ const clickRemoveFile = () => {
195
+ setFiles([]);
196
+ if (imageInputRef.current) {
197
+ imageInputRef.current.value = "";
198
+ }
199
+ props.onClear?.();
200
+ };
201
+
202
+ const checkValid = (filesInner: FileWithPreview[]): string | null => {
203
+ if (!filesInner) return null;
204
+ if (filesInner.length >= 10) return "you can't send more than 10 images";
205
+ for (let i = 0; i < filesInner.length; i++) {
206
+ const file = filesInner[i].file;
207
+ const split = file.name.split(".");
208
+ if (!["png", "jpg", "jpeg"].includes(split[split.length - 1])) {
209
+ return `Extension of the file can only be "png", "jpg" or "jpeg" `;
210
+ }
211
+ if (file) {
212
+ if (file.size > 1048576) {
213
+ return `Size of "${file.name}" can't be bigger than 1mb`;
214
+ }
215
+ }
216
+ }
217
+ return null;
218
+ };
219
+
220
+ const onFileChange = (filesInner: FileWithPreview[]): string | null => {
221
+ const check = checkValid(filesInner);
222
+ if (!check) {
223
+ setFiles(filesInner);
224
+ }
225
+ props.onError?.(check);
226
+ return check;
227
+ };
228
+
229
+ const renderImages = () => {
230
+ const imageElements = [];
231
+ if (files) {
232
+ console.log("---->", files);
233
+ for (let i = 0; i < files.length; i++) {
234
+ let imageClassName = "image";
235
+ imageElements.push(
236
+ <div key={i} className="image-container">
237
+ <div className={imageClassName}>
238
+ <ThumbnailImage name={files[i].file.name} src={files[i].image} size={files[i].file.size} />
239
+ </div>
240
+ </div>
241
+ );
242
+ }
243
+ }
244
+ return imageElements;
245
+ };
246
+
247
+ return (
248
+ <div ref={dropzoneElement} className={"multi-image form-element dropzone " + currentUploadState}>
249
+ <input ref={imageInputRef} type="file" style={{ display: "none" }} className="target" name={"file"} />
250
+ <div className="container">
251
+ <button className="trash" onClick={clickRemoveFile} type="button">
252
+ Delete All
253
+ </button>
254
+ {renderImages()}
255
+ <div>
256
+ <button
257
+ type={"button"}
258
+ onClick={() => {
259
+ const fileInput = document.getElementById("file__") as HTMLInputElement;
260
+ if (fileInput) {
261
+ fileInput.click();
262
+ }
263
+ }}
264
+ className="plus">
265
+ <span>
266
+ + Add Image
267
+ <p>Drag image here or Select Image</p>
268
+ </span>
269
+ </button>
270
+ </div>
271
+ <input
272
+ hidden
273
+ id={"file__"}
274
+ multiple
275
+ type={"file"}
276
+ onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
277
+ const selectedFiles = event.target.files;
278
+ if (!selectedFiles) return;
279
+
280
+ setFiles([]);
281
+ setUploadState(uploadState.AFTER);
282
+ for (let i = 0; i < selectedFiles.length; i++) {
283
+ const reader = new FileReader();
284
+ reader.onload = (eventInner) => {
285
+ if (!eventInner.target) return;
286
+ onFileChange([
287
+ ...files,
288
+ { file: selectedFiles[i], image: eventInner.target.result as string },
289
+ ]);
290
+ };
291
+ reader.readAsDataURL(selectedFiles[i]);
292
+ }
293
+ if (imageInputRef.current) {
294
+ imageInputRef.current.files = selectedFiles;
295
+ }
296
+ }}
297
+ />
298
+ </div>
299
+ </div>
300
+ );
301
+ }
@@ -0,0 +1,74 @@
1
+ import React, { useEffect, useState } from 'react';
2
+ import { FormProvider, useForm } from 'react-hook-form';
3
+ import { InputOptions } from '../../decorators/form/Input';
4
+ import { FormField } from './FormField';
5
+ import { FormOptions } from '../../decorators/form/FormOptions';
6
+ import { AnyClass } from '../../types/AnyClass';
7
+ import { OnSubmitFN, GetDetailsDataFN } from '../pages/FormPage';
8
+ import { useParams, useNavigate } from 'react-router';
9
+
10
+ interface InnerFormProps<T extends AnyClass> {
11
+ formOptions: FormOptions;
12
+ onSubmit: OnSubmitFN<T>;
13
+ getDetailsData?: GetDetailsDataFN<T>;
14
+ redirectBackOnSuccess?: boolean;
15
+ }
16
+
17
+ export function InnerForm<T extends AnyClass>({
18
+ formOptions,
19
+ onSubmit,
20
+ getDetailsData,
21
+ redirectBackOnSuccess,
22
+ }: InnerFormProps<T>) {
23
+ const params = useParams();
24
+ const form = useForm<T>({
25
+ resolver: formOptions.resolver,
26
+ });
27
+ const navigate = useNavigate();
28
+ const inputs = formOptions.inputs;
29
+ useEffect(() => {
30
+ if (getDetailsData) {
31
+ getDetailsData(params as Record<string, string>).then(data => {
32
+ form.reset({ ...data });
33
+ });
34
+ }
35
+ }, [params, form.reset]);
36
+
37
+ return (
38
+ <div className="form-wrapper">
39
+ <FormProvider {...form}>
40
+ <form
41
+ onSubmit={form.handleSubmit(
42
+ async dataForm => {
43
+ await onSubmit(dataForm);
44
+ if (redirectBackOnSuccess) {
45
+ navigate(-1);
46
+ }
47
+ },
48
+ (errors, event) => {
49
+ console.log('error creating creation', errors, event);
50
+ }
51
+ )}
52
+ >
53
+ <div>
54
+ {inputs?.map((input: InputOptions) => (
55
+ <FormField
56
+ key={input.name || ''}
57
+ input={input}
58
+ register={form.register}
59
+ error={
60
+ input.name
61
+ ? { message: (form.formState.errors[input.name as keyof T] as any)?.message }
62
+ : undefined
63
+ }
64
+ />
65
+ ))}
66
+ <button type="submit" className="submit-button">
67
+ Submit
68
+ </button>
69
+ </div>
70
+ </form>
71
+ </FormProvider>
72
+ </div>
73
+ );
74
+ }
@@ -0,0 +1,12 @@
1
+ import React from 'react';
2
+
3
+ export function LoadingScreen() {
4
+ return (
5
+ <div className="loading-screen">
6
+ <div className="loading-container">
7
+ <div className="loading-spinner"></div>
8
+ <div className="loading-text">Loading...</div>
9
+ </div>
10
+ </div>
11
+ );
12
+ }
@@ -0,0 +1,8 @@
1
+ export { InnerForm } from './InnerForm';
2
+ export { FormField } from './FormField';
3
+ export { LoadingScreen } from './LoadingScreen';
4
+ export { Counter } from './Counter';
5
+ export { ImageUploader } from './ImageUploader';
6
+ export { ErrorComponent } from './ErrorComponent';
7
+ export { Label } from './Label';
8
+ export { ErrorBoundary } from './ErrorBoundary';
@@ -0,0 +1,121 @@
1
+ import React from 'react';
2
+ import { CellOptions } from '../../../decorators/list/Cell';
3
+ import { Link } from 'react-router';
4
+ import { useAppStore } from '../../../store/store';
5
+ import { ImageCellOptions } from '../../../decorators/list/ImageCell';
6
+ import { ListData } from '../../../decorators/list/ListData';
7
+ import { EmptyList } from './EmptyList';
8
+ import SearchIcon from '../../../assets/icons/svg/search.svg';
9
+ import PencilIcon from '../../../assets/icons/svg/pencil.svg';
10
+ import TrashIcon from '../../../assets/icons/svg/trash.svg';
11
+
12
+ interface DatagridProps<T extends { id: string }> {
13
+ data: T[];
14
+ listData: ListData;
15
+ onRemoveItem?: (item: T) => Promise<void>;
16
+ }
17
+
18
+ export function Datagrid<T extends { id: string }>({ data, listData, onRemoveItem }: DatagridProps<T>) {
19
+ const cells = listData.cells;
20
+ const utilCells = listData.list?.utilCells;
21
+
22
+ return (
23
+ <div className="datagrid">
24
+ {!data || data.length === 0 ? (
25
+ <EmptyList />
26
+ ) : (
27
+ <table className="datagrid-table">
28
+ <thead>
29
+ <tr>
30
+ {cells.map(cellOptions => (
31
+ <th key={cellOptions.name}>{cellOptions.title ?? cellOptions.name}</th>
32
+ ))}
33
+ {utilCells?.details && <th>Details</th>}
34
+ {utilCells?.edit && <th>Edit</th>}
35
+ {utilCells?.delete && <th>Delete</th>}
36
+ </tr>
37
+ </thead>
38
+ <tbody>
39
+ {data.map((item, index) => (
40
+ <tr key={index}>
41
+ {cells.map(cellOptions => {
42
+ // @ts-ignore
43
+ const value = item[cellOptions.name];
44
+ let render = value ?? '-'; // Default value if the field is undefined or null
45
+
46
+ switch (cellOptions.type) {
47
+ case 'date':
48
+ if (value) {
49
+ const date = new Date(value);
50
+ render = `${date.getDate().toString().padStart(2, '0')}/${(
51
+ date.getMonth() + 1
52
+ )
53
+ .toString()
54
+ .padStart(
55
+ 2,
56
+ '0'
57
+ )}/${date.getFullYear()} ${date.getHours().toString().padStart(2, '0')}:${date
58
+ .getMinutes()
59
+ .toString()
60
+ .padStart(2, '0')}`;
61
+ }
62
+ break;
63
+
64
+ case 'image': {
65
+ const imageCellOptions = cellOptions as ImageCellOptions;
66
+ render = (
67
+ <img
68
+ width={100}
69
+ height={100}
70
+ src={imageCellOptions.baseUrl + value}
71
+ style={{ objectFit: 'contain' }}
72
+ />
73
+ );
74
+ break;
75
+ }
76
+ case 'string':
77
+ default:
78
+ render = value ? value.toString() : (cellOptions?.placeHolder ?? '-'); // Handles string type or default fallback
79
+ break;
80
+ }
81
+ /*
82
+ if (cellOptions.linkTo) {
83
+ render = <Link to={cellOptions.linkTo(item)}>{formattedValue}</Link>;
84
+ }
85
+ */
86
+ return <td key={cellOptions.name}>{render}</td>;
87
+ })}
88
+ {utilCells?.details && (
89
+ <td>
90
+ <Link to={`${utilCells.details.path}/${item.id}`} className="util-cell-link">
91
+ <SearchIcon className="icon icon-search" />
92
+ <span className="util-cell-label">{utilCells.details.label}</span>
93
+ </Link>
94
+ </td>
95
+ )}
96
+ {utilCells?.edit && (
97
+ <td>
98
+ <Link to={`${utilCells.edit.path}/${item.id}`} className="util-cell-link">
99
+ <PencilIcon className="icon icon-pencil" />
100
+ <span className="util-cell-label">{utilCells.edit.label}</span>
101
+ </Link>
102
+ </td>
103
+ )}
104
+ {utilCells?.delete && (
105
+ <td>
106
+ <a onClick={() => {
107
+ onRemoveItem?.(item)
108
+ }} className="util-cell-link">
109
+ <TrashIcon className="icon icon-trash" />
110
+ <span className="util-cell-label">{utilCells.delete.label}</span>
111
+ </a>
112
+ </td>
113
+ )}
114
+ </tr>
115
+ ))}
116
+ </tbody>
117
+ </table>
118
+ )}
119
+ </div>
120
+ );
121
+ }
@@ -0,0 +1,26 @@
1
+ import React from 'react';
2
+
3
+ export const EmptyList: React.FC = () => {
4
+ return (
5
+ <div className="empty-list">
6
+ <div className="empty-list-content">
7
+ <svg
8
+ xmlns="http://www.w3.org/2000/svg"
9
+ width="64"
10
+ height="64"
11
+ viewBox="0 0 24 24"
12
+ fill="none"
13
+ stroke="currentColor"
14
+ strokeWidth="2"
15
+ strokeLinecap="round"
16
+ strokeLinejoin="round"
17
+ >
18
+ <path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z" />
19
+ <polyline points="13 2 13 9 20 9" />
20
+ </svg>
21
+ <h3>No Data Found</h3>
22
+ <p>There are no items to display at the moment.</p>
23
+ </div>
24
+ </div>
25
+ );
26
+ };