@superdangerous/app-framework 4.9.2 → 4.15.0
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 +8 -2
- package/dist/api/logsRouter.d.ts +4 -1
- package/dist/api/logsRouter.d.ts.map +1 -1
- package/dist/api/logsRouter.js +100 -118
- package/dist/api/logsRouter.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/middleware/validation.d.ts +48 -43
- package/dist/middleware/validation.d.ts.map +1 -1
- package/dist/middleware/validation.js +48 -43
- package/dist/middleware/validation.js.map +1 -1
- package/dist/services/emailService.d.ts +146 -0
- package/dist/services/emailService.d.ts.map +1 -0
- package/dist/services/emailService.js +649 -0
- package/dist/services/emailService.js.map +1 -0
- package/dist/services/index.d.ts +2 -0
- package/dist/services/index.d.ts.map +1 -1
- package/dist/services/index.js +2 -0
- package/dist/services/index.js.map +1 -1
- package/dist/services/websocketServer.d.ts +7 -4
- package/dist/services/websocketServer.d.ts.map +1 -1
- package/dist/services/websocketServer.js +22 -16
- package/dist/services/websocketServer.js.map +1 -1
- package/dist/types/index.d.ts +7 -8
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +11 -2
- package/src/api/logsRouter.ts +119 -138
- package/src/index.ts +14 -0
- package/src/middleware/validation.ts +82 -90
- package/src/services/emailService.ts +812 -0
- package/src/services/index.ts +14 -0
- package/src/services/websocketServer.ts +37 -23
- package/src/types/index.ts +7 -8
- package/ui/data-table/components/BatchActionsBar.tsx +53 -0
- package/ui/data-table/components/ColumnVisibility.tsx +111 -0
- package/ui/data-table/components/DataTablePage.tsx +238 -0
- package/ui/data-table/components/Pagination.tsx +203 -0
- package/ui/data-table/components/PaginationControls.tsx +122 -0
- package/ui/data-table/components/TableFilters.tsx +139 -0
- package/ui/data-table/components/index.ts +27 -0
- package/ui/data-table/hooks/index.ts +17 -0
- package/ui/data-table/hooks/useColumnOrder.ts +233 -0
- package/ui/data-table/hooks/useColumnVisibility.ts +128 -0
- package/ui/data-table/hooks/usePagination.ts +160 -0
- package/ui/data-table/hooks/useResizableColumns.ts +280 -0
- package/ui/data-table/index.ts +74 -0
- package/ui/dist/index.d.mts +207 -5
- package/ui/dist/index.d.ts +207 -5
- package/ui/dist/index.js +36 -43
- package/ui/dist/index.js.map +1 -1
- package/ui/dist/index.mjs +36 -43
- package/ui/dist/index.mjs.map +1 -1
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import { useState, useCallback, useEffect } from 'react';
|
|
2
|
+
|
|
3
|
+
export interface ColumnOrderConfig {
|
|
4
|
+
id: string;
|
|
5
|
+
label: string;
|
|
6
|
+
locked?: boolean; // Locked columns can't be moved
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface UseColumnOrderOptions {
|
|
10
|
+
storageKey: string;
|
|
11
|
+
defaultOrder: string[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface UseColumnOrderReturn {
|
|
15
|
+
columnOrder: string[];
|
|
16
|
+
moveColumn: (fromIndex: number, toIndex: number) => void;
|
|
17
|
+
moveColumnById: (columnId: string, direction: 'left' | 'right') => void;
|
|
18
|
+
resetOrder: () => void;
|
|
19
|
+
getOrderedColumns: <T extends { id: string }>(columns: T[]) => T[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Hook for managing column order with localStorage persistence
|
|
24
|
+
*/
|
|
25
|
+
export function useColumnOrder({
|
|
26
|
+
storageKey,
|
|
27
|
+
defaultOrder,
|
|
28
|
+
}: UseColumnOrderOptions): UseColumnOrderReturn {
|
|
29
|
+
const [columnOrder, setColumnOrder] = useState<string[]>(() => {
|
|
30
|
+
try {
|
|
31
|
+
const stored = localStorage.getItem(storageKey);
|
|
32
|
+
if (stored) {
|
|
33
|
+
const parsed = JSON.parse(stored);
|
|
34
|
+
const storedSet = new Set(parsed);
|
|
35
|
+
const defaultSet = new Set(defaultOrder);
|
|
36
|
+
|
|
37
|
+
// Remove old columns that aren't in default anymore
|
|
38
|
+
const validStored = parsed.filter((col: string) => defaultSet.has(col));
|
|
39
|
+
|
|
40
|
+
// Find missing columns and insert them at their default positions
|
|
41
|
+
const missingColumns = defaultOrder.filter(col => !storedSet.has(col));
|
|
42
|
+
|
|
43
|
+
if (missingColumns.length > 0) {
|
|
44
|
+
// Build new order by inserting missing columns at their default positions
|
|
45
|
+
const result = [...validStored];
|
|
46
|
+
for (const missing of missingColumns) {
|
|
47
|
+
const defaultIndex = defaultOrder.indexOf(missing);
|
|
48
|
+
// Find the best insertion point based on surrounding columns in default order
|
|
49
|
+
let insertAt = result.length;
|
|
50
|
+
for (let i = 0; i < result.length; i++) {
|
|
51
|
+
const currentDefaultIndex = defaultOrder.indexOf(result[i]);
|
|
52
|
+
if (currentDefaultIndex > defaultIndex) {
|
|
53
|
+
insertAt = i;
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
result.splice(insertAt, 0, missing);
|
|
58
|
+
}
|
|
59
|
+
return result;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return validStored.length > 0 ? validStored : defaultOrder;
|
|
63
|
+
}
|
|
64
|
+
} catch (e) {
|
|
65
|
+
console.warn('Failed to load column order from localStorage:', e);
|
|
66
|
+
}
|
|
67
|
+
return defaultOrder;
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// Persist to localStorage
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
try {
|
|
73
|
+
localStorage.setItem(storageKey, JSON.stringify(columnOrder));
|
|
74
|
+
} catch (e) {
|
|
75
|
+
console.warn('Failed to save column order to localStorage:', e);
|
|
76
|
+
}
|
|
77
|
+
}, [storageKey, columnOrder]);
|
|
78
|
+
|
|
79
|
+
const moveColumn = useCallback((fromIndex: number, toIndex: number) => {
|
|
80
|
+
if (fromIndex === toIndex) return;
|
|
81
|
+
|
|
82
|
+
setColumnOrder(prev => {
|
|
83
|
+
const newOrder = [...prev];
|
|
84
|
+
const [removed] = newOrder.splice(fromIndex, 1);
|
|
85
|
+
newOrder.splice(toIndex, 0, removed);
|
|
86
|
+
return newOrder;
|
|
87
|
+
});
|
|
88
|
+
}, []);
|
|
89
|
+
|
|
90
|
+
const moveColumnById = useCallback((columnId: string, direction: 'left' | 'right') => {
|
|
91
|
+
setColumnOrder(prev => {
|
|
92
|
+
const currentIndex = prev.indexOf(columnId);
|
|
93
|
+
if (currentIndex === -1) return prev;
|
|
94
|
+
|
|
95
|
+
const newIndex = direction === 'left'
|
|
96
|
+
? Math.max(0, currentIndex - 1)
|
|
97
|
+
: Math.min(prev.length - 1, currentIndex + 1);
|
|
98
|
+
|
|
99
|
+
if (currentIndex === newIndex) return prev;
|
|
100
|
+
|
|
101
|
+
const newOrder = [...prev];
|
|
102
|
+
const [removed] = newOrder.splice(currentIndex, 1);
|
|
103
|
+
newOrder.splice(newIndex, 0, removed);
|
|
104
|
+
return newOrder;
|
|
105
|
+
});
|
|
106
|
+
}, []);
|
|
107
|
+
|
|
108
|
+
const resetOrder = useCallback(() => {
|
|
109
|
+
setColumnOrder(defaultOrder);
|
|
110
|
+
}, [defaultOrder]);
|
|
111
|
+
|
|
112
|
+
const getOrderedColumns = useCallback(<T extends { id: string }>(columns: T[]): T[] => {
|
|
113
|
+
const columnMap = new Map(columns.map(col => [col.id, col]));
|
|
114
|
+
return columnOrder
|
|
115
|
+
.map(id => columnMap.get(id))
|
|
116
|
+
.filter((col): col is T => col !== undefined);
|
|
117
|
+
}, [columnOrder]);
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
columnOrder,
|
|
121
|
+
moveColumn,
|
|
122
|
+
moveColumnById,
|
|
123
|
+
resetOrder,
|
|
124
|
+
getOrderedColumns,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Drag and drop helpers for column reordering
|
|
130
|
+
*/
|
|
131
|
+
export interface DragState {
|
|
132
|
+
isDragging: boolean;
|
|
133
|
+
draggedId: string | null;
|
|
134
|
+
dropIndex: number | null; // Index where the column will be inserted
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function useColumnDragDrop(
|
|
138
|
+
columnOrder: string[],
|
|
139
|
+
moveColumn: (from: number, to: number) => void,
|
|
140
|
+
lockedColumns: string[] = []
|
|
141
|
+
) {
|
|
142
|
+
const [dragState, setDragState] = useState<DragState>({
|
|
143
|
+
isDragging: false,
|
|
144
|
+
draggedId: null,
|
|
145
|
+
dropIndex: null,
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
const handleDragStart = useCallback((columnId: string) => {
|
|
149
|
+
if (lockedColumns.includes(columnId)) return;
|
|
150
|
+
setDragState({
|
|
151
|
+
isDragging: true,
|
|
152
|
+
draggedId: columnId,
|
|
153
|
+
dropIndex: null,
|
|
154
|
+
});
|
|
155
|
+
}, [lockedColumns]);
|
|
156
|
+
|
|
157
|
+
const handleDragOver = useCallback((columnId: string, e: React.DragEvent) => {
|
|
158
|
+
if (lockedColumns.includes(columnId)) return;
|
|
159
|
+
|
|
160
|
+
const targetIndex = columnOrder.indexOf(columnId);
|
|
161
|
+
if (targetIndex === -1) return;
|
|
162
|
+
|
|
163
|
+
// Determine drop index based on mouse position relative to column center
|
|
164
|
+
const rect = e.currentTarget.getBoundingClientRect();
|
|
165
|
+
const midpoint = rect.left + rect.width / 2;
|
|
166
|
+
const dropIndex = e.clientX < midpoint ? targetIndex : targetIndex + 1;
|
|
167
|
+
|
|
168
|
+
setDragState(prev => ({
|
|
169
|
+
...prev,
|
|
170
|
+
dropIndex,
|
|
171
|
+
}));
|
|
172
|
+
}, [lockedColumns, columnOrder]);
|
|
173
|
+
|
|
174
|
+
const handleDrop = useCallback(() => {
|
|
175
|
+
if (!dragState.draggedId || dragState.dropIndex === null) {
|
|
176
|
+
setDragState({ isDragging: false, draggedId: null, dropIndex: null });
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const fromIndex = columnOrder.indexOf(dragState.draggedId);
|
|
181
|
+
let toIndex = dragState.dropIndex;
|
|
182
|
+
|
|
183
|
+
// Adjust if moving from before the drop position
|
|
184
|
+
if (fromIndex < toIndex) {
|
|
185
|
+
toIndex = toIndex - 1;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (fromIndex !== -1 && toIndex !== -1 && fromIndex !== toIndex) {
|
|
189
|
+
moveColumn(fromIndex, toIndex);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
setDragState({ isDragging: false, draggedId: null, dropIndex: null });
|
|
193
|
+
}, [dragState.draggedId, dragState.dropIndex, columnOrder, moveColumn]);
|
|
194
|
+
|
|
195
|
+
const handleDragEnd = useCallback(() => {
|
|
196
|
+
setDragState({ isDragging: false, draggedId: null, dropIndex: null });
|
|
197
|
+
}, []);
|
|
198
|
+
|
|
199
|
+
const getDragHandleProps = useCallback((columnId: string) => ({
|
|
200
|
+
draggable: !lockedColumns.includes(columnId),
|
|
201
|
+
onDragStart: (e: React.DragEvent) => {
|
|
202
|
+
if (lockedColumns.includes(columnId)) {
|
|
203
|
+
e.preventDefault();
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
e.dataTransfer.effectAllowed = 'move';
|
|
207
|
+
handleDragStart(columnId);
|
|
208
|
+
},
|
|
209
|
+
onDragOver: (e: React.DragEvent) => {
|
|
210
|
+
e.preventDefault();
|
|
211
|
+
e.dataTransfer.dropEffect = 'move';
|
|
212
|
+
handleDragOver(columnId, e);
|
|
213
|
+
},
|
|
214
|
+
onDrop: (e: React.DragEvent) => {
|
|
215
|
+
e.preventDefault();
|
|
216
|
+
handleDrop();
|
|
217
|
+
},
|
|
218
|
+
onDragEnd: handleDragEnd,
|
|
219
|
+
}), [lockedColumns, handleDragStart, handleDragOver, handleDrop, handleDragEnd]);
|
|
220
|
+
|
|
221
|
+
// Helper to check if drop indicator should show on the left of a column
|
|
222
|
+
const showDropIndicator = useCallback((columnId: string) => {
|
|
223
|
+
if (!dragState.isDragging || dragState.dropIndex === null) return false;
|
|
224
|
+
const columnIndex = columnOrder.indexOf(columnId);
|
|
225
|
+
return columnIndex === dragState.dropIndex;
|
|
226
|
+
}, [dragState.isDragging, dragState.dropIndex, columnOrder]);
|
|
227
|
+
|
|
228
|
+
return {
|
|
229
|
+
dragState,
|
|
230
|
+
getDragHandleProps,
|
|
231
|
+
showDropIndicator,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
2
|
+
|
|
3
|
+
export interface ColumnConfig {
|
|
4
|
+
id: string;
|
|
5
|
+
label: string;
|
|
6
|
+
defaultVisible?: boolean;
|
|
7
|
+
locked?: boolean; // If true, column cannot be hidden
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface ColumnVisibilityState {
|
|
11
|
+
[key: string]: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface UseColumnVisibilityOptions {
|
|
15
|
+
columns: ColumnConfig[];
|
|
16
|
+
storageKey: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface UseColumnVisibilityReturn {
|
|
20
|
+
visibleColumns: ColumnVisibilityState;
|
|
21
|
+
isColumnVisible: (columnId: string) => boolean;
|
|
22
|
+
toggleColumn: (columnId: string) => void;
|
|
23
|
+
showAllColumns: () => void;
|
|
24
|
+
hideAllColumns: () => void;
|
|
25
|
+
columns: ColumnConfig[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function useColumnVisibility({
|
|
29
|
+
columns,
|
|
30
|
+
storageKey,
|
|
31
|
+
}: UseColumnVisibilityOptions): UseColumnVisibilityReturn {
|
|
32
|
+
const fullStorageKey = `column-visibility-${storageKey}`;
|
|
33
|
+
|
|
34
|
+
// Initialize state from localStorage or defaults (with SSR guard)
|
|
35
|
+
const [visibleColumns, setVisibleColumns] = useState<ColumnVisibilityState>(() => {
|
|
36
|
+
// Default state
|
|
37
|
+
const defaults: ColumnVisibilityState = {};
|
|
38
|
+
columns.forEach(col => {
|
|
39
|
+
defaults[col.id] = col.defaultVisible !== false;
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// SSR guard - localStorage is not available on server
|
|
43
|
+
if (typeof window === 'undefined') {
|
|
44
|
+
return defaults;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
const stored = localStorage.getItem(fullStorageKey);
|
|
49
|
+
if (stored) {
|
|
50
|
+
const parsed = JSON.parse(stored);
|
|
51
|
+
// Merge with defaults to handle new columns
|
|
52
|
+
const merged: ColumnVisibilityState = {};
|
|
53
|
+
columns.forEach(col => {
|
|
54
|
+
if (col.locked) {
|
|
55
|
+
merged[col.id] = true;
|
|
56
|
+
} else if (parsed[col.id] !== undefined) {
|
|
57
|
+
merged[col.id] = parsed[col.id];
|
|
58
|
+
} else {
|
|
59
|
+
merged[col.id] = col.defaultVisible !== false;
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
return merged;
|
|
63
|
+
}
|
|
64
|
+
} catch (e) {
|
|
65
|
+
console.error('Error loading column visibility state:', e);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return defaults;
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// Persist to localStorage (with SSR guard)
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
if (typeof window === 'undefined') return;
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
localStorage.setItem(fullStorageKey, JSON.stringify(visibleColumns));
|
|
77
|
+
} catch (e) {
|
|
78
|
+
console.error('Error saving column visibility state:', e);
|
|
79
|
+
}
|
|
80
|
+
}, [visibleColumns, fullStorageKey]);
|
|
81
|
+
|
|
82
|
+
const isColumnVisible = useCallback(
|
|
83
|
+
(columnId: string): boolean => {
|
|
84
|
+
const col = columns.find(c => c.id === columnId);
|
|
85
|
+
if (col?.locked) return true;
|
|
86
|
+
return visibleColumns[columnId] !== false;
|
|
87
|
+
},
|
|
88
|
+
[visibleColumns, columns]
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
const toggleColumn = useCallback(
|
|
92
|
+
(columnId: string) => {
|
|
93
|
+
const col = columns.find(c => c.id === columnId);
|
|
94
|
+
if (col?.locked) return; // Cannot toggle locked columns
|
|
95
|
+
|
|
96
|
+
setVisibleColumns(prev => ({
|
|
97
|
+
...prev,
|
|
98
|
+
[columnId]: !prev[columnId],
|
|
99
|
+
}));
|
|
100
|
+
},
|
|
101
|
+
[columns]
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
const showAllColumns = useCallback(() => {
|
|
105
|
+
const allVisible: ColumnVisibilityState = {};
|
|
106
|
+
columns.forEach(col => {
|
|
107
|
+
allVisible[col.id] = true;
|
|
108
|
+
});
|
|
109
|
+
setVisibleColumns(allVisible);
|
|
110
|
+
}, [columns]);
|
|
111
|
+
|
|
112
|
+
const hideAllColumns = useCallback(() => {
|
|
113
|
+
const onlyLocked: ColumnVisibilityState = {};
|
|
114
|
+
columns.forEach(col => {
|
|
115
|
+
onlyLocked[col.id] = col.locked === true;
|
|
116
|
+
});
|
|
117
|
+
setVisibleColumns(onlyLocked);
|
|
118
|
+
}, [columns]);
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
visibleColumns,
|
|
122
|
+
isColumnVisible,
|
|
123
|
+
toggleColumn,
|
|
124
|
+
showAllColumns,
|
|
125
|
+
hideAllColumns,
|
|
126
|
+
columns,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { useState, useMemo, useCallback, useEffect } from 'react';
|
|
2
|
+
|
|
3
|
+
interface UsePaginationOptions<T> {
|
|
4
|
+
data: T[];
|
|
5
|
+
pageSize?: number;
|
|
6
|
+
storageKey?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface PaginationState {
|
|
10
|
+
page: number;
|
|
11
|
+
pageSize: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface UsePaginationResult<T> {
|
|
15
|
+
// Paginated data
|
|
16
|
+
paginatedData: T[];
|
|
17
|
+
|
|
18
|
+
// Current state
|
|
19
|
+
page: number;
|
|
20
|
+
pageSize: number;
|
|
21
|
+
totalPages: number;
|
|
22
|
+
totalItems: number;
|
|
23
|
+
|
|
24
|
+
// Navigation
|
|
25
|
+
setPage: (page: number) => void;
|
|
26
|
+
setPageSize: (size: number) => void;
|
|
27
|
+
nextPage: () => void;
|
|
28
|
+
prevPage: () => void;
|
|
29
|
+
firstPage: () => void;
|
|
30
|
+
lastPage: () => void;
|
|
31
|
+
|
|
32
|
+
// Info
|
|
33
|
+
startIndex: number;
|
|
34
|
+
endIndex: number;
|
|
35
|
+
canGoNext: boolean;
|
|
36
|
+
canGoPrev: boolean;
|
|
37
|
+
|
|
38
|
+
// Page size options
|
|
39
|
+
pageSizeOptions: number[];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const DEFAULT_PAGE_SIZE = 25;
|
|
43
|
+
const PAGE_SIZE_OPTIONS = [10, 25, 50, 100];
|
|
44
|
+
|
|
45
|
+
export function usePagination<T>({
|
|
46
|
+
data,
|
|
47
|
+
pageSize: initialPageSize = DEFAULT_PAGE_SIZE,
|
|
48
|
+
storageKey,
|
|
49
|
+
}: UsePaginationOptions<T>): UsePaginationResult<T> {
|
|
50
|
+
// Initialize state from localStorage if available
|
|
51
|
+
const [state, setState] = useState<PaginationState>(() => {
|
|
52
|
+
if (storageKey) {
|
|
53
|
+
try {
|
|
54
|
+
const stored = localStorage.getItem(`pagination-${storageKey}`);
|
|
55
|
+
if (stored) {
|
|
56
|
+
const parsed = JSON.parse(stored);
|
|
57
|
+
return {
|
|
58
|
+
page: 1, // Always start on first page
|
|
59
|
+
pageSize: parsed.pageSize || initialPageSize,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
} catch {
|
|
63
|
+
// Ignore localStorage errors
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
page: 1,
|
|
68
|
+
pageSize: initialPageSize,
|
|
69
|
+
};
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const { page, pageSize } = state;
|
|
73
|
+
|
|
74
|
+
// Calculate total pages
|
|
75
|
+
const totalPages = useMemo(
|
|
76
|
+
() => Math.max(1, Math.ceil(data.length / pageSize)),
|
|
77
|
+
[data.length, pageSize]
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
// Reset to first page if current page is out of bounds
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
if (page > totalPages) {
|
|
83
|
+
setState((prev) => ({ ...prev, page: Math.max(1, totalPages) }));
|
|
84
|
+
}
|
|
85
|
+
}, [totalPages, page]);
|
|
86
|
+
|
|
87
|
+
// Persist pageSize to localStorage
|
|
88
|
+
useEffect(() => {
|
|
89
|
+
if (storageKey) {
|
|
90
|
+
try {
|
|
91
|
+
localStorage.setItem(
|
|
92
|
+
`pagination-${storageKey}`,
|
|
93
|
+
JSON.stringify({ pageSize })
|
|
94
|
+
);
|
|
95
|
+
} catch {
|
|
96
|
+
// Ignore localStorage errors
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}, [pageSize, storageKey]);
|
|
100
|
+
|
|
101
|
+
// Calculate start and end indices
|
|
102
|
+
const startIndex = (page - 1) * pageSize;
|
|
103
|
+
const endIndex = Math.min(startIndex + pageSize, data.length);
|
|
104
|
+
|
|
105
|
+
// Get paginated data
|
|
106
|
+
const paginatedData = useMemo(
|
|
107
|
+
() => data.slice(startIndex, endIndex),
|
|
108
|
+
[data, startIndex, endIndex]
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
// Navigation functions
|
|
112
|
+
const setPage = useCallback((newPage: number) => {
|
|
113
|
+
setState((prev) => ({
|
|
114
|
+
...prev,
|
|
115
|
+
page: Math.max(1, Math.min(newPage, totalPages)),
|
|
116
|
+
}));
|
|
117
|
+
}, [totalPages]);
|
|
118
|
+
|
|
119
|
+
const setPageSize = useCallback((newSize: number) => {
|
|
120
|
+
setState({
|
|
121
|
+
page: 1, // Reset to first page when changing page size
|
|
122
|
+
pageSize: newSize,
|
|
123
|
+
});
|
|
124
|
+
}, []);
|
|
125
|
+
|
|
126
|
+
const nextPage = useCallback(() => {
|
|
127
|
+
setPage(page + 1);
|
|
128
|
+
}, [page, setPage]);
|
|
129
|
+
|
|
130
|
+
const prevPage = useCallback(() => {
|
|
131
|
+
setPage(page - 1);
|
|
132
|
+
}, [page, setPage]);
|
|
133
|
+
|
|
134
|
+
const firstPage = useCallback(() => {
|
|
135
|
+
setPage(1);
|
|
136
|
+
}, [setPage]);
|
|
137
|
+
|
|
138
|
+
const lastPage = useCallback(() => {
|
|
139
|
+
setPage(totalPages);
|
|
140
|
+
}, [totalPages, setPage]);
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
paginatedData,
|
|
144
|
+
page,
|
|
145
|
+
pageSize,
|
|
146
|
+
totalPages,
|
|
147
|
+
totalItems: data.length,
|
|
148
|
+
setPage,
|
|
149
|
+
setPageSize,
|
|
150
|
+
nextPage,
|
|
151
|
+
prevPage,
|
|
152
|
+
firstPage,
|
|
153
|
+
lastPage,
|
|
154
|
+
startIndex: startIndex + 1, // 1-indexed for display
|
|
155
|
+
endIndex,
|
|
156
|
+
canGoNext: page < totalPages,
|
|
157
|
+
canGoPrev: page > 1,
|
|
158
|
+
pageSizeOptions: PAGE_SIZE_OPTIONS,
|
|
159
|
+
};
|
|
160
|
+
}
|