@tsiky/components-r19 1.0.0 → 1.1.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/index.ts +35 -33
- package/package.json +1 -1
- package/src/components/Charts/area-chart-admission/AreaChartAdmission.tsx +123 -89
- package/src/components/Charts/bar-chart/BarChart.tsx +167 -132
- package/src/components/Charts/mixed-chart/MixedChart.tsx +65 -9
- package/src/components/Charts/sankey-chart/SankeyChart.tsx +183 -155
- package/src/components/Confirmationpopup/ConfirmationPopup.module.css +88 -0
- package/src/components/Confirmationpopup/ConfirmationPopup.stories.tsx +94 -0
- package/src/components/Confirmationpopup/ConfirmationPopup.tsx +47 -0
- package/src/components/Confirmationpopup/index.ts +6 -0
- package/src/components/Confirmationpopup/useConfirmationPopup.ts +48 -0
- package/src/components/DayStatCard/DayStatCard.tsx +96 -69
- package/src/components/DynamicTable/AdvancedFilters.tsx +196 -196
- package/src/components/DynamicTable/ColumnSorter.tsx +185 -185
- package/src/components/DynamicTable/Pagination.tsx +115 -115
- package/src/components/DynamicTable/TableauDynamique.module.css +1287 -1287
- package/src/components/DynamicTable/filters/SelectFilter.tsx +69 -69
- package/src/components/EntryControl/EntryControl.tsx +117 -117
- package/src/components/Grid/Grid.tsx +5 -0
- package/src/components/Header/Header.tsx +4 -2
- package/src/components/Header/header.css +61 -31
- package/src/components/MetricsPanel/MetricsPanel.module.css +688 -636
- package/src/components/MetricsPanel/MetricsPanel.tsx +220 -282
- package/src/components/MetricsPanel/renderers/CompactRenderer.tsx +148 -125
- package/src/components/NavBar/NavBar.tsx +1 -1
- package/src/components/SelectFilter/SelectFilter.module.css +249 -0
- package/src/components/SelectFilter/SelectFilter.stories.tsx +321 -0
- package/src/components/SelectFilter/SelectFilter.tsx +219 -0
- package/src/components/SelectFilter/index.ts +2 -0
- package/src/components/SelectFilter/types.ts +19 -0
- package/src/components/TranslationKey/TranslationKey.tsx +265 -245
- package/src/components/TrendList/TrendList.tsx +72 -45
|
@@ -1,185 +1,185 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import React, { useEffect, useRef, useState } from 'react';
|
|
4
|
-
import { ChevronsUpDown, ChevronUp, ChevronDown, X } from 'lucide-react';
|
|
5
|
-
import type { SortConfig, TableColumn } from './tools/tableTypes';
|
|
6
|
-
import styles from './TableauDynamique.module.css';
|
|
7
|
-
|
|
8
|
-
interface ColumnSorterProps<T = unknown> {
|
|
9
|
-
columns: TableColumn<T>[];
|
|
10
|
-
sortConfig: SortConfig;
|
|
11
|
-
onSort: (sortConfig: SortConfig) => void;
|
|
12
|
-
onToggleFilters?: () => void;
|
|
13
|
-
filtersActive?: boolean;
|
|
14
|
-
showFilters?: boolean;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
const isAlphaUpperCase = (s: string) => {
|
|
18
|
-
const letters = s.replace(/[^A-Za-zÀ-ÖØ-öø-ÿ]/g, '');
|
|
19
|
-
return letters.length > 0 && letters === letters.toUpperCase();
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
const ColumnSorter = <T,>({ columns, sortConfig, onSort }: ColumnSorterProps<T>) => {
|
|
23
|
-
const [isOpen, setIsOpen] = useState(false);
|
|
24
|
-
const sortableColumns = columns.filter((col) => col.sortable);
|
|
25
|
-
const wrapperRef = useRef<HTMLDivElement | null>(null);
|
|
26
|
-
|
|
27
|
-
const handleSort = (key: string | null, direction: 'asc' | 'desc') => {
|
|
28
|
-
onSort({ key, direction });
|
|
29
|
-
setIsOpen(false);
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
const toggleOpen = () => setIsOpen((v) => !v);
|
|
33
|
-
|
|
34
|
-
const handleCancel = () => {
|
|
35
|
-
setIsOpen(false);
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
// fermer au clic extérieur + Échap (déjà en place)
|
|
39
|
-
useEffect(() => {
|
|
40
|
-
if (!isOpen) return;
|
|
41
|
-
|
|
42
|
-
const onPointerDown = (e: PointerEvent) => {
|
|
43
|
-
if (!wrapperRef.current) return;
|
|
44
|
-
const target = e.target as Node | null;
|
|
45
|
-
if (target && !wrapperRef.current.contains(target)) {
|
|
46
|
-
setIsOpen(false);
|
|
47
|
-
}
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
const onKeyDown = (e: KeyboardEvent) => {
|
|
51
|
-
if (e.key === 'Escape') {
|
|
52
|
-
setIsOpen(false);
|
|
53
|
-
}
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
document.addEventListener('pointerdown', onPointerDown);
|
|
57
|
-
document.addEventListener('keydown', onKeyDown);
|
|
58
|
-
|
|
59
|
-
return () => {
|
|
60
|
-
document.removeEventListener('pointerdown', onPointerDown);
|
|
61
|
-
document.removeEventListener('keydown', onKeyDown);
|
|
62
|
-
};
|
|
63
|
-
}, [isOpen]);
|
|
64
|
-
|
|
65
|
-
// --> NO SCROLL on the table container while dropdown is open
|
|
66
|
-
useEffect(() => {
|
|
67
|
-
const tableWrapper = document.querySelector('[data-table-wrapper]') as HTMLElement | null;
|
|
68
|
-
if (!tableWrapper) return;
|
|
69
|
-
|
|
70
|
-
// sauvegarde la valeur inline actuelle pour restaurer plus tard
|
|
71
|
-
const previousInline = tableWrapper.style.overflow ?? '';
|
|
72
|
-
|
|
73
|
-
if (isOpen) {
|
|
74
|
-
// masquer scrollbars (on cache à la fois X et Y)
|
|
75
|
-
tableWrapper.style.overflow = 'hidden';
|
|
76
|
-
} else {
|
|
77
|
-
// restaurer à la fermeture
|
|
78
|
-
tableWrapper.style.overflow = previousInline;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// cleanup si le composant se démonte pendant que c'est ouvert
|
|
82
|
-
return () => {
|
|
83
|
-
tableWrapper.style.overflow = previousInline;
|
|
84
|
-
};
|
|
85
|
-
}, [isOpen]);
|
|
86
|
-
|
|
87
|
-
return (
|
|
88
|
-
<div className={styles.columnSorterContainer}>
|
|
89
|
-
<div className={styles.sorterControls}>
|
|
90
|
-
{sortableColumns.length > 0 && (
|
|
91
|
-
<div className={styles.sortDropdownWrapper} ref={wrapperRef}>
|
|
92
|
-
<button
|
|
93
|
-
className={styles.toggleSortButton}
|
|
94
|
-
onClick={toggleOpen}
|
|
95
|
-
aria-expanded={isOpen}
|
|
96
|
-
aria-haspopup='true'
|
|
97
|
-
type='button'
|
|
98
|
-
>
|
|
99
|
-
<ChevronsUpDown size={14} />
|
|
100
|
-
<span
|
|
101
|
-
className={`${styles.buttonTextCompact} ${
|
|
102
|
-
isAlphaUpperCase('Sort') ? styles.uppercaseSmall : ''
|
|
103
|
-
}`}
|
|
104
|
-
>
|
|
105
|
-
Sort
|
|
106
|
-
</span>
|
|
107
|
-
{sortConfig.key &&
|
|
108
|
-
(sortConfig.direction === 'asc' ? (
|
|
109
|
-
<ChevronUp size={14} />
|
|
110
|
-
) : (
|
|
111
|
-
<ChevronDown size={14} />
|
|
112
|
-
))}
|
|
113
|
-
</button>
|
|
114
|
-
|
|
115
|
-
{isOpen && (
|
|
116
|
-
<div className={`${styles.sortDropdown} ${styles.sortDropdownInline}`} role='menu'>
|
|
117
|
-
<div className={styles.sortDropdownHeader}>
|
|
118
|
-
<span>Trier par:</span>
|
|
119
|
-
<button className={styles.closeButton} onClick={handleCancel} aria-label='Fermer'>
|
|
120
|
-
<X size={16} />
|
|
121
|
-
</button>
|
|
122
|
-
</div>
|
|
123
|
-
|
|
124
|
-
<div className={styles.sortDropdownContent}>
|
|
125
|
-
{sortableColumns.map((column) => {
|
|
126
|
-
const label = String(column.label ?? column.id);
|
|
127
|
-
const smallUpper = isAlphaUpperCase(label);
|
|
128
|
-
return (
|
|
129
|
-
<div
|
|
130
|
-
key={column.id}
|
|
131
|
-
className={styles.sortDropdownItem}
|
|
132
|
-
role='menuitem'
|
|
133
|
-
tabIndex={0}
|
|
134
|
-
onKeyDown={(e) => {
|
|
135
|
-
if (e.key === 'Enter') handleSort(column.id, 'asc');
|
|
136
|
-
}}
|
|
137
|
-
>
|
|
138
|
-
<span
|
|
139
|
-
className={`${styles.columnSorterLabel} ${smallUpper ? styles.uppercaseSmall : ''}`}
|
|
140
|
-
title={label}
|
|
141
|
-
aria-label={label}
|
|
142
|
-
>
|
|
143
|
-
{label}
|
|
144
|
-
</span>
|
|
145
|
-
|
|
146
|
-
<div className={styles.sortDirectionButtons}>
|
|
147
|
-
<button
|
|
148
|
-
className={`${styles.sortDirectionButton} ${
|
|
149
|
-
sortConfig.key === column.id && sortConfig.direction === 'asc'
|
|
150
|
-
? styles.activeSort
|
|
151
|
-
: ''
|
|
152
|
-
}`}
|
|
153
|
-
onClick={() => handleSort(column.id, 'asc')}
|
|
154
|
-
title='Trier par ordre croissant'
|
|
155
|
-
type='button'
|
|
156
|
-
>
|
|
157
|
-
<ChevronUp size={12} />
|
|
158
|
-
</button>
|
|
159
|
-
<button
|
|
160
|
-
className={`${styles.sortDirectionButton} ${
|
|
161
|
-
sortConfig.key === column.id && sortConfig.direction === 'desc'
|
|
162
|
-
? styles.activeSort
|
|
163
|
-
: ''
|
|
164
|
-
}`}
|
|
165
|
-
onClick={() => handleSort(column.id, 'desc')}
|
|
166
|
-
title='Trier par ordre décroissant'
|
|
167
|
-
type='button'
|
|
168
|
-
>
|
|
169
|
-
<ChevronDown size={12} />
|
|
170
|
-
</button>
|
|
171
|
-
</div>
|
|
172
|
-
</div>
|
|
173
|
-
);
|
|
174
|
-
})}
|
|
175
|
-
</div>
|
|
176
|
-
</div>
|
|
177
|
-
)}
|
|
178
|
-
</div>
|
|
179
|
-
)}
|
|
180
|
-
</div>
|
|
181
|
-
</div>
|
|
182
|
-
);
|
|
183
|
-
};
|
|
184
|
-
|
|
185
|
-
export default ColumnSorter;
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { useEffect, useRef, useState } from 'react';
|
|
4
|
+
import { ChevronsUpDown, ChevronUp, ChevronDown, X } from 'lucide-react';
|
|
5
|
+
import type { SortConfig, TableColumn } from './tools/tableTypes';
|
|
6
|
+
import styles from './TableauDynamique.module.css';
|
|
7
|
+
|
|
8
|
+
interface ColumnSorterProps<T = unknown> {
|
|
9
|
+
columns: TableColumn<T>[];
|
|
10
|
+
sortConfig: SortConfig;
|
|
11
|
+
onSort: (sortConfig: SortConfig) => void;
|
|
12
|
+
onToggleFilters?: () => void;
|
|
13
|
+
filtersActive?: boolean;
|
|
14
|
+
showFilters?: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const isAlphaUpperCase = (s: string) => {
|
|
18
|
+
const letters = s.replace(/[^A-Za-zÀ-ÖØ-öø-ÿ]/g, '');
|
|
19
|
+
return letters.length > 0 && letters === letters.toUpperCase();
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const ColumnSorter = <T,>({ columns, sortConfig, onSort }: ColumnSorterProps<T>) => {
|
|
23
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
24
|
+
const sortableColumns = columns.filter((col) => col.sortable);
|
|
25
|
+
const wrapperRef = useRef<HTMLDivElement | null>(null);
|
|
26
|
+
|
|
27
|
+
const handleSort = (key: string | null, direction: 'asc' | 'desc') => {
|
|
28
|
+
onSort({ key, direction });
|
|
29
|
+
setIsOpen(false);
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const toggleOpen = () => setIsOpen((v) => !v);
|
|
33
|
+
|
|
34
|
+
const handleCancel = () => {
|
|
35
|
+
setIsOpen(false);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// fermer au clic extérieur + Échap (déjà en place)
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
if (!isOpen) return;
|
|
41
|
+
|
|
42
|
+
const onPointerDown = (e: PointerEvent) => {
|
|
43
|
+
if (!wrapperRef.current) return;
|
|
44
|
+
const target = e.target as Node | null;
|
|
45
|
+
if (target && !wrapperRef.current.contains(target)) {
|
|
46
|
+
setIsOpen(false);
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const onKeyDown = (e: KeyboardEvent) => {
|
|
51
|
+
if (e.key === 'Escape') {
|
|
52
|
+
setIsOpen(false);
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
document.addEventListener('pointerdown', onPointerDown);
|
|
57
|
+
document.addEventListener('keydown', onKeyDown);
|
|
58
|
+
|
|
59
|
+
return () => {
|
|
60
|
+
document.removeEventListener('pointerdown', onPointerDown);
|
|
61
|
+
document.removeEventListener('keydown', onKeyDown);
|
|
62
|
+
};
|
|
63
|
+
}, [isOpen]);
|
|
64
|
+
|
|
65
|
+
// --> NO SCROLL on the table container while dropdown is open
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
const tableWrapper = document.querySelector('[data-table-wrapper]') as HTMLElement | null;
|
|
68
|
+
if (!tableWrapper) return;
|
|
69
|
+
|
|
70
|
+
// sauvegarde la valeur inline actuelle pour restaurer plus tard
|
|
71
|
+
const previousInline = tableWrapper.style.overflow ?? '';
|
|
72
|
+
|
|
73
|
+
if (isOpen) {
|
|
74
|
+
// masquer scrollbars (on cache à la fois X et Y)
|
|
75
|
+
tableWrapper.style.overflow = 'hidden';
|
|
76
|
+
} else {
|
|
77
|
+
// restaurer à la fermeture
|
|
78
|
+
tableWrapper.style.overflow = previousInline;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// cleanup si le composant se démonte pendant que c'est ouvert
|
|
82
|
+
return () => {
|
|
83
|
+
tableWrapper.style.overflow = previousInline;
|
|
84
|
+
};
|
|
85
|
+
}, [isOpen]);
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
<div className={styles.columnSorterContainer}>
|
|
89
|
+
<div className={styles.sorterControls}>
|
|
90
|
+
{sortableColumns.length > 0 && (
|
|
91
|
+
<div className={styles.sortDropdownWrapper} ref={wrapperRef}>
|
|
92
|
+
<button
|
|
93
|
+
className={styles.toggleSortButton}
|
|
94
|
+
onClick={toggleOpen}
|
|
95
|
+
aria-expanded={isOpen}
|
|
96
|
+
aria-haspopup='true'
|
|
97
|
+
type='button'
|
|
98
|
+
>
|
|
99
|
+
<ChevronsUpDown size={14} />
|
|
100
|
+
<span
|
|
101
|
+
className={`${styles.buttonTextCompact} ${
|
|
102
|
+
isAlphaUpperCase('Sort') ? styles.uppercaseSmall : ''
|
|
103
|
+
}`}
|
|
104
|
+
>
|
|
105
|
+
Sort
|
|
106
|
+
</span>
|
|
107
|
+
{sortConfig.key &&
|
|
108
|
+
(sortConfig.direction === 'asc' ? (
|
|
109
|
+
<ChevronUp size={14} />
|
|
110
|
+
) : (
|
|
111
|
+
<ChevronDown size={14} />
|
|
112
|
+
))}
|
|
113
|
+
</button>
|
|
114
|
+
|
|
115
|
+
{isOpen && (
|
|
116
|
+
<div className={`${styles.sortDropdown} ${styles.sortDropdownInline}`} role='menu'>
|
|
117
|
+
<div className={styles.sortDropdownHeader}>
|
|
118
|
+
<span>Trier par:</span>
|
|
119
|
+
<button className={styles.closeButton} onClick={handleCancel} aria-label='Fermer'>
|
|
120
|
+
<X size={16} />
|
|
121
|
+
</button>
|
|
122
|
+
</div>
|
|
123
|
+
|
|
124
|
+
<div className={styles.sortDropdownContent}>
|
|
125
|
+
{sortableColumns.map((column) => {
|
|
126
|
+
const label = String(column.label ?? column.id);
|
|
127
|
+
const smallUpper = isAlphaUpperCase(label);
|
|
128
|
+
return (
|
|
129
|
+
<div
|
|
130
|
+
key={column.id}
|
|
131
|
+
className={styles.sortDropdownItem}
|
|
132
|
+
role='menuitem'
|
|
133
|
+
tabIndex={0}
|
|
134
|
+
onKeyDown={(e) => {
|
|
135
|
+
if (e.key === 'Enter') handleSort(column.id, 'asc');
|
|
136
|
+
}}
|
|
137
|
+
>
|
|
138
|
+
<span
|
|
139
|
+
className={`${styles.columnSorterLabel} ${smallUpper ? styles.uppercaseSmall : ''}`}
|
|
140
|
+
title={label}
|
|
141
|
+
aria-label={label}
|
|
142
|
+
>
|
|
143
|
+
{label}
|
|
144
|
+
</span>
|
|
145
|
+
|
|
146
|
+
<div className={styles.sortDirectionButtons}>
|
|
147
|
+
<button
|
|
148
|
+
className={`${styles.sortDirectionButton} ${
|
|
149
|
+
sortConfig.key === column.id && sortConfig.direction === 'asc'
|
|
150
|
+
? styles.activeSort
|
|
151
|
+
: ''
|
|
152
|
+
}`}
|
|
153
|
+
onClick={() => handleSort(column.id, 'asc')}
|
|
154
|
+
title='Trier par ordre croissant'
|
|
155
|
+
type='button'
|
|
156
|
+
>
|
|
157
|
+
<ChevronUp size={12} />
|
|
158
|
+
</button>
|
|
159
|
+
<button
|
|
160
|
+
className={`${styles.sortDirectionButton} ${
|
|
161
|
+
sortConfig.key === column.id && sortConfig.direction === 'desc'
|
|
162
|
+
? styles.activeSort
|
|
163
|
+
: ''
|
|
164
|
+
}`}
|
|
165
|
+
onClick={() => handleSort(column.id, 'desc')}
|
|
166
|
+
title='Trier par ordre décroissant'
|
|
167
|
+
type='button'
|
|
168
|
+
>
|
|
169
|
+
<ChevronDown size={12} />
|
|
170
|
+
</button>
|
|
171
|
+
</div>
|
|
172
|
+
</div>
|
|
173
|
+
);
|
|
174
|
+
})}
|
|
175
|
+
</div>
|
|
176
|
+
</div>
|
|
177
|
+
)}
|
|
178
|
+
</div>
|
|
179
|
+
)}
|
|
180
|
+
</div>
|
|
181
|
+
</div>
|
|
182
|
+
);
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
export default ColumnSorter;
|
|
@@ -1,115 +1,115 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import React from 'react';
|
|
4
|
-
import styles from './TableauDynamique.module.css';
|
|
5
|
-
|
|
6
|
-
interface PaginationProps {
|
|
7
|
-
currentPage: number;
|
|
8
|
-
totalPages: number;
|
|
9
|
-
pageSize: number;
|
|
10
|
-
totalItems: number;
|
|
11
|
-
onPageChange: (page: number) => void;
|
|
12
|
-
onPageSizeChange?: (size: number) => void;
|
|
13
|
-
onNewPage?: (page: number) => void;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
const Pagination: React.FC<PaginationProps> = ({
|
|
17
|
-
currentPage,
|
|
18
|
-
totalPages,
|
|
19
|
-
pageSize,
|
|
20
|
-
totalItems,
|
|
21
|
-
onPageChange,
|
|
22
|
-
onPageSizeChange,
|
|
23
|
-
onNewPage,
|
|
24
|
-
}) => {
|
|
25
|
-
const safeTotalPages = Math.max(1, Math.floor(totalPages));
|
|
26
|
-
const safeCurrentPage = Math.min(Math.max(1, Math.floor(currentPage)), safeTotalPages);
|
|
27
|
-
|
|
28
|
-
const handlePrev = () => safeCurrentPage > 1 && onPageChange(safeCurrentPage - 1);
|
|
29
|
-
const handleNext = () => {
|
|
30
|
-
if (onNewPage && safeCurrentPage === safeTotalPages) {
|
|
31
|
-
onNewPage(safeTotalPages + 1);
|
|
32
|
-
return;
|
|
33
|
-
}
|
|
34
|
-
safeCurrentPage < safeTotalPages && onPageChange(safeCurrentPage + 1);
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
const startIndex = totalItems > 0 ? (safeCurrentPage - 1) * pageSize + 1 : 0;
|
|
38
|
-
const endIndex = totalItems > 0 ? Math.min(safeCurrentPage * pageSize, totalItems) : 0;
|
|
39
|
-
|
|
40
|
-
const getPageNumbers = () => {
|
|
41
|
-
const pages: number[] = [];
|
|
42
|
-
const maxVisible = 5;
|
|
43
|
-
let start = Math.max(1, safeCurrentPage - Math.floor(maxVisible / 2));
|
|
44
|
-
let end = start + maxVisible - 1;
|
|
45
|
-
|
|
46
|
-
if (end > safeTotalPages) {
|
|
47
|
-
end = safeTotalPages;
|
|
48
|
-
start = Math.max(1, end - maxVisible + 1);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
for (let i = start; i <= end; i++) pages.push(i);
|
|
52
|
-
return pages;
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
return (
|
|
56
|
-
<div className={styles.paginationContainer} role='navigation' aria-label='Pagination'>
|
|
57
|
-
<div className={styles.leftGroup}>
|
|
58
|
-
{onPageSizeChange && (
|
|
59
|
-
<div className={styles.pageSizeControls}>
|
|
60
|
-
<label className={styles.pageSizeLabel}>Show</label>
|
|
61
|
-
<select
|
|
62
|
-
value={pageSize}
|
|
63
|
-
onChange={(e) => onPageSizeChange(Number(e.target.value))}
|
|
64
|
-
className={styles.pageSizeSelect}
|
|
65
|
-
aria-label='Taille de page'
|
|
66
|
-
>
|
|
67
|
-
{[5, 10, 20, 50].map((size) => (
|
|
68
|
-
<option key={size} value={size}>
|
|
69
|
-
{size}
|
|
70
|
-
</option>
|
|
71
|
-
))}
|
|
72
|
-
</select>
|
|
73
|
-
<span className={styles.pageSizeSuffix}>entries</span>
|
|
74
|
-
</div>
|
|
75
|
-
)}
|
|
76
|
-
</div>
|
|
77
|
-
|
|
78
|
-
<div className={styles.paginationInfo} aria-live='polite'>
|
|
79
|
-
{startIndex}-{endIndex} out of {totalItems}
|
|
80
|
-
</div>
|
|
81
|
-
|
|
82
|
-
<div className={styles.paginationControls}>
|
|
83
|
-
<button
|
|
84
|
-
onClick={handlePrev}
|
|
85
|
-
disabled={safeCurrentPage === 1}
|
|
86
|
-
className={`${styles.paginationButton} ${styles.navigationButton} ${safeCurrentPage === 1 ? styles.disabledButton : ''}`}
|
|
87
|
-
aria-label='Page précédente'
|
|
88
|
-
>
|
|
89
|
-
«
|
|
90
|
-
</button>
|
|
91
|
-
|
|
92
|
-
{getPageNumbers().map((page) => (
|
|
93
|
-
<button
|
|
94
|
-
key={page}
|
|
95
|
-
onClick={() => onPageChange(page)}
|
|
96
|
-
aria-current={safeCurrentPage === page ? 'page' : undefined}
|
|
97
|
-
className={`${styles.paginationButton} ${safeCurrentPage === page ? styles.activeButton : ''}`}
|
|
98
|
-
>
|
|
99
|
-
{page}
|
|
100
|
-
</button>
|
|
101
|
-
))}
|
|
102
|
-
|
|
103
|
-
<button
|
|
104
|
-
onClick={handleNext}
|
|
105
|
-
className={`${styles.paginationButton} ${styles.navigationButton}`}
|
|
106
|
-
aria-label='Page suivante'
|
|
107
|
-
>
|
|
108
|
-
»
|
|
109
|
-
</button>
|
|
110
|
-
</div>
|
|
111
|
-
</div>
|
|
112
|
-
);
|
|
113
|
-
};
|
|
114
|
-
|
|
115
|
-
export default Pagination;
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import styles from './TableauDynamique.module.css';
|
|
5
|
+
|
|
6
|
+
interface PaginationProps {
|
|
7
|
+
currentPage: number;
|
|
8
|
+
totalPages: number;
|
|
9
|
+
pageSize: number;
|
|
10
|
+
totalItems: number;
|
|
11
|
+
onPageChange: (page: number) => void;
|
|
12
|
+
onPageSizeChange?: (size: number) => void;
|
|
13
|
+
onNewPage?: (page: number) => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const Pagination: React.FC<PaginationProps> = ({
|
|
17
|
+
currentPage,
|
|
18
|
+
totalPages,
|
|
19
|
+
pageSize,
|
|
20
|
+
totalItems,
|
|
21
|
+
onPageChange,
|
|
22
|
+
onPageSizeChange,
|
|
23
|
+
onNewPage,
|
|
24
|
+
}) => {
|
|
25
|
+
const safeTotalPages = Math.max(1, Math.floor(totalPages));
|
|
26
|
+
const safeCurrentPage = Math.min(Math.max(1, Math.floor(currentPage)), safeTotalPages);
|
|
27
|
+
|
|
28
|
+
const handlePrev = () => safeCurrentPage > 1 && onPageChange(safeCurrentPage - 1);
|
|
29
|
+
const handleNext = () => {
|
|
30
|
+
if (onNewPage && safeCurrentPage === safeTotalPages) {
|
|
31
|
+
onNewPage(safeTotalPages + 1);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
safeCurrentPage < safeTotalPages && onPageChange(safeCurrentPage + 1);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const startIndex = totalItems > 0 ? (safeCurrentPage - 1) * pageSize + 1 : 0;
|
|
38
|
+
const endIndex = totalItems > 0 ? Math.min(safeCurrentPage * pageSize, totalItems) : 0;
|
|
39
|
+
|
|
40
|
+
const getPageNumbers = () => {
|
|
41
|
+
const pages: number[] = [];
|
|
42
|
+
const maxVisible = 5;
|
|
43
|
+
let start = Math.max(1, safeCurrentPage - Math.floor(maxVisible / 2));
|
|
44
|
+
let end = start + maxVisible - 1;
|
|
45
|
+
|
|
46
|
+
if (end > safeTotalPages) {
|
|
47
|
+
end = safeTotalPages;
|
|
48
|
+
start = Math.max(1, end - maxVisible + 1);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
for (let i = start; i <= end; i++) pages.push(i);
|
|
52
|
+
return pages;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<div className={styles.paginationContainer} role='navigation' aria-label='Pagination'>
|
|
57
|
+
<div className={styles.leftGroup}>
|
|
58
|
+
{onPageSizeChange && (
|
|
59
|
+
<div className={styles.pageSizeControls}>
|
|
60
|
+
<label className={styles.pageSizeLabel}>Show</label>
|
|
61
|
+
<select
|
|
62
|
+
value={pageSize}
|
|
63
|
+
onChange={(e) => onPageSizeChange(Number(e.target.value))}
|
|
64
|
+
className={styles.pageSizeSelect}
|
|
65
|
+
aria-label='Taille de page'
|
|
66
|
+
>
|
|
67
|
+
{[5, 10, 20, 50].map((size) => (
|
|
68
|
+
<option key={size} value={size}>
|
|
69
|
+
{size}
|
|
70
|
+
</option>
|
|
71
|
+
))}
|
|
72
|
+
</select>
|
|
73
|
+
<span className={styles.pageSizeSuffix}>entries</span>
|
|
74
|
+
</div>
|
|
75
|
+
)}
|
|
76
|
+
</div>
|
|
77
|
+
|
|
78
|
+
<div className={styles.paginationInfo} aria-live='polite'>
|
|
79
|
+
{startIndex}-{endIndex} out of {totalItems}
|
|
80
|
+
</div>
|
|
81
|
+
|
|
82
|
+
<div className={styles.paginationControls}>
|
|
83
|
+
<button
|
|
84
|
+
onClick={handlePrev}
|
|
85
|
+
disabled={safeCurrentPage === 1}
|
|
86
|
+
className={`${styles.paginationButton} ${styles.navigationButton} ${safeCurrentPage === 1 ? styles.disabledButton : ''}`}
|
|
87
|
+
aria-label='Page précédente'
|
|
88
|
+
>
|
|
89
|
+
«
|
|
90
|
+
</button>
|
|
91
|
+
|
|
92
|
+
{getPageNumbers().map((page) => (
|
|
93
|
+
<button
|
|
94
|
+
key={page}
|
|
95
|
+
onClick={() => onPageChange(page)}
|
|
96
|
+
aria-current={safeCurrentPage === page ? 'page' : undefined}
|
|
97
|
+
className={`${styles.paginationButton} ${safeCurrentPage === page ? styles.activeButton : ''}`}
|
|
98
|
+
>
|
|
99
|
+
{page}
|
|
100
|
+
</button>
|
|
101
|
+
))}
|
|
102
|
+
|
|
103
|
+
<button
|
|
104
|
+
onClick={handleNext}
|
|
105
|
+
className={`${styles.paginationButton} ${styles.navigationButton}`}
|
|
106
|
+
aria-label='Page suivante'
|
|
107
|
+
>
|
|
108
|
+
»
|
|
109
|
+
</button>
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
);
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
export default Pagination;
|