proje-react-panel 1.0.14 → 1.0.15

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 (149) 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 +17 -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 +12 -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/layout/Layout.d.ts +2 -1
  20. package/dist/components/layout/SideBar.d.ts +4 -3
  21. package/dist/components/layout/index.d.ts +2 -0
  22. package/dist/components/list/Datagrid.d.ts +8 -0
  23. package/dist/components/list/Pagination.d.ts +11 -0
  24. package/dist/components/list/index.d.ts +0 -0
  25. package/dist/{src/screens → components/pages}/ControllerDetails.d.ts +1 -1
  26. package/dist/components/pages/FormPage.d.ts +11 -0
  27. package/dist/components/pages/ListPage.d.ts +17 -0
  28. package/dist/components/pages/Login.d.ts +13 -0
  29. package/dist/decorators/form/Form.d.ts +6 -0
  30. package/dist/decorators/form/FormOptions.d.ts +7 -0
  31. package/dist/decorators/form/Input.d.ts +13 -0
  32. package/dist/decorators/form/getFormFields.d.ts +3 -0
  33. package/dist/decorators/{Cell.d.ts → list/Cell.d.ts} +2 -2
  34. package/dist/decorators/list/GetCellFields.d.ts +2 -0
  35. package/dist/decorators/list/ImageCell.d.ts +6 -0
  36. package/dist/decorators/list/List.d.ts +5 -0
  37. package/dist/decorators/list/ListData.d.ts +6 -0
  38. package/dist/decorators/list/getListFields.d.ts +2 -0
  39. package/dist/index.cjs.js +1 -1
  40. package/dist/index.d.ts +19 -10
  41. package/dist/index.esm.js +1 -1
  42. package/dist/initPanel.d.ts +2 -2
  43. package/dist/store/store.d.ts +1 -5
  44. package/dist/types/AnyClass.d.ts +1 -0
  45. package/dist/types/ScreenCreatorData.d.ts +5 -3
  46. package/dist/types/initPanelOptions.d.ts +0 -6
  47. package/dist/utils/format.d.ts +1 -0
  48. package/dist/utils/getFields.d.ts +2 -1
  49. package/package.json +5 -4
  50. package/src/api/CrudApi.ts +30 -11
  51. package/src/components/Panel.tsx +11 -11
  52. package/src/components/components/Checkbox.tsx +9 -0
  53. package/src/components/components/Counter.tsx +51 -0
  54. package/src/components/components/FormField.tsx +60 -0
  55. package/src/components/components/ImageUploader.tsx +301 -0
  56. package/src/components/components/InnerForm.tsx +75 -0
  57. package/src/components/components/LoadingScreen.tsx +12 -0
  58. package/src/components/components/index.ts +8 -0
  59. package/src/components/layout/Layout.tsx +8 -1
  60. package/src/components/layout/SideBar.tsx +103 -31
  61. package/src/components/layout/index.ts +2 -0
  62. package/src/components/list/Datagrid.tsx +101 -0
  63. package/src/components/list/Pagination.tsx +110 -0
  64. package/src/components/list/index.ts +1 -0
  65. package/src/components/pages/ControllerDetails.tsx +37 -0
  66. package/src/components/pages/FormPage.tsx +32 -0
  67. package/src/components/pages/ListPage.tsx +85 -0
  68. package/src/components/pages/Login.tsx +79 -0
  69. package/src/decorators/form/Form.ts +18 -0
  70. package/src/decorators/form/FormOptions.ts +8 -0
  71. package/src/decorators/form/Input.ts +52 -0
  72. package/src/decorators/form/getFormFields.ts +13 -0
  73. package/src/decorators/{Cell.ts → list/Cell.ts} +2 -14
  74. package/src/decorators/list/GetCellFields.ts +13 -0
  75. package/src/decorators/list/ImageCell.ts +13 -0
  76. package/src/decorators/list/List.ts +17 -0
  77. package/src/decorators/list/ListData.ts +7 -0
  78. package/src/decorators/list/getListFields.ts +10 -0
  79. package/src/index.ts +23 -10
  80. package/src/initPanel.ts +4 -12
  81. package/src/store/store.ts +23 -28
  82. package/src/styles/_scrollbar.scss +19 -0
  83. package/src/styles/counter.scss +42 -0
  84. package/src/styles/image-uploader.scss +94 -0
  85. package/src/styles/index.scss +30 -7
  86. package/src/styles/layout.scss +1 -6
  87. package/src/styles/list.scss +32 -5
  88. package/src/styles/loading-screen.scss +42 -0
  89. package/src/styles/pagination.scss +66 -0
  90. package/src/styles/sidebar.scss +64 -0
  91. package/src/types/AnyClass.ts +1 -0
  92. package/src/types/ScreenCreatorData.ts +5 -3
  93. package/src/types/initPanelOptions.ts +1 -7
  94. package/src/utils/format.ts +7 -0
  95. package/src/utils/getFields.ts +11 -9
  96. package/dist/api/crudApi.d.ts +0 -17
  97. package/dist/components/Form.d.ts +0 -6
  98. package/dist/components/FormField.d.ts +0 -13
  99. package/dist/components/list/List.d.ts +0 -10
  100. package/dist/components/screens/ControllerCreate.d.ts +0 -5
  101. package/dist/components/screens/ControllerDetails.d.ts +0 -5
  102. package/dist/components/screens/ControllerEdit.d.ts +0 -5
  103. package/dist/components/screens/ControllerList.d.ts +0 -5
  104. package/dist/components/screens/Login.d.ts +0 -2
  105. package/dist/decorators/Input.d.ts +0 -13
  106. package/dist/hooks/useScreens.d.ts +0 -2
  107. package/dist/initPanelOptions.d.ts +0 -8
  108. package/dist/screens/ControllerCreate.d.ts +0 -5
  109. package/dist/screens/ControllerDetails.d.ts +0 -5
  110. package/dist/screens/ControllerEdit.d.ts +0 -5
  111. package/dist/screens/ControllerList.d.ts +0 -5
  112. package/dist/screens/Form.d.ts +0 -6
  113. package/dist/src/api/crudApi.d.ts +0 -6
  114. package/dist/src/components/Panel.d.ts +0 -9
  115. package/dist/src/components/layout/Layout.d.ts +0 -11
  116. package/dist/src/components/layout/SideBar.d.ts +0 -10
  117. package/dist/src/components/list/List.d.ts +0 -10
  118. package/dist/src/decorators/Cell.d.ts +0 -10
  119. package/dist/src/decorators/Crud.d.ts +0 -6
  120. package/dist/src/index.d.ts +0 -8
  121. package/dist/src/screens/ControllerCreate.d.ts +0 -5
  122. package/dist/src/screens/ControllerEdit.d.ts +0 -5
  123. package/dist/src/screens/ControllerList.d.ts +0 -5
  124. package/dist/src/screens/Form.d.ts +0 -6
  125. package/dist/src/store/store.d.ts +0 -19
  126. package/dist/src/types/Screen.d.ts +0 -4
  127. package/dist/src/types/ScreenCreatorData.d.ts +0 -8
  128. package/dist/src/utils/createScreens.d.ts +0 -1
  129. package/dist/src/utils/getFields.d.ts +0 -2
  130. package/dist/src/utils/getScreens.d.ts +0 -2
  131. package/dist/utils/crudScreens.d.ts +0 -2
  132. package/dist/utils/getScreens.d.ts +0 -2
  133. package/src/api/AuthApi.ts +0 -14
  134. package/src/components/Form.tsx +0 -70
  135. package/src/components/FormField.tsx +0 -60
  136. package/src/components/list/List.tsx +0 -81
  137. package/src/components/screens/ControllerCreate.tsx +0 -7
  138. package/src/components/screens/ControllerDetails.tsx +0 -40
  139. package/src/components/screens/ControllerEdit.tsx +0 -35
  140. package/src/components/screens/ControllerList.tsx +0 -45
  141. package/src/components/screens/Login.tsx +0 -68
  142. package/src/decorators/Input.ts +0 -50
  143. package/src/hooks/useScreens.tsx +0 -36
  144. /package/dist/components/{ErrorBoundary.d.ts → components/ErrorBoundary.d.ts} +0 -0
  145. /package/dist/components/{ErrorComponent.d.ts → components/ErrorComponent.d.ts} +0 -0
  146. /package/dist/components/{Label.d.ts → components/Label.d.ts} +0 -0
  147. /package/src/components/{ErrorBoundary.tsx → components/ErrorBoundary.tsx} +0 -0
  148. /package/src/components/{ErrorComponent.tsx → components/ErrorComponent.tsx} +0 -0
  149. /package/src/components/{Label.tsx → components/Label.tsx} +0 -0
@@ -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,75 @@
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 { DefaultValues } from 'react-hook-form';
9
+ import { useParams } from 'react-router';
10
+
11
+ interface InnerFormProps<T extends AnyClass> {
12
+ formOptions: FormOptions;
13
+ onSubmit: OnSubmitFN<T>;
14
+ redirect?: string;
15
+ getDetailsData?: GetDetailsDataFN<T>;
16
+ }
17
+
18
+ export function InnerForm<T extends AnyClass>({
19
+ formOptions,
20
+ onSubmit,
21
+ redirect,
22
+ getDetailsData,
23
+ }: InnerFormProps<T>) {
24
+ const params = useParams();
25
+ const form = useForm<T>({
26
+ resolver: formOptions.resolver,
27
+ });
28
+
29
+ const inputs = formOptions.inputs;
30
+ useEffect(() => {
31
+ if (getDetailsData) {
32
+ getDetailsData(params.id as string).then(data => {
33
+ form.reset({ ...data });
34
+ });
35
+ }
36
+ }, [, form.reset]);
37
+
38
+ return (
39
+ <div className="form-wrapper">
40
+ <FormProvider {...form}>
41
+ <form
42
+ onSubmit={form.handleSubmit(
43
+ async dataForm => {
44
+ await onSubmit(dataForm);
45
+ if (redirect) {
46
+ window.location.href = redirect;
47
+ }
48
+ },
49
+ (errors, event) => {
50
+ console.log('error creating creation', errors, event);
51
+ }
52
+ )}
53
+ >
54
+ <div>
55
+ {inputs?.map((input: InputOptions) => (
56
+ <FormField
57
+ key={input.name || ''}
58
+ input={input}
59
+ register={form.register}
60
+ error={
61
+ input.name
62
+ ? { message: (form.formState.errors[input.name as keyof T] as any)?.message }
63
+ : undefined
64
+ }
65
+ />
66
+ ))}
67
+ <button type="submit" className="submit-button">
68
+ Submit
69
+ </button>
70
+ </div>
71
+ </form>
72
+ </FormProvider>
73
+ </div>
74
+ );
75
+ }
@@ -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';
@@ -8,15 +8,18 @@ export function Layout<IconType>({
8
8
  children,
9
9
  menu,
10
10
  getIcons,
11
+ logout,
11
12
  }: {
12
13
  children?: React.ReactNode;
13
14
  menu?: (screens: Record<string, ScreenCreatorData>) => { name: string; path: string; iconType: IconType }[];
14
15
  getIcons?: (iconType: IconType) => React.ReactNode;
16
+ logout?: () => void;
15
17
  }) {
16
18
  const { user, screenPaths } = useAppStore((s) => ({
17
19
  user: s.user,
18
20
  screenPaths: s.screenPaths,
19
21
  }));
22
+ const data = useAppStore();
20
23
  const navigate = useNavigate();
21
24
  if (!user) {
22
25
  navigate(screenPaths.login);
@@ -24,7 +27,11 @@ export function Layout<IconType>({
24
27
 
25
28
  return (
26
29
  <div className="layout">
27
- <SideBar menu={menu} getIcons={getIcons} />
30
+ <SideBar onLogout={() => {
31
+ if (logout) {
32
+ logout();
33
+ }
34
+ }} menu={menu} getIcons={getIcons} />
28
35
  <main className="content">{children}</main>
29
36
  </div>
30
37
  );
@@ -1,42 +1,114 @@
1
- import React, { useState } from "react";
2
- import { Link } from "react-router";
3
- import { ScreenCreatorData } from "../../types/ScreenCreatorData";
4
- import { useAppStore } from "../../store/store";
1
+ import React, { useState } from 'react';
2
+ import { Link, useLocation, useNavigate } from 'react-router';
3
+ import { ScreenCreatorData } from '../../types/ScreenCreatorData';
4
+ import { useAppStore } from '../../store/store';
5
5
 
6
6
  type GetMenuFunction<IconType> = (
7
- screens: Record<string, ScreenCreatorData>
7
+ screens: Record<string, ScreenCreatorData>
8
8
  ) => { name: string; path: string; iconType: IconType }[];
9
9
 
10
10
  type GetIconsFunction<IconType> = (iconType: IconType) => React.ReactNode;
11
11
 
12
12
  export function SideBar<IconType>({
13
- menu,
14
- getIcons,
13
+ menu,
14
+ getIcons,
15
+ onLogout,
15
16
  }: {
16
- menu?: GetMenuFunction<IconType>;
17
- getIcons?: GetIconsFunction<IconType>;
17
+ menu?: GetMenuFunction<IconType>;
18
+ getIcons?: GetIconsFunction<IconType>;
19
+ onLogout?: () => void;
18
20
  }) {
19
- const screens = useAppStore((s) => s.screens ?? {});
20
- const [isOpen, setIsOpen] = useState(true);
21
+ const { screens, screenPaths } = useAppStore(s => ({
22
+ screens: s.screens ?? {},
23
+ screenPaths: s.screenPaths ?? {},
24
+ }));
25
+ const [isOpen, setIsOpen] = useState(true);
26
+ const location = useLocation();
27
+ const navigate = useNavigate();
21
28
 
22
- return (
23
- <div className={`sidebar ${isOpen ? "open" : "closed"}`}>
24
- <button className="toggle-button" onClick={() => setIsOpen(!isOpen)}>
25
- {isOpen ? "<" : ">"}
26
- </button>
27
- <nav className="nav-links">
28
- {menu?.(screens).map((item, index) => (
29
- <Link key={index} to={item.path} className="nav-link">
30
- <span className={"nav-links-icon"}>{getIcons?.(item.iconType)}</span>
31
- {isOpen ? <span>{item.name}</span> : null}
32
- </Link>
33
- ))}
34
- {/*{screens.map(([key, screen], index) => (
35
- <Link key={`screen-${index}`} to={`/${key}`} className="nav-link">
36
- {isOpen ? <span>{key}</span> : null}
37
- </Link>
38
- ))}*/}
39
- </nav>
40
- </div>
41
- );
29
+ // Helper function to determine if a link is active
30
+ const isActiveLink = (path: string) => {
31
+ // For root path, we need exact match
32
+ if (path === '/') {
33
+ return location.pathname === path;
34
+ }
35
+
36
+ // Normalize paths by removing leading/trailing slashes for comparison
37
+ const normalizedPath = path.replace(/^\/+|\/+$/g, '');
38
+ const normalizedLocation = location.pathname.replace(/^\/+|\/+$/g, '');
39
+
40
+ // Check if the current path matches the link path
41
+ return (
42
+ normalizedLocation === normalizedPath || normalizedLocation.startsWith(`${normalizedPath}/`)
43
+ );
44
+ };
45
+
46
+ return (
47
+ <div className={`sidebar ${isOpen ? 'open' : 'closed'}`}>
48
+ <button
49
+ className="toggle-button"
50
+ onClick={() => setIsOpen(!isOpen)}
51
+ aria-label={isOpen ? 'Collapse sidebar' : 'Expand sidebar'}
52
+ aria-expanded={isOpen}
53
+ >
54
+ {isOpen ? '<' : '>'}
55
+ </button>
56
+ <nav className="nav-links">
57
+ {menu?.(screens).map((item, index) => (
58
+ <Link
59
+ key={index}
60
+ to={item.path}
61
+ className={`nav-link ${isActiveLink(item.path) ? 'active' : ''}`}
62
+ aria-current={isActiveLink(item.path) ? 'page' : undefined}
63
+ >
64
+ <span className={'nav-links-icon'}>{getIcons?.(item.iconType)}</span>
65
+ {isOpen ? <span>{item.name}</span> : null}
66
+ </Link>
67
+ ))}
68
+ </nav>
69
+ {onLogout && (
70
+ <div className="sidebar-footer">
71
+ <button
72
+ className="logout-button"
73
+ onClick={() => {
74
+ if (onLogout) {
75
+ onLogout();
76
+ navigate(screenPaths.login);
77
+ }
78
+ }}
79
+ aria-label="Logout"
80
+ >
81
+ <span className="nav-links-icon">
82
+ {
83
+ /*TODO: remove*/
84
+ <svg
85
+ width="16"
86
+ height="16"
87
+ viewBox="0 0 16 16"
88
+ fill="none"
89
+ xmlns="http://www.w3.org/2000/svg"
90
+ >
91
+ <path
92
+ d="M6 12H2V4H6"
93
+ stroke="currentColor"
94
+ strokeWidth="1.5"
95
+ strokeLinecap="round"
96
+ strokeLinejoin="round"
97
+ />
98
+ <path
99
+ d="M10 8L14 4M14 4L10 0M14 4H6"
100
+ stroke="currentColor"
101
+ strokeWidth="1.5"
102
+ strokeLinecap="round"
103
+ strokeLinejoin="round"
104
+ />
105
+ </svg>
106
+ }
107
+ </span>
108
+ {isOpen ? <span>Logout</span> : null}
109
+ </button>
110
+ </div>
111
+ )}
112
+ </div>
113
+ );
42
114
  }
@@ -0,0 +1,2 @@
1
+ export { SideBar } from './SideBar';
2
+ export { Layout } from './Layout';
@@ -0,0 +1,101 @@
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
+
7
+ interface ListProps<T> {
8
+ data: T[];
9
+ cells: CellOptions[];
10
+ }
11
+
12
+ export function Datagrid<T>({ data, cells }: ListProps<T>) {
13
+ if (!data || data.length === 0) {
14
+ return <div>No items available</div>;
15
+ }
16
+
17
+ return (
18
+ <div className="datagrid">
19
+ <table className="datagrid-table">
20
+ <thead>
21
+ <tr>
22
+ {cells.map(cellOptions => (
23
+ <th key={cellOptions.name}>{cellOptions.title ?? cellOptions.name}</th>
24
+ ))}
25
+ <th />
26
+ <th>Delete</th>
27
+ </tr>
28
+ </thead>
29
+ <tbody>
30
+ {data.map((item, index) => (
31
+ <tr key={index}>
32
+ {cells.map(cellOptions => {
33
+ // @ts-ignore
34
+ const value = item[cellOptions.name];
35
+ let render = value ?? '-'; // Default value if the field is undefined or null
36
+
37
+ switch (cellOptions.type) {
38
+ case 'date':
39
+ if (value) {
40
+ const date = new Date(value);
41
+ render = `${date.getDate().toString().padStart(2, '0')}/${(
42
+ date.getMonth() + 1
43
+ )
44
+ .toString()
45
+ .padStart(
46
+ 2,
47
+ '0'
48
+ )}/${date.getFullYear()} ${date.getHours().toString().padStart(2, '0')}:${date
49
+ .getMinutes()
50
+ .toString()
51
+ .padStart(2, '0')}`;
52
+ }
53
+ break;
54
+
55
+ case 'image': {
56
+ const imageCellOptions = cellOptions as ImageCellOptions;
57
+ render = (
58
+ <img
59
+ width={100}
60
+ height={100}
61
+ src={imageCellOptions.baseUrl + value}
62
+ style={{ objectFit: 'contain' }}
63
+ />
64
+ );
65
+ break;
66
+ }
67
+ case 'string':
68
+ default:
69
+ render = value ? value.toString() : (cellOptions?.placeHolder ?? '-'); // Handles string type or default fallback
70
+ break;
71
+ }
72
+ /*
73
+ if (cellOptions.linkTo) {
74
+ render = <Link to={cellOptions.linkTo(item)}>{formattedValue}</Link>;
75
+ }
76
+ */
77
+ return <td key={cellOptions.name}>{render}</td>;
78
+ })}
79
+ <td>
80
+ {/*@ts-ignore*/}
81
+ <Link to={'edit/' + (item?.id ?? '-')}>Edit</Link>
82
+ {/*@ts-ignore*/}
83
+ <Link to={'details/' + (item?.id ?? '-')}>Details</Link>
84
+ </td>
85
+ <td>
86
+ <button
87
+ onClick={() => {
88
+ /*@ts-ignore*/
89
+ //CrudApi.delete({ ...fetchSettings, token }, screen.controller, item?.id);
90
+ }}
91
+ >
92
+ Delete
93
+ </button>
94
+ </td>
95
+ </tr>
96
+ ))}
97
+ </tbody>
98
+ </table>
99
+ </div>
100
+ );
101
+ }