@goplusvn/core 0.1.20 → 0.1.21

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@goplusvn/core",
3
3
  "description": "GoPlusVN Platform Kit - ERP kernel: layout, RBAC, CRUD, multi-tenant, system pages",
4
- "version": "0.1.20",
4
+ "version": "0.1.21",
5
5
  "private": false,
6
6
  "publishConfig": {
7
7
  "registry": "https://registry.npmjs.org",
@@ -480,7 +480,7 @@ export function DataTable<TData extends Record<string, unknown>>({
480
480
 
481
481
  {/* Pagination Section */}
482
482
  {pagination && (
483
- <div className="border-t border-border dark:border-slate-800 bg-card px-4 sm:px-6 py-2 shrink-0">
483
+ <div className="border-t border-border dark:border-slate-800 bg-card px-2 sm:px-4 py-1 shrink-0">
484
484
  <MemoizedPagination
485
485
  table={table}
486
486
  currentPage={pagination.page}
@@ -65,21 +65,21 @@ export function DataTablePagination<TData>({
65
65
  };
66
66
 
67
67
  return (
68
- <div className="flex flex-col sm:flex-row items-center justify-between gap-1 sm:gap-2 py-0.5 px-1 sm:px-2">
69
- <div className="hidden sm:block flex-1 text-xs text-left text-muted-foreground w-full">
68
+ <div className="flex flex-col sm:flex-row items-center justify-between gap-1 px-1 sm:px-2">
69
+ <div className="hidden sm:block flex-1 text-[11px] text-left text-muted-foreground w-full">
70
70
  Hiển thị {startItem} - {endItem} trong tổng số {total} mục
71
71
  </div>
72
72
 
73
- <div className="flex items-center justify-center gap-2 w-full sm:w-auto overflow-x-auto pb-1 sm:pb-0">
74
- <div className="flex items-center gap-1 sm:gap-2 px-1 sm:px-2 border-r pr-1 sm:pr-2">
75
- <p className="hidden sm:block text-xs font-medium whitespace-nowrap">
73
+ <div className="flex items-center justify-center gap-2 w-full sm:w-auto overflow-x-auto">
74
+ <div className="flex items-center gap-1 gap-1 px-1 border-r pr-1 sm:pr-1.5">
75
+ <p className="hidden sm:block text-[11px] font-medium whitespace-nowrap">
76
76
  Số dòng
77
77
  </p>
78
78
  <Select
79
79
  value={`${effPageSize}`}
80
80
  onValueChange={(value) => table.setPageSize(Number(value))}
81
81
  >
82
- <SelectTrigger className="h-6 w-[65px] sm:w-[75px] text-xs focus:ring-inset focus:ring-offset-0">
82
+ <SelectTrigger className="!h-6 !min-h-0 py-0 w-[60px] sm:w-[65px] text-[11px] px-1.5 focus:ring-inset focus:ring-offset-0">
83
83
  <SelectValue placeholder={effPageSize} />
84
84
  </SelectTrigger>
85
85
  <SelectContent side="top">
@@ -113,11 +113,11 @@ export function DataTablePagination<TData>({
113
113
  </Button>
114
114
 
115
115
  <div className="flex items-center gap-1 sm:gap-2 px-1">
116
- <span className="hidden sm:inline text-xs font-medium whitespace-nowrap">
116
+ <span className="hidden sm:inline text-[11px] font-medium whitespace-nowrap">
117
117
  Trang
118
118
  </span>
119
119
  <input
120
- className="h-6 w-10 sm:w-12 rounded-md border border-input bg-background px-1 text-xs text-center focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
120
+ className="h-6 w-10 sm:w-12 rounded-md border border-input bg-background px-1 text-[11px] text-center focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
121
121
  value={inputPage}
122
122
  onChange={(e) => setInputPage(e.target.value)}
123
123
  onBlur={handlePageInputBlur}
@@ -126,7 +126,7 @@ export function DataTablePagination<TData>({
126
126
  }}
127
127
  disabled={total === 0}
128
128
  />
129
- <span className="text-sm font-medium text-muted-foreground whitespace-nowrap">
129
+ <span className="text-[11px] font-medium text-muted-foreground whitespace-nowrap">
130
130
  / {pageCount}
131
131
  </span>
132
132
  </div>
@@ -0,0 +1,178 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { usePathname, useRouter, useSearchParams } from "next/navigation"
5
+ import {
6
+ Button,
7
+ Select,
8
+ SelectContent,
9
+ SelectItem,
10
+ SelectTrigger,
11
+ SelectValue,
12
+ } from "../primitives"
13
+ import {
14
+ ChevronLeft,
15
+ ChevronRight,
16
+ ChevronsLeft,
17
+ ChevronsRight,
18
+ } from "lucide-react"
19
+
20
+ interface PaginationProps {
21
+ totalItems: number
22
+ currentPage: number
23
+ pageSize: number
24
+ disabled?: boolean
25
+ }
26
+
27
+ export function UrlPagination({
28
+ totalItems,
29
+ currentPage,
30
+ pageSize,
31
+ disabled,
32
+ }: PaginationProps) {
33
+ const router = useRouter()
34
+ const pathname = usePathname()
35
+ const searchParams = useSearchParams()
36
+
37
+ const pageCount = Math.ceil(totalItems / pageSize)
38
+ const startItem = (currentPage - 1) * pageSize + 1
39
+ const endItem = Math.min(currentPage * pageSize, totalItems)
40
+
41
+ // Create a new URLSearchParams instance to update params
42
+ const createQueryString = React.useCallback(
43
+ (params: Record<string, string | number | null>) => {
44
+ const newSearchParams = new URLSearchParams(searchParams?.toString())
45
+
46
+ for (const [key, value] of Object.entries(params)) {
47
+ if (value === null) {
48
+ newSearchParams.delete(key)
49
+ } else {
50
+ newSearchParams.set(key, String(value))
51
+ }
52
+ }
53
+
54
+ return newSearchParams.toString()
55
+ },
56
+ [searchParams]
57
+ )
58
+
59
+ const handlePageChange = (page: number) => {
60
+ if (page < 1 || page > Math.max(1, pageCount)) return
61
+ router.push(`${pathname}?${createQueryString({ page })}`)
62
+ }
63
+
64
+ const handlePageSizeChange = (value: string) => {
65
+ const newPageSize = Number(value)
66
+ // When changing page size, reset to page 1 to avoid out of bounds
67
+ router.push(
68
+ `${pathname}?${createQueryString({ pageSize: newPageSize, page: 1 })}`
69
+ )
70
+ }
71
+
72
+ const [inputPage, setInputPage] = React.useState(String(currentPage))
73
+
74
+ React.useEffect(() => {
75
+ setInputPage(String(currentPage))
76
+ }, [currentPage])
77
+
78
+ const handlePageInputBlur = () => {
79
+ const pageNum = Number(inputPage)
80
+ if (isNaN(pageNum) || pageNum < 1 || pageNum > pageCount) {
81
+ setInputPage(String(currentPage))
82
+ } else if (pageNum !== currentPage) {
83
+ handlePageChange(pageNum)
84
+ }
85
+ }
86
+
87
+ const handlePageInputKeyDown = (e: React.KeyboardEvent) => {
88
+ if (e.key === "Enter") {
89
+ handlePageInputBlur()
90
+ }
91
+ }
92
+
93
+ return (
94
+ <div className="flex flex-col sm:flex-row items-center justify-between gap-1 sm:gap-2 px-1 sm:px-2">
95
+ {/* Ẩn text "Hiển thị" trên mobile, chỉ hiện trên màn hình sm trở lên */}
96
+ <div className="hidden sm:block flex-1 text-[11px] text-left text-muted-foreground w-full">
97
+ Hiển thị {totalItems > 0 ? startItem : 0} -{" "}
98
+ {totalItems > 0 ? endItem : 0} trong tổng số {totalItems} mục
99
+ </div>
100
+
101
+ <div className="flex items-center justify-center gap-2 w-full sm:w-auto overflow-x-auto">
102
+ <div className="flex items-center gap-1 gap-1 px-1 border-r pr-1 sm:pr-1.5">
103
+ <p className="hidden sm:block text-[11px] font-medium whitespace-nowrap">Số dòng</p>
104
+ <Select
105
+ value={`${pageSize}`}
106
+ onValueChange={handlePageSizeChange}
107
+ disabled={disabled}
108
+ >
109
+ <SelectTrigger className="!h-6 !min-h-0 py-0 w-[60px] sm:w-[65px] text-[11px] px-1.5 focus:ring-inset focus:ring-offset-0">
110
+ <SelectValue placeholder={pageSize} />
111
+ </SelectTrigger>
112
+ <SelectContent side="top">
113
+ {[10, 20, 50, 100, 500, 1000].map((size) => (
114
+ <SelectItem key={size} value={`${size}`}>
115
+ {size}
116
+ </SelectItem>
117
+ ))}
118
+ </SelectContent>
119
+ </Select>
120
+ </div>
121
+ <div className="flex items-center gap-1 pl-1">
122
+ <Button
123
+ variant="outline"
124
+ className="hidden h-6 w-6 p-0 lg:flex"
125
+ onClick={() => handlePageChange(1)}
126
+ disabled={currentPage <= 1 || disabled || totalItems === 0}
127
+ >
128
+ <span className="sr-only">Trang đầu</span>
129
+ <ChevronsLeft className="h-3 w-3 sm:h-3 sm:w-3" />
130
+ </Button>
131
+ <Button
132
+ variant="outline"
133
+ className="h-6 w-6 p-0"
134
+ onClick={() => handlePageChange(currentPage - 1)}
135
+ disabled={currentPage <= 1 || disabled || totalItems === 0}
136
+ >
137
+ <span className="sr-only">Trang trước</span>
138
+ <ChevronLeft className="h-3 w-3 sm:h-3 sm:w-3" />
139
+ </Button>
140
+
141
+ <div className="flex items-center gap-1 sm:gap-2 px-1">
142
+ <span className="hidden sm:inline text-[11px] font-medium whitespace-nowrap">Trang</span>
143
+ <input
144
+ className="h-6 w-10 sm:w-12 rounded-md border border-input bg-background px-1 text-[11px] text-center focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
145
+ value={inputPage}
146
+ onChange={(e) => setInputPage(e.target.value)}
147
+ onBlur={handlePageInputBlur}
148
+ onKeyDown={handlePageInputKeyDown}
149
+ disabled={disabled || totalItems === 0}
150
+ />
151
+ <span className="text-sm font-medium text-muted-foreground whitespace-nowrap">
152
+ / {Math.max(1, pageCount)}
153
+ </span>
154
+ </div>
155
+
156
+ <Button
157
+ variant="outline"
158
+ className="h-6 w-6 p-0"
159
+ onClick={() => handlePageChange(currentPage + 1)}
160
+ disabled={currentPage >= pageCount || disabled || totalItems === 0}
161
+ >
162
+ <span className="sr-only">Trang sau</span>
163
+ <ChevronRight className="h-3 w-3 sm:h-3 sm:w-3" />
164
+ </Button>
165
+ <Button
166
+ variant="outline"
167
+ className="hidden h-6 w-6 p-0 lg:flex"
168
+ onClick={() => handlePageChange(pageCount)}
169
+ disabled={currentPage >= pageCount || disabled || totalItems === 0}
170
+ >
171
+ <span className="sr-only">Trang cuối</span>
172
+ <ChevronsRight className="h-3 w-3 sm:h-3 sm:w-3" />
173
+ </Button>
174
+ </div>
175
+ </div>
176
+ </div>
177
+ )
178
+ }
package/src/ui/index.tsx CHANGED
@@ -16,3 +16,5 @@ export * from "./auth";
16
16
  export * from "./management";
17
17
  export * from "./pages/not-found";
18
18
  export * from "./shared";
19
+
20
+ export { UrlPagination } from "./data-display/url-pagination";