@goplusvn/core 0.1.20 → 0.1.22
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 +1 -1
- package/src/ui/data-display/data-table/data-table.tsx +1 -1
- package/src/ui/data-display/data-table-pagination.tsx +9 -9
- package/src/ui/data-display/url-pagination.tsx +178 -0
- package/src/ui/forms/date-picker.tsx +167 -19
- package/src/ui/forms/multi-select.tsx +475 -153
- package/src/ui/index.tsx +2 -0
package/package.json
CHANGED
|
@@ -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-
|
|
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
|
|
69
|
-
<div className="hidden sm:block flex-1 text-
|
|
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
|
|
74
|
-
<div className="flex items-center gap-1
|
|
75
|
-
<p className="hidden sm:block text-
|
|
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-[
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
+
}
|
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
|
+
import { addDays, format, isValid, parse } from "date-fns";
|
|
4
5
|
import { CalendarIcon } from "lucide-react";
|
|
5
6
|
|
|
6
7
|
import type { ComponentProps } from "react";
|
|
7
8
|
|
|
8
9
|
import { cn } from "../../utils";
|
|
9
10
|
|
|
10
|
-
import { Button } from "../primitives";
|
|
11
|
+
import { Button, Input } from "../primitives";
|
|
11
12
|
import {
|
|
12
13
|
Calendar,
|
|
13
14
|
Popover,
|
|
15
|
+
PopoverAnchor,
|
|
14
16
|
PopoverContent,
|
|
15
17
|
PopoverTrigger,
|
|
16
18
|
} from "../primitives/client";
|
|
@@ -24,11 +26,50 @@ type DatePickerProps = Omit<
|
|
|
24
26
|
formatStr?: string;
|
|
25
27
|
popoverContentClassName?: string;
|
|
26
28
|
popoverContentOptions?: ComponentProps<typeof PopoverContent>;
|
|
29
|
+
/** Class cho ô input (giữ tên cũ để tương thích các consumer hiện có) */
|
|
27
30
|
buttonClassName?: string;
|
|
28
31
|
buttonOptions?: ComponentProps<typeof Button>;
|
|
29
32
|
placeholder?: string;
|
|
30
33
|
};
|
|
31
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Các định dạng chấp nhận khi người dùng GÕ ngày (ngoài formatStr hiển thị).
|
|
37
|
+
* Dạng yy đặt TRƯỚC yyyy-fallback để "09/07/26" ra 2026 (không phải năm 0026);
|
|
38
|
+
* dạng thiếu năm/tháng ("09/07", "9") hiểu theo hôm nay.
|
|
39
|
+
*/
|
|
40
|
+
const TYPING_FORMATS = [
|
|
41
|
+
"dd/MM/yyyy",
|
|
42
|
+
"d/M/yyyy",
|
|
43
|
+
"dd/MM/yy",
|
|
44
|
+
"d/M/yy",
|
|
45
|
+
"dd-MM-yyyy",
|
|
46
|
+
"yyyy-MM-dd",
|
|
47
|
+
"dd/MM",
|
|
48
|
+
"d/M",
|
|
49
|
+
"d",
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
/** Tự chèn dấu phân cách theo formatStr khi người dùng gõ toàn số. */
|
|
53
|
+
function maskDigits(raw: string, formatStr: string): string {
|
|
54
|
+
const digits = raw.replace(/\D/g, "");
|
|
55
|
+
let out = "";
|
|
56
|
+
let di = 0;
|
|
57
|
+
for (let i = 0; i < formatStr.length && di < digits.length; i++) {
|
|
58
|
+
const ch = formatStr[i];
|
|
59
|
+
if (/[a-zA-Z]/.test(ch)) {
|
|
60
|
+
out += digits[di++];
|
|
61
|
+
} else {
|
|
62
|
+
out += ch;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return out;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* DatePicker cho phép GÕ ngày trực tiếp (tự chèn dấu phân cách, chấp nhận
|
|
70
|
+
* dd/MM/yyyy, d/M, yy..., ↑/↓ để +/- 1 ngày) hoặc bấm icon lịch để chọn.
|
|
71
|
+
* Gõ sai → viền đỏ, blur trả về giá trị đang có.
|
|
72
|
+
*/
|
|
32
73
|
export function DatePicker({
|
|
33
74
|
value,
|
|
34
75
|
onValueChange,
|
|
@@ -37,25 +78,104 @@ export function DatePicker({
|
|
|
37
78
|
popoverContentOptions,
|
|
38
79
|
buttonClassName,
|
|
39
80
|
buttonOptions,
|
|
40
|
-
placeholder = "
|
|
81
|
+
placeholder = "Chọn ngày",
|
|
41
82
|
...props
|
|
42
83
|
}: DatePickerProps) {
|
|
84
|
+
const [open, setOpen] = useState(false);
|
|
85
|
+
const [text, setText] = useState(value ? format(value, formatStr) : "");
|
|
86
|
+
|
|
87
|
+
// Đồng bộ khi giá trị đổi từ bên ngoài (chọn lịch, reset form...)
|
|
88
|
+
useEffect(() => {
|
|
89
|
+
setText(value ? format(value, formatStr) : "");
|
|
90
|
+
}, [value, formatStr]);
|
|
91
|
+
|
|
92
|
+
const parseTyped = (raw: string): Date | null => {
|
|
93
|
+
for (const fmt of [formatStr, ...TYPING_FORMATS]) {
|
|
94
|
+
const parsed = parse(raw, fmt, new Date());
|
|
95
|
+
// Chặn năm rác kiểu 0026 khi yyyy khớp nhầm chuỗi 2 chữ số
|
|
96
|
+
if (isValid(parsed) && parsed.getFullYear() >= 1000) return parsed;
|
|
97
|
+
}
|
|
98
|
+
return null;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const commitText = () => {
|
|
102
|
+
const raw = text.trim();
|
|
103
|
+
if (!raw) {
|
|
104
|
+
if (value) onValueChange?.(undefined);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
const parsed = parseTyped(raw);
|
|
108
|
+
if (parsed) {
|
|
109
|
+
onValueChange?.(parsed);
|
|
110
|
+
setText(format(parsed, formatStr));
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
// Không parse được → trả về giá trị đang có
|
|
114
|
+
setText(value ? format(value, formatStr) : "");
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
// Báo đỏ khi đã gõ đủ độ dài mà vẫn không ra ngày hợp lệ
|
|
118
|
+
const slotCount = formatStr.replace(/[^a-zA-Z]/g, "").length;
|
|
119
|
+
const isComplete = text.replace(/\D/g, "").length >= slotCount;
|
|
120
|
+
const invalid = isComplete && !parseTyped(text.trim());
|
|
121
|
+
|
|
122
|
+
const stepDay = (delta: number) => {
|
|
123
|
+
onValueChange?.(addDays(value ?? new Date(), delta));
|
|
124
|
+
};
|
|
125
|
+
|
|
43
126
|
return (
|
|
44
|
-
<Popover modal>
|
|
45
|
-
<
|
|
46
|
-
<
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
127
|
+
<Popover modal open={open} onOpenChange={setOpen}>
|
|
128
|
+
<PopoverAnchor asChild>
|
|
129
|
+
<div className="relative w-full">
|
|
130
|
+
<Input
|
|
131
|
+
value={text}
|
|
132
|
+
onChange={(e) => {
|
|
133
|
+
const v = e.target.value;
|
|
134
|
+
// Chỉ mask khi đang gõ thêm (không phá thao tác xoá/sửa)
|
|
135
|
+
if (v.length > text.length && /^[\d/\-.]*$/.test(v)) {
|
|
136
|
+
setText(maskDigits(v, formatStr));
|
|
137
|
+
} else {
|
|
138
|
+
setText(v);
|
|
139
|
+
}
|
|
140
|
+
}}
|
|
141
|
+
onBlur={commitText}
|
|
142
|
+
onKeyDown={(e) => {
|
|
143
|
+
if (e.key === "Enter") {
|
|
144
|
+
commitText();
|
|
145
|
+
(e.target as HTMLInputElement).blur();
|
|
146
|
+
} else if (e.key === "Escape") {
|
|
147
|
+
setOpen(false);
|
|
148
|
+
} else if (e.key === "ArrowUp") {
|
|
149
|
+
e.preventDefault();
|
|
150
|
+
stepDay(1);
|
|
151
|
+
} else if (e.key === "ArrowDown") {
|
|
152
|
+
e.preventDefault();
|
|
153
|
+
stepDay(-1);
|
|
154
|
+
}
|
|
155
|
+
}}
|
|
156
|
+
placeholder={placeholder}
|
|
157
|
+
inputMode="numeric"
|
|
158
|
+
aria-invalid={invalid || undefined}
|
|
159
|
+
className={cn(
|
|
160
|
+
"w-full pe-9",
|
|
161
|
+
invalid &&
|
|
162
|
+
"border-destructive focus-visible:ring-destructive/40",
|
|
163
|
+
buttonClassName,
|
|
164
|
+
)}
|
|
165
|
+
/>
|
|
166
|
+
<PopoverTrigger asChild>
|
|
167
|
+
<Button
|
|
168
|
+
type="button"
|
|
169
|
+
variant="ghost"
|
|
170
|
+
aria-label="Mở lịch"
|
|
171
|
+
className="absolute end-0 top-0 h-full w-9 p-0 text-muted-foreground hover:text-foreground"
|
|
172
|
+
{...buttonOptions}
|
|
173
|
+
>
|
|
174
|
+
<CalendarIcon className="h-4 w-4 shrink-0" />
|
|
175
|
+
</Button>
|
|
176
|
+
</PopoverTrigger>
|
|
177
|
+
</div>
|
|
178
|
+
</PopoverAnchor>
|
|
59
179
|
<PopoverContent
|
|
60
180
|
className={cn("w-auto p-0", popoverContentClassName)}
|
|
61
181
|
align="start"
|
|
@@ -64,9 +184,37 @@ export function DatePicker({
|
|
|
64
184
|
<Calendar
|
|
65
185
|
mode="single"
|
|
66
186
|
selected={value}
|
|
67
|
-
onSelect={
|
|
187
|
+
onSelect={(date) => {
|
|
188
|
+
onValueChange?.(date ?? undefined);
|
|
189
|
+
setOpen(false);
|
|
190
|
+
}}
|
|
191
|
+
defaultMonth={value}
|
|
68
192
|
{...props}
|
|
69
193
|
/>
|
|
194
|
+
<div className="flex items-center justify-between border-t p-2">
|
|
195
|
+
<Button
|
|
196
|
+
type="button"
|
|
197
|
+
variant="ghost"
|
|
198
|
+
className="h-7 px-2 text-xs"
|
|
199
|
+
onClick={() => {
|
|
200
|
+
onValueChange?.(new Date());
|
|
201
|
+
setOpen(false);
|
|
202
|
+
}}
|
|
203
|
+
>
|
|
204
|
+
Hôm nay
|
|
205
|
+
</Button>
|
|
206
|
+
<Button
|
|
207
|
+
type="button"
|
|
208
|
+
variant="ghost"
|
|
209
|
+
className="h-7 px-2 text-xs text-muted-foreground"
|
|
210
|
+
onClick={() => {
|
|
211
|
+
onValueChange?.(undefined);
|
|
212
|
+
setOpen(false);
|
|
213
|
+
}}
|
|
214
|
+
>
|
|
215
|
+
Xoá
|
|
216
|
+
</Button>
|
|
217
|
+
</div>
|
|
70
218
|
</PopoverContent>
|
|
71
219
|
</Popover>
|
|
72
220
|
);
|