@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.
Files changed (101) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +152 -0
  3. package/package.json +110 -0
  4. package/src/animations/AnimatedBackground.tsx +645 -0
  5. package/src/animations/index.ts +2 -0
  6. package/src/blocks/ArticleCard.tsx +94 -0
  7. package/src/blocks/ArticleList.tsx +95 -0
  8. package/src/blocks/CTASection.tsx +136 -0
  9. package/src/blocks/FeatureSection.tsx +104 -0
  10. package/src/blocks/Hero.tsx +102 -0
  11. package/src/blocks/NewsletterSection.tsx +119 -0
  12. package/src/blocks/StatsSection.tsx +103 -0
  13. package/src/blocks/SuperHero.tsx +328 -0
  14. package/src/blocks/TestimonialSection.tsx +122 -0
  15. package/src/blocks/index.ts +9 -0
  16. package/src/components/README.md +2018 -0
  17. package/src/components/breadcrumb-navigation.tsx +127 -0
  18. package/src/components/breadcrumb.tsx +132 -0
  19. package/src/components/button-download.tsx +275 -0
  20. package/src/components/dropdown-menu.tsx +219 -0
  21. package/src/components/index.ts +86 -0
  22. package/src/components/markdown/MarkdownMessage.tsx +338 -0
  23. package/src/components/markdown/index.ts +5 -0
  24. package/src/components/menubar.tsx +274 -0
  25. package/src/components/multi-select-pro/async.tsx +608 -0
  26. package/src/components/multi-select-pro/helpers.tsx +84 -0
  27. package/src/components/multi-select-pro/index.tsx +622 -0
  28. package/src/components/navigation-menu.tsx +153 -0
  29. package/src/components/pagination-static.tsx +348 -0
  30. package/src/components/pagination.tsx +138 -0
  31. package/src/components/phone-input.tsx +276 -0
  32. package/src/components/sidebar.tsx +866 -0
  33. package/src/components/sonner.tsx +31 -0
  34. package/src/components/ssr-pagination.tsx +237 -0
  35. package/src/hooks/index.ts +19 -0
  36. package/src/hooks/useCfgRouter.ts +153 -0
  37. package/src/hooks/useLocalStorage.ts +221 -0
  38. package/src/hooks/useQueryParams.ts +73 -0
  39. package/src/hooks/useSessionStorage.ts +188 -0
  40. package/src/hooks/useTheme.ts +57 -0
  41. package/src/index.ts +24 -0
  42. package/src/lib/index.ts +2 -0
  43. package/src/styles/index.css +2 -0
  44. package/src/theme/ForceTheme.tsx +115 -0
  45. package/src/theme/ThemeProvider.tsx +82 -0
  46. package/src/theme/ThemeToggle.tsx +52 -0
  47. package/src/theme/index.ts +3 -0
  48. package/src/tools/JsonForm/JsonSchemaForm.tsx +199 -0
  49. package/src/tools/JsonForm/examples/BotConfigExample.tsx +245 -0
  50. package/src/tools/JsonForm/examples/RealBotConfigExample.tsx +157 -0
  51. package/src/tools/JsonForm/index.ts +46 -0
  52. package/src/tools/JsonForm/templates/ArrayFieldItemTemplate.tsx +46 -0
  53. package/src/tools/JsonForm/templates/ArrayFieldTemplate.tsx +73 -0
  54. package/src/tools/JsonForm/templates/BaseInputTemplate.tsx +106 -0
  55. package/src/tools/JsonForm/templates/ErrorListTemplate.tsx +34 -0
  56. package/src/tools/JsonForm/templates/FieldTemplate.tsx +61 -0
  57. package/src/tools/JsonForm/templates/ObjectFieldTemplate.tsx +43 -0
  58. package/src/tools/JsonForm/templates/index.ts +12 -0
  59. package/src/tools/JsonForm/types.ts +83 -0
  60. package/src/tools/JsonForm/utils.ts +212 -0
  61. package/src/tools/JsonForm/widgets/CheckboxWidget.tsx +36 -0
  62. package/src/tools/JsonForm/widgets/NumberWidget.tsx +88 -0
  63. package/src/tools/JsonForm/widgets/SelectWidget.tsx +100 -0
  64. package/src/tools/JsonForm/widgets/SwitchWidget.tsx +34 -0
  65. package/src/tools/JsonForm/widgets/TextWidget.tsx +95 -0
  66. package/src/tools/JsonForm/widgets/index.ts +12 -0
  67. package/src/tools/JsonTree/index.tsx +252 -0
  68. package/src/tools/LottiePlayer/LottiePlayer.client.tsx +212 -0
  69. package/src/tools/LottiePlayer/index.tsx +54 -0
  70. package/src/tools/LottiePlayer/types.ts +108 -0
  71. package/src/tools/LottiePlayer/useLottie.ts +163 -0
  72. package/src/tools/Mermaid/Mermaid.client.tsx +341 -0
  73. package/src/tools/Mermaid/index.tsx +40 -0
  74. package/src/tools/OpenapiViewer/components/EndpointInfo.tsx +144 -0
  75. package/src/tools/OpenapiViewer/components/EndpointsLibrary.tsx +255 -0
  76. package/src/tools/OpenapiViewer/components/PlaygroundLayout.tsx +123 -0
  77. package/src/tools/OpenapiViewer/components/PlaygroundStepper.tsx +98 -0
  78. package/src/tools/OpenapiViewer/components/RequestBuilder.tsx +164 -0
  79. package/src/tools/OpenapiViewer/components/RequestParametersForm.tsx +253 -0
  80. package/src/tools/OpenapiViewer/components/ResponseViewer.tsx +169 -0
  81. package/src/tools/OpenapiViewer/components/VersionSelector.tsx +64 -0
  82. package/src/tools/OpenapiViewer/components/index.ts +14 -0
  83. package/src/tools/OpenapiViewer/constants.ts +39 -0
  84. package/src/tools/OpenapiViewer/context/PlaygroundContext.tsx +338 -0
  85. package/src/tools/OpenapiViewer/hooks/index.ts +8 -0
  86. package/src/tools/OpenapiViewer/hooks/useMobile.ts +10 -0
  87. package/src/tools/OpenapiViewer/hooks/useOpenApiSchema.ts +203 -0
  88. package/src/tools/OpenapiViewer/index.tsx +36 -0
  89. package/src/tools/OpenapiViewer/types.ts +152 -0
  90. package/src/tools/OpenapiViewer/utils/apiKeyManager.ts +149 -0
  91. package/src/tools/OpenapiViewer/utils/formatters.ts +71 -0
  92. package/src/tools/OpenapiViewer/utils/index.ts +9 -0
  93. package/src/tools/OpenapiViewer/utils/versionManager.ts +161 -0
  94. package/src/tools/PrettyCode/PrettyCode.client.tsx +217 -0
  95. package/src/tools/PrettyCode/index.tsx +43 -0
  96. package/src/tools/VideoPlayer/README.md +239 -0
  97. package/src/tools/VideoPlayer/VideoControls.tsx +138 -0
  98. package/src/tools/VideoPlayer/VideoPlayer.tsx +230 -0
  99. package/src/tools/VideoPlayer/index.ts +9 -0
  100. package/src/tools/VideoPlayer/types.ts +62 -0
  101. 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
+ }