@djangocfg/ui-core 2.1.320 → 2.1.322
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 +56 -473
- package/package.json +4 -4
- package/src/components/index.ts +51 -0
- package/src/components/navigation/breadcrumb/breadcrumb-navigation.tsx +127 -0
- package/src/components/navigation/breadcrumb/breadcrumb.tsx +133 -0
- package/src/components/navigation/breadcrumb/index.ts +15 -0
- package/src/components/navigation/pagination/index.ts +18 -0
- package/src/components/navigation/pagination/pagination-static.tsx +249 -0
- package/src/components/navigation/pagination/pagination.tsx +139 -0
- package/src/components/navigation/pagination/ssr-pagination.tsx +176 -0
- package/src/components/navigation/sidebar/index.ts +26 -0
- package/src/components/navigation/sidebar/sidebar.tsx +895 -0
- package/src/components/select/README.md +2 -0
- package/src/components/select/select.tsx +39 -3
- package/src/lib/configurator-schema.ts +105 -0
- package/src/lib/index.ts +7 -0
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
import { Link } from '../link';
|
|
4
|
+
import {
|
|
5
|
+
Breadcrumb, BreadcrumbEllipsis, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbPage,
|
|
6
|
+
BreadcrumbSeparator
|
|
7
|
+
} from './breadcrumb';
|
|
8
|
+
|
|
9
|
+
export interface BreadcrumbItem {
|
|
10
|
+
label: string;
|
|
11
|
+
href?: string;
|
|
12
|
+
/**
|
|
13
|
+
* Force render as current page (non-interactive, aria-current="page").
|
|
14
|
+
* By default only the last item without href is treated as current page.
|
|
15
|
+
*/
|
|
16
|
+
isCurrentPage?: boolean;
|
|
17
|
+
icon?: React.ReactNode;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface BreadcrumbNavigationProps {
|
|
21
|
+
items: BreadcrumbItem[];
|
|
22
|
+
separator?: React.ReactNode;
|
|
23
|
+
/**
|
|
24
|
+
* Max visible items (excluding the ellipsis slot) before collapsing.
|
|
25
|
+
* Must be >= 2. Defaults to 5.
|
|
26
|
+
*/
|
|
27
|
+
maxItems?: number;
|
|
28
|
+
className?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
type DisplayEntry =
|
|
32
|
+
| { type: 'item'; item: BreadcrumbItem; originalIndex: number }
|
|
33
|
+
| { type: 'ellipsis' };
|
|
34
|
+
|
|
35
|
+
export const BreadcrumbNavigation: React.FC<BreadcrumbNavigationProps> = ({
|
|
36
|
+
items,
|
|
37
|
+
separator,
|
|
38
|
+
maxItems = 5,
|
|
39
|
+
className,
|
|
40
|
+
}) => {
|
|
41
|
+
if (!items || items.length === 0) return null;
|
|
42
|
+
|
|
43
|
+
const clampedMax = Math.max(2, maxItems);
|
|
44
|
+
|
|
45
|
+
const entries: DisplayEntry[] = (() => {
|
|
46
|
+
if (items.length <= clampedMax) {
|
|
47
|
+
return items.map((item, i) => ({ type: 'item' as const, item, originalIndex: i }));
|
|
48
|
+
}
|
|
49
|
+
const tailCount = Math.max(1, clampedMax - 2);
|
|
50
|
+
const tail = items.slice(-tailCount).map((item, i) => ({
|
|
51
|
+
type: 'item' as const,
|
|
52
|
+
item,
|
|
53
|
+
originalIndex: items.length - tailCount + i,
|
|
54
|
+
}));
|
|
55
|
+
return [
|
|
56
|
+
{ type: 'item' as const, item: items[0]!, originalIndex: 0 },
|
|
57
|
+
{ type: 'ellipsis' as const },
|
|
58
|
+
...tail,
|
|
59
|
+
];
|
|
60
|
+
})();
|
|
61
|
+
|
|
62
|
+
const resolveIsCurrentPage = (item: BreadcrumbItem, originalIndex: number): boolean => {
|
|
63
|
+
if (item.isCurrentPage !== undefined) return item.isCurrentPage;
|
|
64
|
+
return originalIndex === items.length - 1;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const renderItem = (item: BreadcrumbItem, originalIndex: number) => {
|
|
68
|
+
const current = resolveIsCurrentPage(item, originalIndex);
|
|
69
|
+
|
|
70
|
+
const content = (
|
|
71
|
+
<>
|
|
72
|
+
{item.icon && <span className="mr-1">{item.icon}</span>}
|
|
73
|
+
{item.label}
|
|
74
|
+
</>
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
if (current) {
|
|
78
|
+
return (
|
|
79
|
+
<BreadcrumbItem key={`item-${originalIndex}`}>
|
|
80
|
+
<BreadcrumbPage>{content}</BreadcrumbPage>
|
|
81
|
+
</BreadcrumbItem>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<BreadcrumbItem key={`item-${originalIndex}`}>
|
|
87
|
+
{item.href ? (
|
|
88
|
+
<BreadcrumbLink asChild>
|
|
89
|
+
<Link href={item.href}>{content}</Link>
|
|
90
|
+
</BreadcrumbLink>
|
|
91
|
+
) : (
|
|
92
|
+
<BreadcrumbLink>{content}</BreadcrumbLink>
|
|
93
|
+
)}
|
|
94
|
+
</BreadcrumbItem>
|
|
95
|
+
);
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<Breadcrumb className={className}>
|
|
100
|
+
<BreadcrumbList>
|
|
101
|
+
{entries.map((entry, displayIndex) => {
|
|
102
|
+
const isFirst = displayIndex === 0;
|
|
103
|
+
|
|
104
|
+
if (entry.type === 'ellipsis') {
|
|
105
|
+
return (
|
|
106
|
+
<React.Fragment key="ellipsis">
|
|
107
|
+
<BreadcrumbSeparator>{separator}</BreadcrumbSeparator>
|
|
108
|
+
<BreadcrumbItem>
|
|
109
|
+
<BreadcrumbEllipsis />
|
|
110
|
+
</BreadcrumbItem>
|
|
111
|
+
</React.Fragment>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return (
|
|
116
|
+
<React.Fragment key={`fragment-${entry.originalIndex}`}>
|
|
117
|
+
{!isFirst && <BreadcrumbSeparator>{separator}</BreadcrumbSeparator>}
|
|
118
|
+
{renderItem(entry.item, entry.originalIndex)}
|
|
119
|
+
</React.Fragment>
|
|
120
|
+
);
|
|
121
|
+
})}
|
|
122
|
+
</BreadcrumbList>
|
|
123
|
+
</Breadcrumb>
|
|
124
|
+
);
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
BreadcrumbNavigation.displayName = 'BreadcrumbNavigation';
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { ChevronRight, MoreHorizontal } from 'lucide-react';
|
|
4
|
+
import * as React from 'react';
|
|
5
|
+
|
|
6
|
+
import { cn } from '../../../lib';
|
|
7
|
+
import { Slot } from '@radix-ui/react-slot';
|
|
8
|
+
import { Link } from '../link';
|
|
9
|
+
|
|
10
|
+
const Breadcrumb = React.forwardRef<
|
|
11
|
+
HTMLElement,
|
|
12
|
+
React.ComponentPropsWithoutRef<"nav"> & {
|
|
13
|
+
separator?: React.ReactNode
|
|
14
|
+
}
|
|
15
|
+
>(({ ...props }, ref) => <nav ref={ref} aria-label="breadcrumb" {...props} />)
|
|
16
|
+
Breadcrumb.displayName = "Breadcrumb"
|
|
17
|
+
|
|
18
|
+
const BreadcrumbList = React.forwardRef<
|
|
19
|
+
HTMLOListElement,
|
|
20
|
+
React.ComponentPropsWithoutRef<"ol">
|
|
21
|
+
>(({ className, ...props }, ref) => (
|
|
22
|
+
<ol
|
|
23
|
+
ref={ref}
|
|
24
|
+
className={cn(
|
|
25
|
+
"flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5",
|
|
26
|
+
className
|
|
27
|
+
)}
|
|
28
|
+
{...props}
|
|
29
|
+
/>
|
|
30
|
+
))
|
|
31
|
+
BreadcrumbList.displayName = "BreadcrumbList"
|
|
32
|
+
|
|
33
|
+
const BreadcrumbItem = React.forwardRef<
|
|
34
|
+
HTMLLIElement,
|
|
35
|
+
React.ComponentPropsWithoutRef<"li"> & { key?: React.Key }
|
|
36
|
+
>(({ className, ...props }, ref) => (
|
|
37
|
+
<li
|
|
38
|
+
ref={ref}
|
|
39
|
+
className={cn("inline-flex items-center gap-1.5", className)}
|
|
40
|
+
{...props}
|
|
41
|
+
/>
|
|
42
|
+
))
|
|
43
|
+
BreadcrumbItem.displayName = "BreadcrumbItem"
|
|
44
|
+
|
|
45
|
+
const BreadcrumbLink = React.forwardRef<
|
|
46
|
+
HTMLAnchorElement,
|
|
47
|
+
React.ComponentPropsWithoutRef<"a"> & {
|
|
48
|
+
asChild?: boolean
|
|
49
|
+
href?: string
|
|
50
|
+
}
|
|
51
|
+
>(({ asChild, className, href, children, ...props }, ref) => {
|
|
52
|
+
const Comp = asChild ? Slot : "a"
|
|
53
|
+
|
|
54
|
+
if (href) {
|
|
55
|
+
return (
|
|
56
|
+
<Link
|
|
57
|
+
href={href}
|
|
58
|
+
className={cn("transition-colors hover:text-foreground", className)}
|
|
59
|
+
ref={ref}
|
|
60
|
+
>
|
|
61
|
+
{children}
|
|
62
|
+
</Link>
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<Comp
|
|
68
|
+
ref={ref}
|
|
69
|
+
className={cn("transition-colors hover:text-foreground", className)}
|
|
70
|
+
{...props}
|
|
71
|
+
>
|
|
72
|
+
{children}
|
|
73
|
+
</Comp>
|
|
74
|
+
)
|
|
75
|
+
})
|
|
76
|
+
BreadcrumbLink.displayName = "BreadcrumbLink"
|
|
77
|
+
|
|
78
|
+
const BreadcrumbPage = React.forwardRef<
|
|
79
|
+
HTMLSpanElement,
|
|
80
|
+
React.ComponentPropsWithoutRef<"span">
|
|
81
|
+
>(({ className, ...props }, ref) => (
|
|
82
|
+
<span
|
|
83
|
+
ref={ref}
|
|
84
|
+
role="link"
|
|
85
|
+
aria-disabled="true"
|
|
86
|
+
aria-current="page"
|
|
87
|
+
className={cn("font-normal text-foreground", className)}
|
|
88
|
+
{...props}
|
|
89
|
+
/>
|
|
90
|
+
))
|
|
91
|
+
BreadcrumbPage.displayName = "BreadcrumbPage"
|
|
92
|
+
|
|
93
|
+
const BreadcrumbSeparator = ({
|
|
94
|
+
children,
|
|
95
|
+
className,
|
|
96
|
+
...props
|
|
97
|
+
}: React.ComponentProps<"li"> & { key?: React.Key }) => (
|
|
98
|
+
<li
|
|
99
|
+
role="presentation"
|
|
100
|
+
aria-hidden="true"
|
|
101
|
+
className={cn("[&>svg]:w-3.5 [&>svg]:h-3.5", className)}
|
|
102
|
+
{...props}
|
|
103
|
+
>
|
|
104
|
+
{children ?? <ChevronRight />}
|
|
105
|
+
</li>
|
|
106
|
+
)
|
|
107
|
+
BreadcrumbSeparator.displayName = "BreadcrumbSeparator"
|
|
108
|
+
|
|
109
|
+
const BreadcrumbEllipsis = ({
|
|
110
|
+
className,
|
|
111
|
+
...props
|
|
112
|
+
}: React.ComponentProps<"span">) => (
|
|
113
|
+
<span
|
|
114
|
+
role="presentation"
|
|
115
|
+
aria-hidden="true"
|
|
116
|
+
className={cn("flex h-9 w-9 items-center justify-center", className)}
|
|
117
|
+
{...props}
|
|
118
|
+
>
|
|
119
|
+
<MoreHorizontal className="h-4 w-4" />
|
|
120
|
+
<span className="sr-only">More</span>
|
|
121
|
+
</span>
|
|
122
|
+
)
|
|
123
|
+
BreadcrumbEllipsis.displayName = "BreadcrumbElipssis"
|
|
124
|
+
|
|
125
|
+
export {
|
|
126
|
+
Breadcrumb,
|
|
127
|
+
BreadcrumbList,
|
|
128
|
+
BreadcrumbItem,
|
|
129
|
+
BreadcrumbLink,
|
|
130
|
+
BreadcrumbPage,
|
|
131
|
+
BreadcrumbSeparator,
|
|
132
|
+
BreadcrumbEllipsis,
|
|
133
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export {
|
|
2
|
+
Breadcrumb,
|
|
3
|
+
BreadcrumbEllipsis,
|
|
4
|
+
BreadcrumbItem,
|
|
5
|
+
BreadcrumbLink,
|
|
6
|
+
BreadcrumbList,
|
|
7
|
+
BreadcrumbPage,
|
|
8
|
+
BreadcrumbSeparator,
|
|
9
|
+
} from './breadcrumb';
|
|
10
|
+
|
|
11
|
+
export { BreadcrumbNavigation } from './breadcrumb-navigation';
|
|
12
|
+
export type {
|
|
13
|
+
BreadcrumbItem as BreadcrumbNavigationItem,
|
|
14
|
+
BreadcrumbNavigationProps,
|
|
15
|
+
} from './breadcrumb-navigation';
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export {
|
|
2
|
+
Pagination,
|
|
3
|
+
PaginationContent,
|
|
4
|
+
PaginationEllipsis,
|
|
5
|
+
PaginationItem,
|
|
6
|
+
PaginationLink,
|
|
7
|
+
PaginationNext,
|
|
8
|
+
PaginationPrevious,
|
|
9
|
+
} from './pagination';
|
|
10
|
+
|
|
11
|
+
export {
|
|
12
|
+
StaticPagination,
|
|
13
|
+
useDRFPagination,
|
|
14
|
+
useDRFPaginationInfo,
|
|
15
|
+
} from './pagination-static';
|
|
16
|
+
export type { DRFPaginatedResponse } from './pagination-static';
|
|
17
|
+
|
|
18
|
+
export { SSRPagination } from './ssr-pagination';
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DRF Static Pagination Component
|
|
3
|
+
*
|
|
4
|
+
* Universal pagination component that works with Django REST Framework pagination format.
|
|
5
|
+
* Manages page state internally and provides callbacks for page changes.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
"use client"
|
|
9
|
+
|
|
10
|
+
import { ChevronLeft, ChevronRight } from 'lucide-react';
|
|
11
|
+
import React from 'react';
|
|
12
|
+
|
|
13
|
+
import { cn } from '../../../lib';
|
|
14
|
+
import { Button } from '../../forms/button';
|
|
15
|
+
import { useIsMobile } from '../../../hooks';
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
Pagination, PaginationContent, PaginationEllipsis, PaginationItem
|
|
19
|
+
} from './pagination';
|
|
20
|
+
|
|
21
|
+
export interface DRFPaginatedResponse<T = any> {
|
|
22
|
+
count: number;
|
|
23
|
+
page: number;
|
|
24
|
+
pages: number;
|
|
25
|
+
page_size: number;
|
|
26
|
+
has_next: boolean;
|
|
27
|
+
has_previous: boolean;
|
|
28
|
+
next_page?: number | null;
|
|
29
|
+
previous_page?: number | null;
|
|
30
|
+
results: T[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface StaticPaginationProps {
|
|
34
|
+
data?: DRFPaginatedResponse | null;
|
|
35
|
+
onPageChange: (page: number) => void;
|
|
36
|
+
className?: string;
|
|
37
|
+
showInfo?: boolean;
|
|
38
|
+
maxVisiblePages?: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const StaticPagination: React.FC<StaticPaginationProps> = ({
|
|
42
|
+
data,
|
|
43
|
+
onPageChange,
|
|
44
|
+
className,
|
|
45
|
+
showInfo = true,
|
|
46
|
+
maxVisiblePages = 7,
|
|
47
|
+
}) => {
|
|
48
|
+
const isMobile = useIsMobile();
|
|
49
|
+
|
|
50
|
+
if (!data || !('count' in data) || !('page' in data) || !('pages' in data)) {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const {
|
|
55
|
+
count: totalItems,
|
|
56
|
+
page: currentPage,
|
|
57
|
+
pages: totalPages,
|
|
58
|
+
page_size: itemsPerPage,
|
|
59
|
+
has_next: hasNextPage,
|
|
60
|
+
has_previous: hasPreviousPage,
|
|
61
|
+
} = data;
|
|
62
|
+
|
|
63
|
+
if (totalPages <= 1) {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const getVisiblePages = (): (number | 'ellipsis')[] => {
|
|
68
|
+
const mobileMaxVisible = 3;
|
|
69
|
+
const effectiveMaxVisible = isMobile ? mobileMaxVisible : maxVisiblePages;
|
|
70
|
+
|
|
71
|
+
if (totalPages <= effectiveMaxVisible) {
|
|
72
|
+
return Array.from({ length: totalPages }, (_, i) => i + 1);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const pages: (number | 'ellipsis')[] = [];
|
|
76
|
+
const halfVisible = Math.floor(effectiveMaxVisible / 2);
|
|
77
|
+
|
|
78
|
+
if (isMobile) {
|
|
79
|
+
if (currentPage > 1) {
|
|
80
|
+
pages.push(currentPage - 1);
|
|
81
|
+
}
|
|
82
|
+
pages.push(currentPage);
|
|
83
|
+
if (currentPage < totalPages) {
|
|
84
|
+
pages.push(currentPage + 1);
|
|
85
|
+
}
|
|
86
|
+
} else {
|
|
87
|
+
pages.push(1);
|
|
88
|
+
|
|
89
|
+
let start = Math.max(2, currentPage - halfVisible);
|
|
90
|
+
let end = Math.min(totalPages - 1, currentPage + halfVisible);
|
|
91
|
+
|
|
92
|
+
if (currentPage <= halfVisible + 1) {
|
|
93
|
+
end = Math.min(totalPages - 1, effectiveMaxVisible - 1);
|
|
94
|
+
} else if (currentPage >= totalPages - halfVisible) {
|
|
95
|
+
start = Math.max(2, totalPages - effectiveMaxVisible + 2);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (start > 2) {
|
|
99
|
+
pages.push('ellipsis');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
for (let i = start; i <= end; i++) {
|
|
103
|
+
pages.push(i);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (end < totalPages - 1) {
|
|
107
|
+
pages.push('ellipsis');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (totalPages > 1) {
|
|
111
|
+
pages.push(totalPages);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return pages;
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const visiblePages = getVisiblePages();
|
|
119
|
+
const startItem = (currentPage - 1) * itemsPerPage + 1;
|
|
120
|
+
const endItem = Math.min(currentPage * itemsPerPage, totalItems);
|
|
121
|
+
|
|
122
|
+
const handlePageClick = (page: number) => {
|
|
123
|
+
if (page !== currentPage && page >= 1 && page <= totalPages) {
|
|
124
|
+
onPageChange(page);
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
return (
|
|
129
|
+
<div className={cn("space-y-4", className)}>
|
|
130
|
+
{showInfo && (
|
|
131
|
+
<div className="text-sm text-muted-foreground text-center">
|
|
132
|
+
{isMobile ? (
|
|
133
|
+
`Page ${currentPage} of ${totalPages}`
|
|
134
|
+
) : (
|
|
135
|
+
`Showing ${startItem.toLocaleString()} to ${endItem.toLocaleString()} of ${totalItems.toLocaleString()} results`
|
|
136
|
+
)}
|
|
137
|
+
</div>
|
|
138
|
+
)}
|
|
139
|
+
|
|
140
|
+
<Pagination>
|
|
141
|
+
<PaginationContent>
|
|
142
|
+
<PaginationItem>
|
|
143
|
+
<Button
|
|
144
|
+
variant="ghost"
|
|
145
|
+
size="default"
|
|
146
|
+
onClick={() => handlePageClick(currentPage - 1)}
|
|
147
|
+
disabled={!hasPreviousPage}
|
|
148
|
+
className={cn(
|
|
149
|
+
"gap-1 pl-2.5",
|
|
150
|
+
!hasPreviousPage && "pointer-events-none opacity-50"
|
|
151
|
+
)}
|
|
152
|
+
aria-label="Go to previous page"
|
|
153
|
+
>
|
|
154
|
+
<ChevronLeft className="h-4 w-4" />
|
|
155
|
+
<span>Previous</span>
|
|
156
|
+
</Button>
|
|
157
|
+
</PaginationItem>
|
|
158
|
+
|
|
159
|
+
{visiblePages.map((page, index) => (
|
|
160
|
+
<PaginationItem key={index}>
|
|
161
|
+
{page === 'ellipsis' ? (
|
|
162
|
+
<PaginationEllipsis />
|
|
163
|
+
) : (
|
|
164
|
+
<Button
|
|
165
|
+
variant={page === currentPage ? "outline" : "ghost"}
|
|
166
|
+
size="icon"
|
|
167
|
+
onClick={() => handlePageClick(page)}
|
|
168
|
+
aria-current={page === currentPage ? "page" : undefined}
|
|
169
|
+
className={cn(
|
|
170
|
+
page === currentPage && "pointer-events-none"
|
|
171
|
+
)}
|
|
172
|
+
>
|
|
173
|
+
{page}
|
|
174
|
+
</Button>
|
|
175
|
+
)}
|
|
176
|
+
</PaginationItem>
|
|
177
|
+
))}
|
|
178
|
+
|
|
179
|
+
<PaginationItem>
|
|
180
|
+
<Button
|
|
181
|
+
variant="ghost"
|
|
182
|
+
size="default"
|
|
183
|
+
onClick={() => handlePageClick(currentPage + 1)}
|
|
184
|
+
disabled={!hasNextPage}
|
|
185
|
+
className={cn(
|
|
186
|
+
"gap-1 pr-2.5",
|
|
187
|
+
!hasNextPage && "pointer-events-none opacity-50"
|
|
188
|
+
)}
|
|
189
|
+
aria-label="Go to next page"
|
|
190
|
+
>
|
|
191
|
+
<span>Next</span>
|
|
192
|
+
<ChevronRight className="h-4 w-4" />
|
|
193
|
+
</Button>
|
|
194
|
+
</PaginationItem>
|
|
195
|
+
</PaginationContent>
|
|
196
|
+
</Pagination>
|
|
197
|
+
</div>
|
|
198
|
+
);
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
StaticPagination.displayName = 'StaticPagination';
|
|
202
|
+
|
|
203
|
+
export function useDRFPagination(initialPage = 1, initialPageSize = 10) {
|
|
204
|
+
const [page, setPage] = React.useState(initialPage);
|
|
205
|
+
const [pageSize, setPageSize] = React.useState(initialPageSize);
|
|
206
|
+
|
|
207
|
+
const handlePageSizeChange = React.useCallback((newPageSize: number) => {
|
|
208
|
+
setPageSize(newPageSize);
|
|
209
|
+
setPage(1);
|
|
210
|
+
}, []);
|
|
211
|
+
|
|
212
|
+
const params = React.useMemo(() => ({
|
|
213
|
+
page,
|
|
214
|
+
page_size: pageSize,
|
|
215
|
+
}), [page, pageSize]);
|
|
216
|
+
|
|
217
|
+
return {
|
|
218
|
+
page,
|
|
219
|
+
pageSize,
|
|
220
|
+
setPage,
|
|
221
|
+
setPageSize: handlePageSizeChange,
|
|
222
|
+
params,
|
|
223
|
+
reset: () => {
|
|
224
|
+
setPage(initialPage);
|
|
225
|
+
setPageSize(initialPageSize);
|
|
226
|
+
},
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export function useDRFPaginationInfo(data?: DRFPaginatedResponse | null) {
|
|
231
|
+
return React.useMemo(() => {
|
|
232
|
+
if (!data || !('count' in data) || !('page' in data)) {
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return {
|
|
237
|
+
totalItems: data.count,
|
|
238
|
+
currentPage: data.page,
|
|
239
|
+
totalPages: data.pages,
|
|
240
|
+
itemsPerPage: data.page_size,
|
|
241
|
+
hasNext: data.has_next,
|
|
242
|
+
hasPrevious: data.has_previous,
|
|
243
|
+
nextPage: data.next_page,
|
|
244
|
+
previousPage: data.previous_page,
|
|
245
|
+
results: data.results || [],
|
|
246
|
+
resultsCount: data.results?.length || 0,
|
|
247
|
+
};
|
|
248
|
+
}, [data]);
|
|
249
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { ChevronLeft, ChevronRight, MoreHorizontal } from 'lucide-react';
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
|
|
4
|
+
import { cn } from '../../../lib';
|
|
5
|
+
import { type ButtonProps, buttonVariants } from '../../forms/button';
|
|
6
|
+
import { Link } from '../link';
|
|
7
|
+
|
|
8
|
+
const Pagination = ({ className, ...props }: React.ComponentProps<"nav">) => (
|
|
9
|
+
<nav
|
|
10
|
+
role="navigation"
|
|
11
|
+
aria-label="pagination"
|
|
12
|
+
className={cn("mx-auto flex w-full justify-center", className)}
|
|
13
|
+
{...props}
|
|
14
|
+
/>
|
|
15
|
+
)
|
|
16
|
+
Pagination.displayName = "Pagination"
|
|
17
|
+
|
|
18
|
+
const PaginationContent = React.forwardRef<
|
|
19
|
+
HTMLUListElement,
|
|
20
|
+
React.ComponentProps<"ul">
|
|
21
|
+
>(({ className, ...props }, ref) => (
|
|
22
|
+
<ul
|
|
23
|
+
ref={ref}
|
|
24
|
+
className={cn("flex flex-row items-center gap-1", className)}
|
|
25
|
+
{...props}
|
|
26
|
+
/>
|
|
27
|
+
))
|
|
28
|
+
PaginationContent.displayName = "PaginationContent"
|
|
29
|
+
|
|
30
|
+
const PaginationItem = React.forwardRef<
|
|
31
|
+
HTMLLIElement,
|
|
32
|
+
React.ComponentProps<"li"> & { key?: React.Key }
|
|
33
|
+
>(({ className, ...props }, ref) => (
|
|
34
|
+
<li ref={ref} className={cn("", className)} {...props} />
|
|
35
|
+
))
|
|
36
|
+
PaginationItem.displayName = "PaginationItem"
|
|
37
|
+
|
|
38
|
+
type PaginationLinkProps = {
|
|
39
|
+
isActive?: boolean
|
|
40
|
+
href?: string
|
|
41
|
+
} & Pick<ButtonProps, "size"> &
|
|
42
|
+
React.ComponentProps<"a">
|
|
43
|
+
|
|
44
|
+
const PaginationLink = ({
|
|
45
|
+
className,
|
|
46
|
+
isActive,
|
|
47
|
+
size = "icon",
|
|
48
|
+
href,
|
|
49
|
+
children,
|
|
50
|
+
...props
|
|
51
|
+
}: PaginationLinkProps) => {
|
|
52
|
+
const classes = cn(
|
|
53
|
+
buttonVariants({
|
|
54
|
+
variant: isActive ? "outline" : "ghost",
|
|
55
|
+
size,
|
|
56
|
+
}),
|
|
57
|
+
className
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
if (href) {
|
|
61
|
+
return (
|
|
62
|
+
<Link
|
|
63
|
+
href={href}
|
|
64
|
+
aria-current={isActive ? "page" : undefined}
|
|
65
|
+
className={classes}
|
|
66
|
+
>
|
|
67
|
+
{children}
|
|
68
|
+
</Link>
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<a
|
|
74
|
+
aria-current={isActive ? "page" : undefined}
|
|
75
|
+
className={classes}
|
|
76
|
+
{...props}
|
|
77
|
+
>
|
|
78
|
+
{children}
|
|
79
|
+
</a>
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
PaginationLink.displayName = "PaginationLink"
|
|
83
|
+
|
|
84
|
+
const PaginationPrevious = ({
|
|
85
|
+
className,
|
|
86
|
+
...props
|
|
87
|
+
}: React.ComponentProps<typeof PaginationLink>) => (
|
|
88
|
+
<PaginationLink
|
|
89
|
+
aria-label="Go to previous page"
|
|
90
|
+
size="default"
|
|
91
|
+
className={cn("gap-1 pl-2.5", className)}
|
|
92
|
+
{...props}
|
|
93
|
+
>
|
|
94
|
+
<ChevronLeft className="h-4 w-4" />
|
|
95
|
+
<span>Previous</span>
|
|
96
|
+
</PaginationLink>
|
|
97
|
+
)
|
|
98
|
+
PaginationPrevious.displayName = "PaginationPrevious"
|
|
99
|
+
|
|
100
|
+
const PaginationNext = ({
|
|
101
|
+
className,
|
|
102
|
+
...props
|
|
103
|
+
}: React.ComponentProps<typeof PaginationLink>) => (
|
|
104
|
+
<PaginationLink
|
|
105
|
+
aria-label="Go to next page"
|
|
106
|
+
size="default"
|
|
107
|
+
className={cn("gap-1 pr-2.5", className)}
|
|
108
|
+
{...props}
|
|
109
|
+
>
|
|
110
|
+
<span>Next</span>
|
|
111
|
+
<ChevronRight className="h-4 w-4" />
|
|
112
|
+
</PaginationLink>
|
|
113
|
+
)
|
|
114
|
+
PaginationNext.displayName = "PaginationNext"
|
|
115
|
+
|
|
116
|
+
const PaginationEllipsis = ({
|
|
117
|
+
className,
|
|
118
|
+
...props
|
|
119
|
+
}: React.ComponentProps<"span">) => (
|
|
120
|
+
<span
|
|
121
|
+
aria-hidden
|
|
122
|
+
className={cn("flex h-9 w-9 items-center justify-center", className)}
|
|
123
|
+
{...props}
|
|
124
|
+
>
|
|
125
|
+
<MoreHorizontal className="h-4 w-4" />
|
|
126
|
+
<span className="sr-only">More pages</span>
|
|
127
|
+
</span>
|
|
128
|
+
)
|
|
129
|
+
PaginationEllipsis.displayName = "PaginationEllipsis"
|
|
130
|
+
|
|
131
|
+
export {
|
|
132
|
+
Pagination,
|
|
133
|
+
PaginationContent,
|
|
134
|
+
PaginationLink,
|
|
135
|
+
PaginationItem,
|
|
136
|
+
PaginationPrevious,
|
|
137
|
+
PaginationNext,
|
|
138
|
+
PaginationEllipsis,
|
|
139
|
+
}
|