@djangocfg/ui-nextjs 1.4.45
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/LICENSE +21 -0
- package/README.md +152 -0
- package/package.json +110 -0
- package/src/animations/AnimatedBackground.tsx +645 -0
- package/src/animations/index.ts +2 -0
- package/src/blocks/ArticleCard.tsx +94 -0
- package/src/blocks/ArticleList.tsx +95 -0
- package/src/blocks/CTASection.tsx +136 -0
- package/src/blocks/FeatureSection.tsx +104 -0
- package/src/blocks/Hero.tsx +102 -0
- package/src/blocks/NewsletterSection.tsx +119 -0
- package/src/blocks/StatsSection.tsx +103 -0
- package/src/blocks/SuperHero.tsx +328 -0
- package/src/blocks/TestimonialSection.tsx +122 -0
- package/src/blocks/index.ts +9 -0
- package/src/components/README.md +2018 -0
- package/src/components/breadcrumb-navigation.tsx +127 -0
- package/src/components/breadcrumb.tsx +132 -0
- package/src/components/button-download.tsx +275 -0
- package/src/components/dropdown-menu.tsx +219 -0
- package/src/components/index.ts +86 -0
- package/src/components/markdown/MarkdownMessage.tsx +338 -0
- package/src/components/markdown/index.ts +5 -0
- package/src/components/menubar.tsx +274 -0
- package/src/components/multi-select-pro/async.tsx +608 -0
- package/src/components/multi-select-pro/helpers.tsx +84 -0
- package/src/components/multi-select-pro/index.tsx +622 -0
- package/src/components/navigation-menu.tsx +153 -0
- package/src/components/pagination-static.tsx +348 -0
- package/src/components/pagination.tsx +138 -0
- package/src/components/phone-input.tsx +276 -0
- package/src/components/sidebar.tsx +866 -0
- package/src/components/sonner.tsx +31 -0
- package/src/components/ssr-pagination.tsx +237 -0
- package/src/hooks/index.ts +19 -0
- package/src/hooks/useCfgRouter.ts +153 -0
- package/src/hooks/useLocalStorage.ts +221 -0
- package/src/hooks/useQueryParams.ts +73 -0
- package/src/hooks/useSessionStorage.ts +188 -0
- package/src/hooks/useTheme.ts +57 -0
- package/src/index.ts +24 -0
- package/src/lib/index.ts +2 -0
- package/src/styles/index.css +2 -0
- package/src/theme/ForceTheme.tsx +115 -0
- package/src/theme/ThemeProvider.tsx +82 -0
- package/src/theme/ThemeToggle.tsx +52 -0
- package/src/theme/index.ts +3 -0
- package/src/tools/JsonForm/JsonSchemaForm.tsx +199 -0
- package/src/tools/JsonForm/examples/BotConfigExample.tsx +245 -0
- package/src/tools/JsonForm/examples/RealBotConfigExample.tsx +157 -0
- package/src/tools/JsonForm/index.ts +46 -0
- package/src/tools/JsonForm/templates/ArrayFieldItemTemplate.tsx +46 -0
- package/src/tools/JsonForm/templates/ArrayFieldTemplate.tsx +73 -0
- package/src/tools/JsonForm/templates/BaseInputTemplate.tsx +106 -0
- package/src/tools/JsonForm/templates/ErrorListTemplate.tsx +34 -0
- package/src/tools/JsonForm/templates/FieldTemplate.tsx +61 -0
- package/src/tools/JsonForm/templates/ObjectFieldTemplate.tsx +43 -0
- package/src/tools/JsonForm/templates/index.ts +12 -0
- package/src/tools/JsonForm/types.ts +83 -0
- package/src/tools/JsonForm/utils.ts +212 -0
- package/src/tools/JsonForm/widgets/CheckboxWidget.tsx +36 -0
- package/src/tools/JsonForm/widgets/NumberWidget.tsx +88 -0
- package/src/tools/JsonForm/widgets/SelectWidget.tsx +100 -0
- package/src/tools/JsonForm/widgets/SwitchWidget.tsx +34 -0
- package/src/tools/JsonForm/widgets/TextWidget.tsx +95 -0
- package/src/tools/JsonForm/widgets/index.ts +12 -0
- package/src/tools/JsonTree/index.tsx +252 -0
- package/src/tools/LottiePlayer/LottiePlayer.client.tsx +212 -0
- package/src/tools/LottiePlayer/index.tsx +54 -0
- package/src/tools/LottiePlayer/types.ts +108 -0
- package/src/tools/LottiePlayer/useLottie.ts +163 -0
- package/src/tools/Mermaid/Mermaid.client.tsx +341 -0
- package/src/tools/Mermaid/index.tsx +40 -0
- package/src/tools/OpenapiViewer/components/EndpointInfo.tsx +144 -0
- package/src/tools/OpenapiViewer/components/EndpointsLibrary.tsx +255 -0
- package/src/tools/OpenapiViewer/components/PlaygroundLayout.tsx +123 -0
- package/src/tools/OpenapiViewer/components/PlaygroundStepper.tsx +98 -0
- package/src/tools/OpenapiViewer/components/RequestBuilder.tsx +164 -0
- package/src/tools/OpenapiViewer/components/RequestParametersForm.tsx +253 -0
- package/src/tools/OpenapiViewer/components/ResponseViewer.tsx +169 -0
- package/src/tools/OpenapiViewer/components/VersionSelector.tsx +64 -0
- package/src/tools/OpenapiViewer/components/index.ts +14 -0
- package/src/tools/OpenapiViewer/constants.ts +39 -0
- package/src/tools/OpenapiViewer/context/PlaygroundContext.tsx +338 -0
- package/src/tools/OpenapiViewer/hooks/index.ts +8 -0
- package/src/tools/OpenapiViewer/hooks/useMobile.ts +10 -0
- package/src/tools/OpenapiViewer/hooks/useOpenApiSchema.ts +203 -0
- package/src/tools/OpenapiViewer/index.tsx +36 -0
- package/src/tools/OpenapiViewer/types.ts +152 -0
- package/src/tools/OpenapiViewer/utils/apiKeyManager.ts +149 -0
- package/src/tools/OpenapiViewer/utils/formatters.ts +71 -0
- package/src/tools/OpenapiViewer/utils/index.ts +9 -0
- package/src/tools/OpenapiViewer/utils/versionManager.ts +161 -0
- package/src/tools/PrettyCode/PrettyCode.client.tsx +217 -0
- package/src/tools/PrettyCode/index.tsx +43 -0
- package/src/tools/VideoPlayer/README.md +239 -0
- package/src/tools/VideoPlayer/VideoControls.tsx +138 -0
- package/src/tools/VideoPlayer/VideoPlayer.tsx +230 -0
- package/src/tools/VideoPlayer/index.ts +9 -0
- package/src/tools/VideoPlayer/types.ts +62 -0
- package/src/tools/index.ts +43 -0
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu"
|
|
5
|
+
import { cva } from "class-variance-authority"
|
|
6
|
+
import { cn } from "@djangocfg/ui-core/lib"
|
|
7
|
+
import { ChevronDownIcon } from "@radix-ui/react-icons"
|
|
8
|
+
import Link from "next/link"
|
|
9
|
+
|
|
10
|
+
const NavigationMenu = React.forwardRef<
|
|
11
|
+
React.ElementRef<typeof NavigationMenuPrimitive.Root>,
|
|
12
|
+
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Root>
|
|
13
|
+
>(({ className, children, ...props }, ref) => (
|
|
14
|
+
<NavigationMenuPrimitive.Root
|
|
15
|
+
ref={ref}
|
|
16
|
+
className={cn(
|
|
17
|
+
"relative z-10 flex max-w-max items-center justify-center",
|
|
18
|
+
className
|
|
19
|
+
)}
|
|
20
|
+
{...props}
|
|
21
|
+
>
|
|
22
|
+
{children}
|
|
23
|
+
<NavigationMenuViewport />
|
|
24
|
+
</NavigationMenuPrimitive.Root>
|
|
25
|
+
))
|
|
26
|
+
NavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName
|
|
27
|
+
|
|
28
|
+
const NavigationMenuList = React.forwardRef<
|
|
29
|
+
React.ElementRef<typeof NavigationMenuPrimitive.List>,
|
|
30
|
+
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.List>
|
|
31
|
+
>(({ className, ...props }, ref) => (
|
|
32
|
+
<NavigationMenuPrimitive.List
|
|
33
|
+
ref={ref}
|
|
34
|
+
className={cn(
|
|
35
|
+
"group flex flex-1 list-none items-center justify-center space-x-1",
|
|
36
|
+
className
|
|
37
|
+
)}
|
|
38
|
+
{...props}
|
|
39
|
+
/>
|
|
40
|
+
))
|
|
41
|
+
NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName
|
|
42
|
+
|
|
43
|
+
const NavigationMenuItem = NavigationMenuPrimitive.Item
|
|
44
|
+
|
|
45
|
+
const navigationMenuTriggerStyle = cva(
|
|
46
|
+
"group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[state=open]:text-accent-foreground data-[state=open]:bg-accent/50 data-[state=open]:hover:bg-accent data-[state=open]:focus:bg-accent"
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
const NavigationMenuTrigger = React.forwardRef<
|
|
50
|
+
React.ElementRef<typeof NavigationMenuPrimitive.Trigger>,
|
|
51
|
+
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Trigger>
|
|
52
|
+
>(({ className, children, ...props }, ref) => (
|
|
53
|
+
<NavigationMenuPrimitive.Trigger
|
|
54
|
+
ref={ref}
|
|
55
|
+
className={cn(navigationMenuTriggerStyle(), "group", className)}
|
|
56
|
+
{...props}
|
|
57
|
+
>
|
|
58
|
+
{children}{" "}
|
|
59
|
+
<ChevronDownIcon
|
|
60
|
+
className="relative top-[1px] ml-1 h-3 w-3 transition duration-300 group-data-[state=open]:rotate-180"
|
|
61
|
+
aria-hidden="true"
|
|
62
|
+
/>
|
|
63
|
+
</NavigationMenuPrimitive.Trigger>
|
|
64
|
+
))
|
|
65
|
+
NavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName
|
|
66
|
+
|
|
67
|
+
const NavigationMenuContent = React.forwardRef<
|
|
68
|
+
React.ElementRef<typeof NavigationMenuPrimitive.Content>,
|
|
69
|
+
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Content>
|
|
70
|
+
>(({ className, ...props }, ref) => (
|
|
71
|
+
<NavigationMenuPrimitive.Content
|
|
72
|
+
ref={ref}
|
|
73
|
+
className={cn(
|
|
74
|
+
"left-0 top-0 w-full data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 md:absolute md:w-auto ",
|
|
75
|
+
className
|
|
76
|
+
)}
|
|
77
|
+
{...props}
|
|
78
|
+
/>
|
|
79
|
+
))
|
|
80
|
+
NavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName
|
|
81
|
+
|
|
82
|
+
const NavigationMenuLink = React.forwardRef<
|
|
83
|
+
React.ElementRef<typeof NavigationMenuPrimitive.Link>,
|
|
84
|
+
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Link> & {
|
|
85
|
+
href?: string
|
|
86
|
+
}
|
|
87
|
+
>(({ href, children, ...props }, ref) => {
|
|
88
|
+
if (href) {
|
|
89
|
+
return (
|
|
90
|
+
<NavigationMenuPrimitive.Link asChild ref={ref} {...props}>
|
|
91
|
+
<Link href={href}>{children}</Link>
|
|
92
|
+
</NavigationMenuPrimitive.Link>
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return (
|
|
97
|
+
<NavigationMenuPrimitive.Link ref={ref} {...props}>
|
|
98
|
+
{children}
|
|
99
|
+
</NavigationMenuPrimitive.Link>
|
|
100
|
+
)
|
|
101
|
+
})
|
|
102
|
+
NavigationMenuLink.displayName = "NavigationMenuLink"
|
|
103
|
+
|
|
104
|
+
const NavigationMenuViewport = React.forwardRef<
|
|
105
|
+
React.ElementRef<typeof NavigationMenuPrimitive.Viewport>,
|
|
106
|
+
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Viewport>
|
|
107
|
+
>(({ className, ...props }, ref) => (
|
|
108
|
+
<div
|
|
109
|
+
className={cn("absolute top-full flex justify-center")}
|
|
110
|
+
style={{ left: 0, right: 0 }}
|
|
111
|
+
>
|
|
112
|
+
<NavigationMenuPrimitive.Viewport
|
|
113
|
+
className={cn(
|
|
114
|
+
"origin-top-center relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] overflow-hidden rounded-md border backdrop-blur-xl bg-popover/80 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 w-[var(--radix-navigation-menu-viewport-width)]",
|
|
115
|
+
className
|
|
116
|
+
)}
|
|
117
|
+
ref={ref}
|
|
118
|
+
{...props}
|
|
119
|
+
/>
|
|
120
|
+
</div>
|
|
121
|
+
))
|
|
122
|
+
NavigationMenuViewport.displayName =
|
|
123
|
+
NavigationMenuPrimitive.Viewport.displayName
|
|
124
|
+
|
|
125
|
+
const NavigationMenuIndicator = React.forwardRef<
|
|
126
|
+
React.ElementRef<typeof NavigationMenuPrimitive.Indicator>,
|
|
127
|
+
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Indicator>
|
|
128
|
+
>(({ className, ...props }, ref) => (
|
|
129
|
+
<NavigationMenuPrimitive.Indicator
|
|
130
|
+
ref={ref}
|
|
131
|
+
className={cn(
|
|
132
|
+
"top-full z-10 flex h-1.5 items-end justify-center overflow-hidden data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in",
|
|
133
|
+
className
|
|
134
|
+
)}
|
|
135
|
+
{...props}
|
|
136
|
+
>
|
|
137
|
+
<div className="relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm bg-border shadow-md" />
|
|
138
|
+
</NavigationMenuPrimitive.Indicator>
|
|
139
|
+
))
|
|
140
|
+
NavigationMenuIndicator.displayName =
|
|
141
|
+
NavigationMenuPrimitive.Indicator.displayName
|
|
142
|
+
|
|
143
|
+
export {
|
|
144
|
+
navigationMenuTriggerStyle,
|
|
145
|
+
NavigationMenu,
|
|
146
|
+
NavigationMenuList,
|
|
147
|
+
NavigationMenuItem,
|
|
148
|
+
NavigationMenuContent,
|
|
149
|
+
NavigationMenuTrigger,
|
|
150
|
+
NavigationMenuLink,
|
|
151
|
+
NavigationMenuIndicator,
|
|
152
|
+
NavigationMenuViewport,
|
|
153
|
+
}
|
|
@@ -0,0 +1,348 @@
|
|
|
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
|
+
* DRF Pagination Response Format:
|
|
8
|
+
* {
|
|
9
|
+
* "count": 150, // Total number of items
|
|
10
|
+
* "page": 1, // Current page number
|
|
11
|
+
* "pages": 15, // Total number of pages
|
|
12
|
+
* "page_size": 10, // Items per page
|
|
13
|
+
* "has_next": true, // Whether there's a next page
|
|
14
|
+
* "has_previous": false, // Whether there's a previous page
|
|
15
|
+
* "next_page": 2, // Next page number (or null)
|
|
16
|
+
* "previous_page": null, // Previous page number (or null)
|
|
17
|
+
* "results": [...] // Array of items
|
|
18
|
+
* }
|
|
19
|
+
*
|
|
20
|
+
* Usage with SWR:
|
|
21
|
+
* ```tsx
|
|
22
|
+
* import { StaticPagination, useDRFPagination } from '@djangocfg/ui-nextjs';
|
|
23
|
+
*
|
|
24
|
+
* function MyComponent() {
|
|
25
|
+
* const pagination = useDRFPagination();
|
|
26
|
+
* const { data, isLoading } = useMyAPI(pagination.params);
|
|
27
|
+
*
|
|
28
|
+
* return (
|
|
29
|
+
* <div>
|
|
30
|
+
* {data?.results?.map(item => <Item key={item.id} {...item} />)}
|
|
31
|
+
* <StaticPagination data={data} onPageChange={pagination.setPage} />
|
|
32
|
+
* </div>
|
|
33
|
+
* );
|
|
34
|
+
* }
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
"use client"
|
|
39
|
+
|
|
40
|
+
import React from 'react';
|
|
41
|
+
import { cn } from '@djangocfg/ui-core/lib';
|
|
42
|
+
import { useIsMobile } from '@djangocfg/ui-core/hooks';
|
|
43
|
+
import { Button } from '@djangocfg/ui-core/components';
|
|
44
|
+
import {
|
|
45
|
+
Pagination,
|
|
46
|
+
PaginationContent,
|
|
47
|
+
PaginationEllipsis,
|
|
48
|
+
PaginationItem,
|
|
49
|
+
PaginationLink,
|
|
50
|
+
PaginationNext,
|
|
51
|
+
PaginationPrevious,
|
|
52
|
+
} from './pagination';
|
|
53
|
+
import { ChevronLeft, ChevronRight } from 'lucide-react';
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* DRF Pagination Response type
|
|
57
|
+
*/
|
|
58
|
+
export interface DRFPaginatedResponse<T = any> {
|
|
59
|
+
count: number;
|
|
60
|
+
page: number;
|
|
61
|
+
pages: number;
|
|
62
|
+
page_size: number;
|
|
63
|
+
has_next: boolean;
|
|
64
|
+
has_previous: boolean;
|
|
65
|
+
next_page?: number | null;
|
|
66
|
+
previous_page?: number | null;
|
|
67
|
+
results: T[];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
interface StaticPaginationProps {
|
|
71
|
+
/** The DRF paginated response data */
|
|
72
|
+
data?: DRFPaginatedResponse | null;
|
|
73
|
+
/** Callback when page changes */
|
|
74
|
+
onPageChange: (page: number) => void;
|
|
75
|
+
/** Additional CSS class */
|
|
76
|
+
className?: string;
|
|
77
|
+
/** Show pagination info (default: true) */
|
|
78
|
+
showInfo?: boolean;
|
|
79
|
+
/** Maximum visible page numbers (default: 7) */
|
|
80
|
+
maxVisiblePages?: number;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export const StaticPagination: React.FC<StaticPaginationProps> = ({
|
|
84
|
+
data,
|
|
85
|
+
onPageChange,
|
|
86
|
+
className,
|
|
87
|
+
showInfo = true,
|
|
88
|
+
maxVisiblePages = 7,
|
|
89
|
+
}) => {
|
|
90
|
+
const isMobile = useIsMobile();
|
|
91
|
+
|
|
92
|
+
// Don't render if no data or not paginated
|
|
93
|
+
if (!data || !('count' in data) || !('page' in data) || !('pages' in data)) {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Extract pagination metadata
|
|
98
|
+
const {
|
|
99
|
+
count: totalItems,
|
|
100
|
+
page: currentPage,
|
|
101
|
+
pages: totalPages,
|
|
102
|
+
page_size: itemsPerPage,
|
|
103
|
+
has_next: hasNextPage,
|
|
104
|
+
has_previous: hasPreviousPage,
|
|
105
|
+
} = data;
|
|
106
|
+
|
|
107
|
+
// Don't render if only one page
|
|
108
|
+
if (totalPages <= 1) {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Generate array of page numbers to display
|
|
113
|
+
const getVisiblePages = (): (number | 'ellipsis')[] => {
|
|
114
|
+
// On mobile, show fewer pages
|
|
115
|
+
const mobileMaxVisible = 3;
|
|
116
|
+
const effectiveMaxVisible = isMobile ? mobileMaxVisible : maxVisiblePages;
|
|
117
|
+
|
|
118
|
+
if (totalPages <= effectiveMaxVisible) {
|
|
119
|
+
return Array.from({ length: totalPages }, (_, i) => i + 1);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const pages: (number | 'ellipsis')[] = [];
|
|
123
|
+
const halfVisible = Math.floor(effectiveMaxVisible / 2);
|
|
124
|
+
|
|
125
|
+
if (isMobile) {
|
|
126
|
+
// Mobile: Show only current page and adjacent pages
|
|
127
|
+
if (currentPage > 1) {
|
|
128
|
+
pages.push(currentPage - 1);
|
|
129
|
+
}
|
|
130
|
+
pages.push(currentPage);
|
|
131
|
+
if (currentPage < totalPages) {
|
|
132
|
+
pages.push(currentPage + 1);
|
|
133
|
+
}
|
|
134
|
+
} else {
|
|
135
|
+
// Desktop: Full pagination logic
|
|
136
|
+
// Always show first page
|
|
137
|
+
pages.push(1);
|
|
138
|
+
|
|
139
|
+
let start = Math.max(2, currentPage - halfVisible);
|
|
140
|
+
let end = Math.min(totalPages - 1, currentPage + halfVisible);
|
|
141
|
+
|
|
142
|
+
// Adjust range if we're near the beginning or end
|
|
143
|
+
if (currentPage <= halfVisible + 1) {
|
|
144
|
+
end = Math.min(totalPages - 1, effectiveMaxVisible - 1);
|
|
145
|
+
} else if (currentPage >= totalPages - halfVisible) {
|
|
146
|
+
start = Math.max(2, totalPages - effectiveMaxVisible + 2);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Add ellipsis after first page if needed
|
|
150
|
+
if (start > 2) {
|
|
151
|
+
pages.push('ellipsis');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Add middle pages
|
|
155
|
+
for (let i = start; i <= end; i++) {
|
|
156
|
+
pages.push(i);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Add ellipsis before last page if needed
|
|
160
|
+
if (end < totalPages - 1) {
|
|
161
|
+
pages.push('ellipsis');
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Always show last page (if more than 1 page)
|
|
165
|
+
if (totalPages > 1) {
|
|
166
|
+
pages.push(totalPages);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return pages;
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const visiblePages = getVisiblePages();
|
|
174
|
+
const startItem = (currentPage - 1) * itemsPerPage + 1;
|
|
175
|
+
const endItem = Math.min(currentPage * itemsPerPage, totalItems);
|
|
176
|
+
|
|
177
|
+
const handlePageClick = (page: number) => {
|
|
178
|
+
if (page !== currentPage && page >= 1 && page <= totalPages) {
|
|
179
|
+
onPageChange(page);
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
return (
|
|
184
|
+
<div className={cn("space-y-4", className)}>
|
|
185
|
+
{/* Pagination Info */}
|
|
186
|
+
{showInfo && (
|
|
187
|
+
<div className="text-sm text-muted-foreground text-center">
|
|
188
|
+
{isMobile ? (
|
|
189
|
+
`Page ${currentPage} of ${totalPages}`
|
|
190
|
+
) : (
|
|
191
|
+
`Showing ${startItem.toLocaleString()} to ${endItem.toLocaleString()} of ${totalItems.toLocaleString()} results`
|
|
192
|
+
)}
|
|
193
|
+
</div>
|
|
194
|
+
)}
|
|
195
|
+
|
|
196
|
+
{/* Pagination Controls */}
|
|
197
|
+
<Pagination>
|
|
198
|
+
<PaginationContent>
|
|
199
|
+
{/* Previous Button */}
|
|
200
|
+
<PaginationItem>
|
|
201
|
+
<Button
|
|
202
|
+
variant="ghost"
|
|
203
|
+
size="default"
|
|
204
|
+
onClick={() => handlePageClick(currentPage - 1)}
|
|
205
|
+
disabled={!hasPreviousPage}
|
|
206
|
+
className={cn(
|
|
207
|
+
"gap-1 pl-2.5",
|
|
208
|
+
!hasPreviousPage && "pointer-events-none opacity-50"
|
|
209
|
+
)}
|
|
210
|
+
aria-label="Go to previous page"
|
|
211
|
+
>
|
|
212
|
+
<ChevronLeft className="h-4 w-4" />
|
|
213
|
+
<span>Previous</span>
|
|
214
|
+
</Button>
|
|
215
|
+
</PaginationItem>
|
|
216
|
+
|
|
217
|
+
{/* Page Numbers */}
|
|
218
|
+
{visiblePages.map((page, index) => (
|
|
219
|
+
<PaginationItem key={index}>
|
|
220
|
+
{page === 'ellipsis' ? (
|
|
221
|
+
<PaginationEllipsis />
|
|
222
|
+
) : (
|
|
223
|
+
<Button
|
|
224
|
+
variant={page === currentPage ? "outline" : "ghost"}
|
|
225
|
+
size="icon"
|
|
226
|
+
onClick={() => handlePageClick(page)}
|
|
227
|
+
aria-current={page === currentPage ? "page" : undefined}
|
|
228
|
+
className={cn(
|
|
229
|
+
page === currentPage && "pointer-events-none"
|
|
230
|
+
)}
|
|
231
|
+
>
|
|
232
|
+
{page}
|
|
233
|
+
</Button>
|
|
234
|
+
)}
|
|
235
|
+
</PaginationItem>
|
|
236
|
+
))}
|
|
237
|
+
|
|
238
|
+
{/* Next Button */}
|
|
239
|
+
<PaginationItem>
|
|
240
|
+
<Button
|
|
241
|
+
variant="ghost"
|
|
242
|
+
size="default"
|
|
243
|
+
onClick={() => handlePageClick(currentPage + 1)}
|
|
244
|
+
disabled={!hasNextPage}
|
|
245
|
+
className={cn(
|
|
246
|
+
"gap-1 pr-2.5",
|
|
247
|
+
!hasNextPage && "pointer-events-none opacity-50"
|
|
248
|
+
)}
|
|
249
|
+
aria-label="Go to next page"
|
|
250
|
+
>
|
|
251
|
+
<span>Next</span>
|
|
252
|
+
<ChevronRight className="h-4 w-4" />
|
|
253
|
+
</Button>
|
|
254
|
+
</PaginationItem>
|
|
255
|
+
</PaginationContent>
|
|
256
|
+
</Pagination>
|
|
257
|
+
</div>
|
|
258
|
+
);
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
StaticPagination.displayName = 'StaticPagination';
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Hook to manage DRF pagination state
|
|
265
|
+
*
|
|
266
|
+
* Manages page and page_size state and provides methods to change them.
|
|
267
|
+
* Works seamlessly with SWR hooks that accept params.
|
|
268
|
+
*
|
|
269
|
+
* Usage:
|
|
270
|
+
* ```tsx
|
|
271
|
+
* import { useDRFPagination } from '@djangocfg/ui-nextjs';
|
|
272
|
+
* import { useMyPaginatedAPI } from './api/hooks';
|
|
273
|
+
*
|
|
274
|
+
* function MyComponent() {
|
|
275
|
+
* const pagination = useDRFPagination();
|
|
276
|
+
* const { data, isLoading } = useMyPaginatedAPI(pagination.params);
|
|
277
|
+
*
|
|
278
|
+
* return (
|
|
279
|
+
* <div>
|
|
280
|
+
* {data?.results?.map(item => <Item key={item.id} {...item} />)}
|
|
281
|
+
* <StaticPagination data={data} onPageChange={pagination.setPage} />
|
|
282
|
+
* </div>
|
|
283
|
+
* );
|
|
284
|
+
* }
|
|
285
|
+
* ```
|
|
286
|
+
*/
|
|
287
|
+
export function useDRFPagination(initialPage = 1, initialPageSize = 10) {
|
|
288
|
+
const [page, setPage] = React.useState(initialPage);
|
|
289
|
+
const [pageSize, setPageSize] = React.useState(initialPageSize);
|
|
290
|
+
|
|
291
|
+
// Reset to page 1 when page size changes
|
|
292
|
+
const handlePageSizeChange = React.useCallback((newPageSize: number) => {
|
|
293
|
+
setPageSize(newPageSize);
|
|
294
|
+
setPage(1);
|
|
295
|
+
}, []);
|
|
296
|
+
|
|
297
|
+
// Params object that can be passed directly to SWR hooks
|
|
298
|
+
const params = React.useMemo(() => ({
|
|
299
|
+
page,
|
|
300
|
+
page_size: pageSize,
|
|
301
|
+
}), [page, pageSize]);
|
|
302
|
+
|
|
303
|
+
return {
|
|
304
|
+
page,
|
|
305
|
+
pageSize,
|
|
306
|
+
setPage,
|
|
307
|
+
setPageSize: handlePageSizeChange,
|
|
308
|
+
params,
|
|
309
|
+
reset: () => {
|
|
310
|
+
setPage(initialPage);
|
|
311
|
+
setPageSize(initialPageSize);
|
|
312
|
+
},
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Hook to extract pagination info from DRF response
|
|
318
|
+
*
|
|
319
|
+
* Useful when you need pagination metadata in your component logic.
|
|
320
|
+
*
|
|
321
|
+
* Usage:
|
|
322
|
+
* ```tsx
|
|
323
|
+
* const paginationInfo = useDRFPaginationInfo(data);
|
|
324
|
+
* if (paginationInfo) {
|
|
325
|
+
* console.log(`Page ${paginationInfo.currentPage} of ${paginationInfo.totalPages}`);
|
|
326
|
+
* }
|
|
327
|
+
* ```
|
|
328
|
+
*/
|
|
329
|
+
export function useDRFPaginationInfo(data?: DRFPaginatedResponse | null) {
|
|
330
|
+
return React.useMemo(() => {
|
|
331
|
+
if (!data || !('count' in data) || !('page' in data)) {
|
|
332
|
+
return null;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return {
|
|
336
|
+
totalItems: data.count,
|
|
337
|
+
currentPage: data.page,
|
|
338
|
+
totalPages: data.pages,
|
|
339
|
+
itemsPerPage: data.page_size,
|
|
340
|
+
hasNext: data.has_next,
|
|
341
|
+
hasPrevious: data.has_previous,
|
|
342
|
+
nextPage: data.next_page,
|
|
343
|
+
previousPage: data.previous_page,
|
|
344
|
+
results: data.results || [],
|
|
345
|
+
resultsCount: data.results?.length || 0,
|
|
346
|
+
};
|
|
347
|
+
}, [data]);
|
|
348
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { cn } from "@djangocfg/ui-core/lib"
|
|
3
|
+
import { type ButtonProps, buttonVariants } from "@djangocfg/ui-core/components"
|
|
4
|
+
import { ChevronLeft, ChevronRight, MoreHorizontal } from "lucide-react"
|
|
5
|
+
import Link from "next/link"
|
|
6
|
+
|
|
7
|
+
const Pagination = ({ className, ...props }: React.ComponentProps<"nav">) => (
|
|
8
|
+
<nav
|
|
9
|
+
role="navigation"
|
|
10
|
+
aria-label="pagination"
|
|
11
|
+
className={cn("mx-auto flex w-full justify-center", className)}
|
|
12
|
+
{...props}
|
|
13
|
+
/>
|
|
14
|
+
)
|
|
15
|
+
Pagination.displayName = "Pagination"
|
|
16
|
+
|
|
17
|
+
const PaginationContent = React.forwardRef<
|
|
18
|
+
HTMLUListElement,
|
|
19
|
+
React.ComponentProps<"ul">
|
|
20
|
+
>(({ className, ...props }, ref) => (
|
|
21
|
+
<ul
|
|
22
|
+
ref={ref}
|
|
23
|
+
className={cn("flex flex-row items-center gap-1", className)}
|
|
24
|
+
{...props}
|
|
25
|
+
/>
|
|
26
|
+
))
|
|
27
|
+
PaginationContent.displayName = "PaginationContent"
|
|
28
|
+
|
|
29
|
+
const PaginationItem = React.forwardRef<
|
|
30
|
+
HTMLLIElement,
|
|
31
|
+
React.ComponentProps<"li"> & { key?: React.Key }
|
|
32
|
+
>(({ className, ...props }, ref) => (
|
|
33
|
+
<li ref={ref} className={cn("", className)} {...props} />
|
|
34
|
+
))
|
|
35
|
+
PaginationItem.displayName = "PaginationItem"
|
|
36
|
+
|
|
37
|
+
type PaginationLinkProps = {
|
|
38
|
+
isActive?: boolean
|
|
39
|
+
href?: string
|
|
40
|
+
} & Pick<ButtonProps, "size"> &
|
|
41
|
+
React.ComponentProps<"a">
|
|
42
|
+
|
|
43
|
+
const PaginationLink = ({
|
|
44
|
+
className,
|
|
45
|
+
isActive,
|
|
46
|
+
size = "icon",
|
|
47
|
+
href,
|
|
48
|
+
children,
|
|
49
|
+
...props
|
|
50
|
+
}: PaginationLinkProps) => {
|
|
51
|
+
const classes = cn(
|
|
52
|
+
buttonVariants({
|
|
53
|
+
variant: isActive ? "outline" : "ghost",
|
|
54
|
+
size,
|
|
55
|
+
}),
|
|
56
|
+
className
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
if (href) {
|
|
60
|
+
return (
|
|
61
|
+
<Link
|
|
62
|
+
href={href}
|
|
63
|
+
aria-current={isActive ? "page" : undefined}
|
|
64
|
+
className={classes}
|
|
65
|
+
>
|
|
66
|
+
{children}
|
|
67
|
+
</Link>
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<a
|
|
73
|
+
aria-current={isActive ? "page" : undefined}
|
|
74
|
+
className={classes}
|
|
75
|
+
{...props}
|
|
76
|
+
>
|
|
77
|
+
{children}
|
|
78
|
+
</a>
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
PaginationLink.displayName = "PaginationLink"
|
|
82
|
+
|
|
83
|
+
const PaginationPrevious = ({
|
|
84
|
+
className,
|
|
85
|
+
...props
|
|
86
|
+
}: React.ComponentProps<typeof PaginationLink>) => (
|
|
87
|
+
<PaginationLink
|
|
88
|
+
aria-label="Go to previous page"
|
|
89
|
+
size="default"
|
|
90
|
+
className={cn("gap-1 pl-2.5", className)}
|
|
91
|
+
{...props}
|
|
92
|
+
>
|
|
93
|
+
<ChevronLeft className="h-4 w-4" />
|
|
94
|
+
<span>Previous</span>
|
|
95
|
+
</PaginationLink>
|
|
96
|
+
)
|
|
97
|
+
PaginationPrevious.displayName = "PaginationPrevious"
|
|
98
|
+
|
|
99
|
+
const PaginationNext = ({
|
|
100
|
+
className,
|
|
101
|
+
...props
|
|
102
|
+
}: React.ComponentProps<typeof PaginationLink>) => (
|
|
103
|
+
<PaginationLink
|
|
104
|
+
aria-label="Go to next page"
|
|
105
|
+
size="default"
|
|
106
|
+
className={cn("gap-1 pr-2.5", className)}
|
|
107
|
+
{...props}
|
|
108
|
+
>
|
|
109
|
+
<span>Next</span>
|
|
110
|
+
<ChevronRight className="h-4 w-4" />
|
|
111
|
+
</PaginationLink>
|
|
112
|
+
)
|
|
113
|
+
PaginationNext.displayName = "PaginationNext"
|
|
114
|
+
|
|
115
|
+
const PaginationEllipsis = ({
|
|
116
|
+
className,
|
|
117
|
+
...props
|
|
118
|
+
}: React.ComponentProps<"span">) => (
|
|
119
|
+
<span
|
|
120
|
+
aria-hidden
|
|
121
|
+
className={cn("flex h-9 w-9 items-center justify-center", className)}
|
|
122
|
+
{...props}
|
|
123
|
+
>
|
|
124
|
+
<MoreHorizontal className="h-4 w-4" />
|
|
125
|
+
<span className="sr-only">More pages</span>
|
|
126
|
+
</span>
|
|
127
|
+
)
|
|
128
|
+
PaginationEllipsis.displayName = "PaginationEllipsis"
|
|
129
|
+
|
|
130
|
+
export {
|
|
131
|
+
Pagination,
|
|
132
|
+
PaginationContent,
|
|
133
|
+
PaginationLink,
|
|
134
|
+
PaginationItem,
|
|
135
|
+
PaginationPrevious,
|
|
136
|
+
PaginationNext,
|
|
137
|
+
PaginationEllipsis,
|
|
138
|
+
}
|