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,202 @@
1
+ import React, { useEffect, useMemo, useRef } from 'react';
2
+ import { ListData } from '../../../decorators/list/ListData';
3
+ import { CellOptions, StaticSelectFilter } from '../../../decorators/list/Cell';
4
+ import Select from 'react-select';
5
+
6
+ interface FilterPopupProps {
7
+ isOpen: boolean;
8
+ onClose: () => void;
9
+ onApplyFilters: (filters: Record<string, string>) => void;
10
+ listData: ListData;
11
+ activeFilters?: Record<string, string>;
12
+ }
13
+
14
+ interface FilterFieldProps {
15
+ field: CellOptions;
16
+ value: string;
17
+ onChange: (value: string) => void;
18
+ }
19
+
20
+ function FilterField({ field, value, onChange }: FilterFieldProps): React.ReactElement {
21
+ switch (field.filter?.type) {
22
+ case 'static-select': {
23
+ const filter = field.filter as StaticSelectFilter;
24
+ return (
25
+ <Select
26
+ id={field.name}
27
+ menuPortalTarget={document.body}
28
+ styles={{
29
+ control: (baseStyles, state) => ({
30
+ ...baseStyles,
31
+ backgroundColor: '#1f2937',
32
+ borderColor: state.isFocused ? '#6366f1' : '#374151',
33
+ boxShadow: state.isFocused ? '0 0 0 1px #6366f1' : 'none',
34
+ '&:hover': {
35
+ borderColor: '#6366f1',
36
+ },
37
+ borderRadius: '6px',
38
+ padding: '2px',
39
+ color: 'white',
40
+ }),
41
+ option: (baseStyles, state) => ({
42
+ ...baseStyles,
43
+ backgroundColor: state.isSelected
44
+ ? '#6366f1'
45
+ : state.isFocused
46
+ ? '#374151'
47
+ : '#1f2937',
48
+ color: 'white',
49
+ '&:active': {
50
+ backgroundColor: '#6366f1',
51
+ },
52
+ '&:hover': {
53
+ backgroundColor: '#374151',
54
+ },
55
+ cursor: 'pointer',
56
+ }),
57
+ input: baseStyles => ({
58
+ ...baseStyles,
59
+ color: 'white',
60
+ }),
61
+ placeholder: baseStyles => ({
62
+ ...baseStyles,
63
+ color: '#9ca3af',
64
+ }),
65
+ singleValue: baseStyles => ({
66
+ ...baseStyles,
67
+ color: 'white',
68
+ }),
69
+ menuPortal: baseStyles => ({
70
+ ...baseStyles,
71
+ zIndex: 9999,
72
+ }),
73
+ menu: baseStyles => ({
74
+ ...baseStyles,
75
+ backgroundColor: '#1f2937',
76
+ border: '1px solid #374151',
77
+ boxShadow: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)',
78
+ }),
79
+ menuList: baseStyles => ({
80
+ ...baseStyles,
81
+ padding: '4px',
82
+ }),
83
+ dropdownIndicator: baseStyles => ({
84
+ ...baseStyles,
85
+ color: '#9ca3af',
86
+ '&:hover': {
87
+ color: '#6366f1',
88
+ },
89
+ }),
90
+ clearIndicator: baseStyles => ({
91
+ ...baseStyles,
92
+ color: '#9ca3af',
93
+ '&:hover': {
94
+ color: '#6366f1',
95
+ },
96
+ }),
97
+ }}
98
+ value={
99
+ value
100
+ ? {
101
+ value: value,
102
+ label: filter.options.find(opt => opt.value === value)?.label || value,
103
+ }
104
+ : null
105
+ }
106
+ onChange={option => onChange(option?.value || '')}
107
+ options={filter.options.map(opt => ({
108
+ value: opt.value,
109
+ label: opt.label,
110
+ }))}
111
+ placeholder={`Filter by ${field.title || field.name}`}
112
+ isClearable
113
+ />
114
+ );
115
+ }
116
+ default:
117
+ return (
118
+ <input
119
+ type={field.type === 'number' ? 'number' : 'text'}
120
+ id={field.name}
121
+ value={value || ''}
122
+ onChange={e => onChange(e.target.value)}
123
+ placeholder={`Filter by ${field.title || field.name}`}
124
+ />
125
+ );
126
+ }
127
+ }
128
+
129
+ export function FilterPopup({
130
+ isOpen,
131
+ onClose,
132
+ onApplyFilters,
133
+ listData,
134
+ activeFilters,
135
+ }: FilterPopupProps): React.ReactElement | null {
136
+ const [filters, setFilters] = React.useState<Record<string, any>>(activeFilters ?? {});
137
+ const popupRef = useRef<HTMLDivElement>(null);
138
+ const fields = useMemo(() => listData.cells.filter(cell => !!cell.filter), [listData.cells]);
139
+
140
+ useEffect(() => {
141
+ const handleClickOutside = (event: MouseEvent) => {
142
+ if (popupRef.current && !popupRef.current.contains(event.target as Node)) {
143
+ onClose();
144
+ }
145
+ };
146
+
147
+ if (isOpen) {
148
+ document.addEventListener('mousedown', handleClickOutside);
149
+ }
150
+
151
+ return () => {
152
+ document.removeEventListener('mousedown', handleClickOutside);
153
+ };
154
+ }, [isOpen, onClose]);
155
+
156
+ if (!isOpen) return null;
157
+
158
+ const handleFilterChange = (fieldName: string, value: any) => {
159
+ setFilters(prev => ({
160
+ ...prev,
161
+ [fieldName]: value,
162
+ }));
163
+ };
164
+
165
+ const handleApply = () => {
166
+ onApplyFilters(filters);
167
+ onClose();
168
+ };
169
+
170
+ return (
171
+ <div className="filter-popup-overlay">
172
+ <div ref={popupRef} className="filter-popup">
173
+ <div className="filter-popup-header">
174
+ <h3>Filter</h3>
175
+ <button onClick={onClose} className="close-button">
176
+ ×
177
+ </button>
178
+ </div>
179
+ <div className="filter-popup-content">
180
+ {fields.map((field: CellOptions) => (
181
+ <div key={field.name} className="filter-field">
182
+ <label htmlFor={field.name}>{field.title || field.name}</label>
183
+ <FilterField
184
+ field={field}
185
+ value={filters[field.name || '']}
186
+ onChange={value => handleFilterChange(field.name || '', value)}
187
+ />
188
+ </div>
189
+ ))}
190
+ </div>
191
+ <div className="filter-popup-footer">
192
+ <button onClick={onClose} className="cancel-button">
193
+ Cancel
194
+ </button>
195
+ <button onClick={handleApply} className="apply-button">
196
+ Apply Filters
197
+ </button>
198
+ </div>
199
+ </div>
200
+ </div>
201
+ );
202
+ }
@@ -0,0 +1,178 @@
1
+ import React, { useMemo, useCallback, useEffect, useState } from 'react';
2
+ import { Link, useParams, useNavigate } from 'react-router';
3
+ import { Datagrid } from './Datagrid';
4
+ import { ErrorComponent } from '../ErrorComponent';
5
+ import { LoadingScreen } from '../LoadingScreen';
6
+ import { AnyClass } from '../../../types/AnyClass';
7
+ import { getListFields } from '../../../decorators/list/getListFields';
8
+ import { Pagination } from './Pagination';
9
+ import { ListData } from '../../../decorators/list/ListData';
10
+ import CreateIcon from '../../../assets/icons/svg/create.svg';
11
+ import FilterIcon from '../../../assets/icons/svg/filter.svg';
12
+ import { FilterPopup } from './FilterPopup';
13
+
14
+ export interface GetDataParams {
15
+ page?: number;
16
+ limit?: number;
17
+ filters?: Record<string, any>;
18
+ }
19
+
20
+ export interface PaginatedResponse<T> {
21
+ data: T[];
22
+ total: number;
23
+ page: number;
24
+ limit: number;
25
+ }
26
+
27
+ export type GetDataForList<T> = (params: GetDataParams) => Promise<PaginatedResponse<T>>;
28
+
29
+ const ListHeader = ({
30
+ listData,
31
+ filtered,
32
+ onFilterClick,
33
+ customHeader,
34
+ }: {
35
+ listData: ListData;
36
+ filtered: boolean;
37
+ onFilterClick: () => void;
38
+ customHeader?: React.ReactNode;
39
+ }) => {
40
+ const fields = useMemo(() => listData.cells.filter(cell => !!cell.filter), [listData.cells]);
41
+
42
+ const header = listData.list?.headers;
43
+ return (
44
+ <div className="list-header">
45
+ <div className="header-title">{header?.title || 'List'}</div>
46
+ {customHeader && <div className="header-custom">{customHeader}</div>}
47
+ <div className="header-actions">
48
+ {!!fields.length && (
49
+ <button onClick={onFilterClick} className="filter-button">
50
+ <FilterIcon className={`icon icon-filter ${filtered ? 'active' : ''}`} />
51
+ Filter
52
+ </button>
53
+ )}
54
+ {header?.create && (
55
+ <Link to={header.create.path} className="create-button">
56
+ <CreateIcon className="icon icon-create" />
57
+ {header.create.label}
58
+ </Link>
59
+ )}
60
+ </div>
61
+ </div>
62
+ );
63
+ };
64
+
65
+ export function ListPage<T extends AnyClass & { id: string }>({
66
+ model,
67
+ getData,
68
+ onRemoveItem,
69
+ customHeader,
70
+ }: {
71
+ model: T;
72
+ getData: GetDataForList<T>;
73
+ customHeader?: React.ReactNode;
74
+ onRemoveItem?: (item: T) => Promise<void>;
75
+ }) {
76
+ const [loading, setLoading] = useState(true);
77
+ const [pagination, setPagination] = useState({ total: 0, page: 0, limit: 0 });
78
+ const [data, setData] = useState<any>(null);
79
+ const [error, setError] = useState<unknown>(null);
80
+ const [isFilterOpen, setIsFilterOpen] = useState(false);
81
+ const [activeFilters, setActiveFilters] = useState<Record<string, string>>();
82
+ const listData = useMemo(() => getListFields(model), [model]);
83
+ const params = useParams();
84
+ const navigate = useNavigate();
85
+
86
+ const fetchData = useCallback(
87
+ async (page: number, filters?: Record<string, string>) => {
88
+ setLoading(true);
89
+ try {
90
+ const result = await getData({ page, filters: filters ?? activeFilters ?? {} });
91
+ setData(result.data);
92
+ setPagination({
93
+ total: result.total,
94
+ page: result.page,
95
+ limit: result.limit,
96
+ });
97
+ } catch (e) {
98
+ setError(e);
99
+ console.error(e);
100
+ } finally {
101
+ setLoading(false);
102
+ }
103
+ },
104
+ [getData, activeFilters]
105
+ );
106
+
107
+ useEffect(() => {
108
+ const searchParams = new URLSearchParams(location.search);
109
+ const filtersFromUrl: Record<string, string> = {};
110
+ searchParams.forEach((value, key) => {
111
+ filtersFromUrl[key] = value;
112
+ });
113
+ setActiveFilters(filtersFromUrl);
114
+ }, [location.search]);
115
+
116
+ useEffect(() => {
117
+ if (activeFilters) {
118
+ fetchData(parseInt(params.page as string) || 1, activeFilters);
119
+ }
120
+ }, [fetchData, params.page, activeFilters]);
121
+
122
+ const handleFilterApply = (filters: Record<string, any>) => {
123
+ setActiveFilters(filters);
124
+
125
+ // Convert filters to URLSearchParams
126
+ const searchParams = new URLSearchParams();
127
+ Object.entries(filters).forEach(([key, value]) => {
128
+ if (value !== undefined && value !== null && value !== '') {
129
+ searchParams.append(key, String(value));
130
+ }
131
+ });
132
+ const queryString = searchParams.toString();
133
+ const newUrl = `${location.pathname}${queryString ? `?${queryString}` : ''}`;
134
+ navigate(newUrl);
135
+ fetchData(1, filters); // Reset to first page when filters change
136
+ };
137
+
138
+ if (loading) return <LoadingScreen />;
139
+ if (error) return <ErrorComponent error={error} />;
140
+
141
+ return (
142
+ <div className="list">
143
+ <ListHeader
144
+ listData={listData}
145
+ filtered={!!(activeFilters && !!Object.keys(activeFilters).length)}
146
+ onFilterClick={() => setIsFilterOpen(true)}
147
+ customHeader={customHeader}
148
+ />
149
+ <Datagrid
150
+ listData={listData}
151
+ data={data}
152
+ onRemoveItem={async (item: T) => {
153
+ if (onRemoveItem) {
154
+ alert({
155
+ title: 'Are you sure you want to delete this item?',
156
+ message: 'This action cannot be undone.',
157
+ onConfirm: async () => {
158
+ await onRemoveItem(item);
159
+ setData(data.filter((d: T) => d.id !== item.id));
160
+ await fetchData(pagination.page);
161
+ },
162
+ });
163
+ }
164
+ }}
165
+ />
166
+ <div className="list-footer">
167
+ <Pagination pagination={pagination} onPageChange={fetchData} />
168
+ </div>
169
+ <FilterPopup
170
+ isOpen={isFilterOpen}
171
+ activeFilters={activeFilters}
172
+ onClose={() => setIsFilterOpen(false)}
173
+ onApplyFilters={handleFilterApply}
174
+ listData={listData}
175
+ />
176
+ </div>
177
+ );
178
+ }
@@ -0,0 +1,110 @@
1
+ import React from 'react';
2
+
3
+ interface PaginationProps {
4
+ pagination: {
5
+ total: number;
6
+ page: number;
7
+ limit: number;
8
+ };
9
+ onPageChange: (page: number) => void;
10
+ }
11
+
12
+ export function Pagination({ pagination, onPageChange }: PaginationProps) {
13
+ const { total, page, limit } = pagination;
14
+ const totalPages = Math.floor(total / limit);
15
+
16
+ if (totalPages <= 1) return null;
17
+
18
+ const renderPageNumbers = () => {
19
+ const pages = [];
20
+ const range = 2; // Number of pages to show before and after current page
21
+
22
+ // Always show first 2 pages
23
+ for (let i = 1; i <= Math.min(2, totalPages); i++) {
24
+ pages.push(
25
+ <button
26
+ key={i}
27
+ onClick={() => onPageChange(i)}
28
+ className={`pagination-item ${page === i ? 'active' : ''}`}
29
+ disabled={page === i}
30
+ >
31
+ {i}
32
+ </button>
33
+ );
34
+ }
35
+
36
+ // Add ellipsis if needed
37
+ if (page - range > 3) {
38
+ pages.push(
39
+ <span key="ellipsis1" className="pagination-ellipsis">
40
+ ...
41
+ </span>
42
+ );
43
+ }
44
+
45
+ // Show pages around current page
46
+ for (let i = Math.max(3, page - range); i <= Math.min(totalPages - 2, page + range); i++) {
47
+ if (i > 2 && i < totalPages - 1) {
48
+ pages.push(
49
+ <button
50
+ key={i}
51
+ onClick={() => onPageChange(i)}
52
+ className={`pagination-item ${page === i ? 'active' : ''}`}
53
+ disabled={page === i}
54
+ >
55
+ {i}
56
+ </button>
57
+ );
58
+ }
59
+ }
60
+
61
+ // Add ellipsis if needed
62
+ if (page + range < totalPages - 2) {
63
+ pages.push(
64
+ <span key="ellipsis2" className="pagination-ellipsis">
65
+ ...
66
+ </span>
67
+ );
68
+ }
69
+
70
+ // Always show last 2 pages
71
+ for (let i = Math.max(totalPages - 1, 3); i <= totalPages; i++) {
72
+ if (i > 2) {
73
+ pages.push(
74
+ <button
75
+ key={i}
76
+ onClick={() => onPageChange(i)}
77
+ className={`pagination-item ${page === i ? 'active' : ''}`}
78
+ disabled={page === i}
79
+ >
80
+ {i}
81
+ </button>
82
+ );
83
+ }
84
+ }
85
+
86
+ return pages;
87
+ };
88
+
89
+ return (
90
+ <div className="pagination">
91
+ <button
92
+ onClick={() => onPageChange(page - 1)}
93
+ className={`pagination-item ${page === 1 ? 'disabled' : ''}`}
94
+ disabled={page === 1}
95
+ aria-disabled={page === 1}
96
+ >
97
+ Previous
98
+ </button>
99
+ {renderPageNumbers()}
100
+ <button
101
+ onClick={() => onPageChange(page + 1)}
102
+ className={`pagination-item ${page === totalPages ? 'disabled' : ''}`}
103
+ disabled={page === totalPages}
104
+ aria-disabled={page === totalPages}
105
+ >
106
+ Next
107
+ </button>
108
+ </div>
109
+ );
110
+ }
@@ -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';