@tuturuuu/ui 0.0.4
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/.checksum +1 -0
- package/README.md +46 -0
- package/components.json +20 -0
- package/eslint.config.mjs +20 -0
- package/jsr.json +10 -0
- package/package.json +120 -0
- package/postcss.config.mjs +8 -0
- package/rollup.config.js +40 -0
- package/src/components/ui/accordion.tsx +70 -0
- package/src/components/ui/alert-dialog.tsx +156 -0
- package/src/components/ui/alert.tsx +58 -0
- package/src/components/ui/aspect-ratio.tsx +11 -0
- package/src/components/ui/avatar.tsx +52 -0
- package/src/components/ui/badge.tsx +49 -0
- package/src/components/ui/breadcrumb.tsx +108 -0
- package/src/components/ui/button.tsx +61 -0
- package/src/components/ui/calendar.tsx +212 -0
- package/src/components/ui/card.tsx +74 -0
- package/src/components/ui/carousel.tsx +240 -0
- package/src/components/ui/chart.tsx +365 -0
- package/src/components/ui/checkbox.tsx +31 -0
- package/src/components/ui/codeblock.tsx +161 -0
- package/src/components/ui/collapsible.tsx +33 -0
- package/src/components/ui/color-picker.tsx +143 -0
- package/src/components/ui/command.tsx +176 -0
- package/src/components/ui/context-menu.tsx +251 -0
- package/src/components/ui/custom/autosize-textarea.tsx +111 -0
- package/src/components/ui/custom/calendar/core.tsx +61 -0
- package/src/components/ui/custom/calendar/day-cell.tsx +74 -0
- package/src/components/ui/custom/calendar/month-header.tsx +59 -0
- package/src/components/ui/custom/calendar/month-view.tsx +110 -0
- package/src/components/ui/custom/calendar/utils.ts +76 -0
- package/src/components/ui/custom/calendar/year-calendar.tsx +64 -0
- package/src/components/ui/custom/calendar/year-view.tsx +58 -0
- package/src/components/ui/custom/combobox.tsx +197 -0
- package/src/components/ui/custom/common-footer.tsx +215 -0
- package/src/components/ui/custom/compared-date-range-picker.tsx +561 -0
- package/src/components/ui/custom/date-input.tsx +279 -0
- package/src/components/ui/custom/empty-card.tsx +39 -0
- package/src/components/ui/custom/feature-summary.tsx +135 -0
- package/src/components/ui/custom/file-uploader.tsx +349 -0
- package/src/components/ui/custom/input-field.tsx +29 -0
- package/src/components/ui/custom/loading-indicator.tsx +28 -0
- package/src/components/ui/custom/modifiable-dialog-trigger.tsx +83 -0
- package/src/components/ui/custom/month-picker.tsx +157 -0
- package/src/components/ui/custom/report-preview.tsx +175 -0
- package/src/components/ui/custom/search-bar.tsx +56 -0
- package/src/components/ui/custom/select-field.tsx +78 -0
- package/src/components/ui/custom/tables/data-table-column-header.tsx +72 -0
- package/src/components/ui/custom/tables/data-table-create-button.tsx +31 -0
- package/src/components/ui/custom/tables/data-table-faceted-filter.tsx +142 -0
- package/src/components/ui/custom/tables/data-table-pagination.tsx +243 -0
- package/src/components/ui/custom/tables/data-table-refresh-button.tsx +45 -0
- package/src/components/ui/custom/tables/data-table-toolbar.tsx +133 -0
- package/src/components/ui/custom/tables/data-table-view-options.tsx +112 -0
- package/src/components/ui/custom/tables/data-table.tsx +228 -0
- package/src/components/ui/custom/uploaded-files-card.tsx +50 -0
- package/src/components/ui/dialog.tsx +137 -0
- package/src/components/ui/drawer.tsx +131 -0
- package/src/components/ui/dropdown-menu.tsx +256 -0
- package/src/components/ui/form.tsx +167 -0
- package/src/components/ui/hover-card.tsx +41 -0
- package/src/components/ui/icons.tsx +506 -0
- package/src/components/ui/input-otp.tsx +78 -0
- package/src/components/ui/input.tsx +18 -0
- package/src/components/ui/label.tsx +23 -0
- package/src/components/ui/markdown.tsx +7 -0
- package/src/components/ui/menubar.tsx +275 -0
- package/src/components/ui/navigation-menu.tsx +169 -0
- package/src/components/ui/pagination.tsx +126 -0
- package/src/components/ui/popover.tsx +47 -0
- package/src/components/ui/progress.tsx +30 -0
- package/src/components/ui/radio-group.tsx +44 -0
- package/src/components/ui/resizable.tsx +55 -0
- package/src/components/ui/scroll-area.tsx +57 -0
- package/src/components/ui/select.tsx +180 -0
- package/src/components/ui/separator.tsx +27 -0
- package/src/components/ui/sheet.tsx +138 -0
- package/src/components/ui/sidebar.tsx +734 -0
- package/src/components/ui/skeleton.tsx +13 -0
- package/src/components/ui/slider.tsx +62 -0
- package/src/components/ui/sonner.tsx +29 -0
- package/src/components/ui/switch.tsx +30 -0
- package/src/components/ui/table.tsx +112 -0
- package/src/components/ui/tabs.tsx +68 -0
- package/src/components/ui/tag-input.tsx +141 -0
- package/src/components/ui/textarea.tsx +17 -0
- package/src/components/ui/time-picker-input.tsx +117 -0
- package/src/components/ui/time-picker-utils.tsx +146 -0
- package/src/components/ui/toast.tsx +128 -0
- package/src/components/ui/toaster.tsx +35 -0
- package/src/components/ui/toggle-group.tsx +72 -0
- package/src/components/ui/toggle.tsx +46 -0
- package/src/components/ui/tooltip.tsx +60 -0
- package/src/globals.css +252 -0
- package/src/hooks/use-callback-ref.ts +28 -0
- package/src/hooks/use-controllable-state.ts +68 -0
- package/src/hooks/use-copy-to-clipboard.ts +46 -0
- package/src/hooks/use-form.ts +23 -0
- package/src/hooks/use-forwarded-ref.ts +17 -0
- package/src/hooks/use-mobile.tsx +21 -0
- package/src/hooks/use-toast.ts +191 -0
- package/src/resolvers.ts +3 -0
- package/tsconfig.json +17 -0
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { Separator } from '../separator';
|
|
2
|
+
import { ReactNode } from 'react';
|
|
3
|
+
|
|
4
|
+
export default function ReportPreview({
|
|
5
|
+
t,
|
|
6
|
+
lang,
|
|
7
|
+
data,
|
|
8
|
+
parseDynamicText,
|
|
9
|
+
getConfig,
|
|
10
|
+
}: {
|
|
11
|
+
lang: string;
|
|
12
|
+
data?: {
|
|
13
|
+
title: string;
|
|
14
|
+
content: string;
|
|
15
|
+
score: string;
|
|
16
|
+
feedback: string;
|
|
17
|
+
};
|
|
18
|
+
t: any;
|
|
19
|
+
// eslint-disable-next-line no-unused-vars
|
|
20
|
+
parseDynamicText: (text?: string | null) => ReactNode;
|
|
21
|
+
// eslint-disable-next-line no-unused-vars
|
|
22
|
+
getConfig: (id: string) => string | null | undefined;
|
|
23
|
+
}) {
|
|
24
|
+
return (
|
|
25
|
+
<div className="overflow-x-auto xl:flex-none">
|
|
26
|
+
<div
|
|
27
|
+
id="printable-area"
|
|
28
|
+
className="h-fit w-full flex-none rounded-xl dark:bg-foreground/10 print:p-4"
|
|
29
|
+
>
|
|
30
|
+
<div className="h-full rounded-lg border p-4 text-foreground md:p-12">
|
|
31
|
+
<div className="flex flex-wrap items-center justify-between gap-8">
|
|
32
|
+
{getConfig('BRAND_LOGO_URL') && (
|
|
33
|
+
<img
|
|
34
|
+
src={getConfig('BRAND_LOGO_URL')!}
|
|
35
|
+
alt="logo"
|
|
36
|
+
// onLoad={() => setIsLogoLoaded(true)}
|
|
37
|
+
/>
|
|
38
|
+
)}
|
|
39
|
+
|
|
40
|
+
<div className="text-center">
|
|
41
|
+
{getConfig('BRAND_NAME') && (
|
|
42
|
+
<div className="text-center text-lg font-bold">
|
|
43
|
+
{getConfig('BRAND_NAME')}
|
|
44
|
+
</div>
|
|
45
|
+
)}
|
|
46
|
+
|
|
47
|
+
{getConfig('BRAND_LOCATION') && (
|
|
48
|
+
<div className="text-center font-semibold">
|
|
49
|
+
{getConfig('BRAND_LOCATION')}
|
|
50
|
+
</div>
|
|
51
|
+
)}
|
|
52
|
+
|
|
53
|
+
{getConfig('BRAND_PHONE_NUMBER') && (
|
|
54
|
+
<div className="flex flex-wrap items-center justify-center gap-2 text-center text-sm font-semibold break-keep print:gap-2">
|
|
55
|
+
{getConfig('BRAND_PHONE_NUMBER')}
|
|
56
|
+
</div>
|
|
57
|
+
)}
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
|
|
61
|
+
{(!!getConfig('BRAND_NAME') ||
|
|
62
|
+
!!getConfig('BRAND_LOCATION') ||
|
|
63
|
+
!!getConfig('BRAND_PHONE_NUMBER')) && (
|
|
64
|
+
<Separator className="my-4" />
|
|
65
|
+
)}
|
|
66
|
+
|
|
67
|
+
<div className="text-center text-lg font-bold text-foreground uppercase">
|
|
68
|
+
{getConfig('REPORT_TITLE_PREFIX')}{' '}
|
|
69
|
+
{new Date().toLocaleDateString(lang, {
|
|
70
|
+
month: 'long',
|
|
71
|
+
})}
|
|
72
|
+
/
|
|
73
|
+
{new Date().toLocaleDateString(lang, {
|
|
74
|
+
year: 'numeric',
|
|
75
|
+
})}{' '}
|
|
76
|
+
{getConfig('REPORT_TITLE_SUFFIX')}
|
|
77
|
+
</div>
|
|
78
|
+
|
|
79
|
+
{getConfig('REPORT_INTRO') && (
|
|
80
|
+
<div className="mt-2 text-left text-sm whitespace-pre-wrap">
|
|
81
|
+
{parseDynamicText(getConfig('REPORT_INTRO'))}
|
|
82
|
+
</div>
|
|
83
|
+
)}
|
|
84
|
+
|
|
85
|
+
{(!!getConfig('REPORT_CONTENT_TEXT') ||
|
|
86
|
+
!!getConfig('REPORT_SCORE_TEXT') ||
|
|
87
|
+
!!getConfig('REPORT_FEEDBACK_TEXT')) && (
|
|
88
|
+
<div className="my-4 flex flex-col justify-stretch rounded border-2 border-foreground/50 text-sm md:flex-row">
|
|
89
|
+
{getConfig('REPORT_CONTENT_TEXT') && (
|
|
90
|
+
<div className="md:flex-[2]">
|
|
91
|
+
<div className="flex h-16 items-center justify-center p-2 text-center text-sm font-bold whitespace-pre-wrap">
|
|
92
|
+
{getConfig('REPORT_CONTENT_TEXT')}
|
|
93
|
+
</div>
|
|
94
|
+
<div
|
|
95
|
+
className={`min-h-[6rem] border-t-2 border-foreground/50 p-2 font-semibold break-words text-ellipsis whitespace-pre-line ${
|
|
96
|
+
!data?.content ? 'text-center underline' : 'text-left'
|
|
97
|
+
}`}
|
|
98
|
+
>
|
|
99
|
+
<span className={data?.content ? '' : 'opacity-50'}>
|
|
100
|
+
{data?.content || t('common.empty')}
|
|
101
|
+
</span>
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
)}
|
|
105
|
+
|
|
106
|
+
{getConfig('REPORT_CONTENT_TEXT') &&
|
|
107
|
+
getConfig('REPORT_SCORE_TEXT') && (
|
|
108
|
+
<div className="h-[2px] min-h-full w-auto shrink-0 bg-foreground/50 md:h-auto md:w-[2px]" />
|
|
109
|
+
)}
|
|
110
|
+
|
|
111
|
+
{getConfig('REPORT_SCORE_TEXT') && (
|
|
112
|
+
<div className="flex-[1] border-foreground/50">
|
|
113
|
+
<div className="flex h-16 flex-col items-center justify-center p-2 text-sm font-bold whitespace-pre-wrap">
|
|
114
|
+
{getConfig('REPORT_SCORE_TEXT')}
|
|
115
|
+
</div>
|
|
116
|
+
<div className="flex min-h-[6rem] justify-center border-t-2 border-foreground/50 p-2 text-center break-words text-ellipsis whitespace-pre-line">
|
|
117
|
+
<span
|
|
118
|
+
className={
|
|
119
|
+
data?.score
|
|
120
|
+
? 'text-2xl font-bold text-red-600 underline dark:text-red-300'
|
|
121
|
+
: 'font-semibold opacity-50'
|
|
122
|
+
}
|
|
123
|
+
>
|
|
124
|
+
{data?.score || '-'}
|
|
125
|
+
</span>
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
128
|
+
)}
|
|
129
|
+
|
|
130
|
+
{(getConfig('REPORT_SCORE_TEXT') ||
|
|
131
|
+
getConfig('REPORT_CONTENT_TEXT')) &&
|
|
132
|
+
getConfig('REPORT_FEEDBACK_TEXT') && (
|
|
133
|
+
<div className="h-[2px] min-h-full w-auto shrink-0 bg-foreground/50 md:h-auto md:w-[2px]" />
|
|
134
|
+
)}
|
|
135
|
+
|
|
136
|
+
{getConfig('REPORT_FEEDBACK_TEXT') && (
|
|
137
|
+
<div className="flex-[2]">
|
|
138
|
+
<div className="flex h-16 items-center justify-center p-2 text-sm font-bold whitespace-pre-wrap">
|
|
139
|
+
{getConfig('REPORT_FEEDBACK_TEXT')}
|
|
140
|
+
</div>
|
|
141
|
+
<div
|
|
142
|
+
className={`min-h-[6rem] border-t-2 border-foreground/50 p-2 font-semibold break-words text-ellipsis whitespace-pre-line ${
|
|
143
|
+
!data?.feedback ? 'text-center underline' : 'text-left'
|
|
144
|
+
}`}
|
|
145
|
+
>
|
|
146
|
+
<span className={data?.feedback ? '' : 'opacity-50'}>
|
|
147
|
+
{data?.feedback || t('common.empty')}
|
|
148
|
+
</span>
|
|
149
|
+
</div>
|
|
150
|
+
</div>
|
|
151
|
+
)}
|
|
152
|
+
</div>
|
|
153
|
+
)}
|
|
154
|
+
|
|
155
|
+
<div className="text-left text-sm">
|
|
156
|
+
{getConfig('REPORT_CONCLUSION')}
|
|
157
|
+
|
|
158
|
+
{getConfig('REPORT_CONCLUSION') && getConfig('REPORT_CLOSING') && (
|
|
159
|
+
<>
|
|
160
|
+
<br />
|
|
161
|
+
<br />
|
|
162
|
+
</>
|
|
163
|
+
)}
|
|
164
|
+
|
|
165
|
+
{getConfig('REPORT_CLOSING') && (
|
|
166
|
+
<span className="font-semibold">
|
|
167
|
+
{getConfig('REPORT_CLOSING')}
|
|
168
|
+
</span>
|
|
169
|
+
)}
|
|
170
|
+
</div>
|
|
171
|
+
</div>
|
|
172
|
+
</div>
|
|
173
|
+
</div>
|
|
174
|
+
);
|
|
175
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Input } from '../input';
|
|
4
|
+
import { cn } from '@tuturuuu/utils/format';
|
|
5
|
+
import { debounce } from 'lodash';
|
|
6
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
t: any;
|
|
10
|
+
defaultValue?: string;
|
|
11
|
+
className?: string;
|
|
12
|
+
// eslint-disable-next-line no-unused-vars
|
|
13
|
+
onSearch?: (query: string) => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Assuming the rest of your imports and Props interface are unchanged
|
|
17
|
+
|
|
18
|
+
const SearchBar = ({ t, defaultValue = '', className, onSearch }: Props) => {
|
|
19
|
+
// Memoize the updateQuery function to ensure debounce works correctly
|
|
20
|
+
const updateQuery = useCallback(
|
|
21
|
+
debounce((query: string) => {
|
|
22
|
+
if (onSearch) {
|
|
23
|
+
onSearch(query);
|
|
24
|
+
}
|
|
25
|
+
}, 300),
|
|
26
|
+
[onSearch] // Re-create the debounced function only if onSearch changes
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
const searchPlaceholder = t('search.search-placeholder');
|
|
30
|
+
const [value, setValue] = useState(defaultValue);
|
|
31
|
+
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
setValue(defaultValue);
|
|
34
|
+
}, [defaultValue]);
|
|
35
|
+
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
// Cleanup the debounced function on component unmount
|
|
38
|
+
return () => {
|
|
39
|
+
updateQuery.cancel();
|
|
40
|
+
};
|
|
41
|
+
}, [updateQuery]);
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<Input
|
|
45
|
+
placeholder={searchPlaceholder}
|
|
46
|
+
value={value}
|
|
47
|
+
onChange={(e) => {
|
|
48
|
+
setValue(e.target.value);
|
|
49
|
+
updateQuery(e.target.value);
|
|
50
|
+
}}
|
|
51
|
+
className={cn('h-8 min-w-64 placeholder:text-foreground/60', className)}
|
|
52
|
+
/>
|
|
53
|
+
);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export default SearchBar;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { Label } from '../label';
|
|
2
|
+
import {
|
|
3
|
+
Select,
|
|
4
|
+
SelectContent,
|
|
5
|
+
SelectItem,
|
|
6
|
+
SelectTrigger,
|
|
7
|
+
SelectValue,
|
|
8
|
+
} from '../select';
|
|
9
|
+
import { Root } from '@radix-ui/react-select';
|
|
10
|
+
import { cn } from '@tuturuuu/utils/format';
|
|
11
|
+
import React, { forwardRef } from 'react';
|
|
12
|
+
|
|
13
|
+
type SelectOption = {
|
|
14
|
+
value: string;
|
|
15
|
+
label: string;
|
|
16
|
+
disabled?: boolean;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
interface ClassNames {
|
|
20
|
+
root?: string;
|
|
21
|
+
label?: string;
|
|
22
|
+
selectTrigger?: string;
|
|
23
|
+
selectContent?: string;
|
|
24
|
+
selectItem?: string;
|
|
25
|
+
selectValue?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface SelectFieldProps {
|
|
29
|
+
id: string;
|
|
30
|
+
label?: string;
|
|
31
|
+
placeholder?: string;
|
|
32
|
+
options: SelectOption[];
|
|
33
|
+
classNames?: ClassNames;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface SelectProps
|
|
37
|
+
extends React.ComponentPropsWithoutRef<typeof Root> {}
|
|
38
|
+
|
|
39
|
+
// Merge the two interfaces
|
|
40
|
+
type Props = SelectFieldProps & SelectProps;
|
|
41
|
+
|
|
42
|
+
const SelectField = forwardRef<React.ComponentRef<typeof Root>, Props>(
|
|
43
|
+
({ id, label, placeholder, options, classNames, ...props }, ref) => {
|
|
44
|
+
return (
|
|
45
|
+
<div className={cn('grid gap-2', classNames?.root)}>
|
|
46
|
+
{label && (
|
|
47
|
+
<Label htmlFor={id} className={classNames?.label}>
|
|
48
|
+
{label}
|
|
49
|
+
</Label>
|
|
50
|
+
)}
|
|
51
|
+
<Select {...props}>
|
|
52
|
+
<SelectTrigger ref={ref} className={classNames?.selectTrigger}>
|
|
53
|
+
<SelectValue
|
|
54
|
+
id={id}
|
|
55
|
+
placeholder={placeholder}
|
|
56
|
+
className={classNames?.selectValue}
|
|
57
|
+
/>
|
|
58
|
+
</SelectTrigger>
|
|
59
|
+
<SelectContent className={classNames?.selectContent}>
|
|
60
|
+
{options.map((option) => (
|
|
61
|
+
<SelectItem
|
|
62
|
+
key={option.value}
|
|
63
|
+
value={option.value}
|
|
64
|
+
className={classNames?.selectItem}
|
|
65
|
+
disabled={option.disabled}
|
|
66
|
+
>
|
|
67
|
+
{option.label}
|
|
68
|
+
</SelectItem>
|
|
69
|
+
))}
|
|
70
|
+
</SelectContent>
|
|
71
|
+
</Select>
|
|
72
|
+
</div>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
SelectField.displayName = 'SelectField';
|
|
78
|
+
export { SelectField };
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { Button } from '../../button';
|
|
2
|
+
import {
|
|
3
|
+
DropdownMenu,
|
|
4
|
+
DropdownMenuContent,
|
|
5
|
+
DropdownMenuItem,
|
|
6
|
+
DropdownMenuSeparator,
|
|
7
|
+
DropdownMenuTrigger,
|
|
8
|
+
} from '../../dropdown-menu';
|
|
9
|
+
import { Column } from '@tanstack/react-table';
|
|
10
|
+
import { cn } from '@tuturuuu/utils/format';
|
|
11
|
+
import { ArrowDown, ArrowUp, ChevronDown, EyeOff } from 'lucide-react';
|
|
12
|
+
import React from 'react';
|
|
13
|
+
|
|
14
|
+
interface DataTableColumnHeaderProps<TData, TValue>
|
|
15
|
+
extends React.HTMLAttributes<HTMLDivElement> {
|
|
16
|
+
t: any;
|
|
17
|
+
column: Column<TData, TValue>;
|
|
18
|
+
title?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function DataTableColumnHeader<TData, TValue>({
|
|
22
|
+
t,
|
|
23
|
+
column,
|
|
24
|
+
title,
|
|
25
|
+
className,
|
|
26
|
+
}: DataTableColumnHeaderProps<TData, TValue>) {
|
|
27
|
+
if (!column.getCanSort()) {
|
|
28
|
+
return <div className={cn(className)}>{title}</div>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<div className={cn('flex items-center space-x-2', className)}>
|
|
33
|
+
<DropdownMenu modal={false}>
|
|
34
|
+
<DropdownMenuTrigger asChild>
|
|
35
|
+
<Button
|
|
36
|
+
size="sm"
|
|
37
|
+
variant="ghost"
|
|
38
|
+
className="-ml-3 h-8 data-[state=open]:bg-accent"
|
|
39
|
+
>
|
|
40
|
+
<span className="line-clamp-1">{title}</span>
|
|
41
|
+
{column.getIsSorted() === 'desc' ? (
|
|
42
|
+
<ArrowDown className="ml-2 h-4 w-4" />
|
|
43
|
+
) : column.getIsSorted() === 'asc' ? (
|
|
44
|
+
<ArrowUp className="ml-2 h-4 w-4" />
|
|
45
|
+
) : (
|
|
46
|
+
<ChevronDown className="ml-2 h-4 w-4" />
|
|
47
|
+
)}
|
|
48
|
+
</Button>
|
|
49
|
+
</DropdownMenuTrigger>
|
|
50
|
+
<DropdownMenuContent align="start">
|
|
51
|
+
<DropdownMenuItem className="justify-center" disabled>
|
|
52
|
+
<span className="line-clamp-1">{title}</span>
|
|
53
|
+
</DropdownMenuItem>
|
|
54
|
+
<DropdownMenuSeparator />
|
|
55
|
+
<DropdownMenuItem onClick={() => column.toggleSorting(false)}>
|
|
56
|
+
<ArrowUp className="mr-2 h-3.5 w-3.5 text-muted-foreground/70" />
|
|
57
|
+
{t('common.ascending')}
|
|
58
|
+
</DropdownMenuItem>
|
|
59
|
+
<DropdownMenuItem onClick={() => column.toggleSorting(true)}>
|
|
60
|
+
<ArrowDown className="mr-2 h-3.5 w-3.5 text-muted-foreground/70" />
|
|
61
|
+
{t('common.descending')}
|
|
62
|
+
</DropdownMenuItem>
|
|
63
|
+
<DropdownMenuSeparator />
|
|
64
|
+
<DropdownMenuItem onClick={() => column.toggleVisibility(false)}>
|
|
65
|
+
<EyeOff className="mr-2 h-3.5 w-3.5 text-muted-foreground/70" />
|
|
66
|
+
{t('common.hide_column')}
|
|
67
|
+
</DropdownMenuItem>
|
|
68
|
+
</DropdownMenuContent>
|
|
69
|
+
</DropdownMenu>
|
|
70
|
+
</div>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Button } from '../../button';
|
|
4
|
+
import { DialogContent, DialogTrigger } from '../../dialog';
|
|
5
|
+
import { Plus } from 'lucide-react';
|
|
6
|
+
import { ReactNode } from 'react';
|
|
7
|
+
|
|
8
|
+
export interface DataTableCreateButtonProps {
|
|
9
|
+
newObjectTitle?: string;
|
|
10
|
+
createButtonText?: string;
|
|
11
|
+
editContent?: ReactNode;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function DataTableCreateButton(props: DataTableCreateButtonProps) {
|
|
15
|
+
return (
|
|
16
|
+
<>
|
|
17
|
+
<DialogTrigger asChild>
|
|
18
|
+
<Button size="sm" className="col-span-full ml-auto h-8 w-full md:w-fit">
|
|
19
|
+
<Plus className="h-4 w-4" />
|
|
20
|
+
{props.newObjectTitle || props.createButtonText}
|
|
21
|
+
</Button>
|
|
22
|
+
</DialogTrigger>
|
|
23
|
+
<DialogContent
|
|
24
|
+
className="sm:max-w-[425px]"
|
|
25
|
+
onOpenAutoFocus={(e) => e.preventDefault()}
|
|
26
|
+
>
|
|
27
|
+
{props.editContent}
|
|
28
|
+
</DialogContent>
|
|
29
|
+
</>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { Badge } from '../../badge';
|
|
2
|
+
import { Button } from '../../button';
|
|
3
|
+
import {
|
|
4
|
+
Command,
|
|
5
|
+
CommandEmpty,
|
|
6
|
+
CommandGroup,
|
|
7
|
+
CommandInput,
|
|
8
|
+
CommandItem,
|
|
9
|
+
CommandList,
|
|
10
|
+
CommandSeparator,
|
|
11
|
+
} from '../../command';
|
|
12
|
+
import { Popover, PopoverContent, PopoverTrigger } from '../../popover';
|
|
13
|
+
import { Separator } from '../../separator';
|
|
14
|
+
import { Column } from '@tanstack/react-table';
|
|
15
|
+
import { cn } from '@tuturuuu/utils/format';
|
|
16
|
+
import { Check, PlusCircle } from 'lucide-react';
|
|
17
|
+
import * as React from 'react';
|
|
18
|
+
|
|
19
|
+
interface DataTableFacetedFilterProps<TData, TValue> {
|
|
20
|
+
column?: Column<TData, TValue>;
|
|
21
|
+
title?: string;
|
|
22
|
+
options: {
|
|
23
|
+
label: string;
|
|
24
|
+
value: string;
|
|
25
|
+
icon?: React.ComponentType<{ className?: string }>;
|
|
26
|
+
}[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function DataTableFacetedFilter<TData, TValue>({
|
|
30
|
+
column,
|
|
31
|
+
title,
|
|
32
|
+
options,
|
|
33
|
+
}: DataTableFacetedFilterProps<TData, TValue>) {
|
|
34
|
+
const facets = column?.getFacetedUniqueValues();
|
|
35
|
+
const selectedValues = new Set(column?.getFilterValue() as string[]);
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<Popover>
|
|
39
|
+
<PopoverTrigger asChild>
|
|
40
|
+
<Button variant="outline" size="sm" className="h-8 border-dashed">
|
|
41
|
+
<PlusCircle className="mr-2 h-4 w-4" />
|
|
42
|
+
{title}
|
|
43
|
+
{selectedValues?.size > 0 && (
|
|
44
|
+
<>
|
|
45
|
+
<Separator orientation="vertical" className="mx-2 h-4" />
|
|
46
|
+
<Badge
|
|
47
|
+
variant="secondary"
|
|
48
|
+
className="rounded-sm px-1 font-normal lg:hidden"
|
|
49
|
+
>
|
|
50
|
+
{selectedValues.size}
|
|
51
|
+
</Badge>
|
|
52
|
+
<div className="hidden space-x-1 lg:flex">
|
|
53
|
+
{selectedValues.size > 2 ? (
|
|
54
|
+
<Badge
|
|
55
|
+
variant="secondary"
|
|
56
|
+
className="rounded-sm px-1 font-normal"
|
|
57
|
+
>
|
|
58
|
+
{selectedValues.size} selected
|
|
59
|
+
</Badge>
|
|
60
|
+
) : (
|
|
61
|
+
options
|
|
62
|
+
.filter((option) => selectedValues.has(option.value))
|
|
63
|
+
.map((option) => (
|
|
64
|
+
<Badge
|
|
65
|
+
variant="secondary"
|
|
66
|
+
key={option.value}
|
|
67
|
+
className="rounded-sm px-1 font-normal"
|
|
68
|
+
>
|
|
69
|
+
{option.label}
|
|
70
|
+
</Badge>
|
|
71
|
+
))
|
|
72
|
+
)}
|
|
73
|
+
</div>
|
|
74
|
+
</>
|
|
75
|
+
)}
|
|
76
|
+
</Button>
|
|
77
|
+
</PopoverTrigger>
|
|
78
|
+
<PopoverContent className="w-[200px] p-0" align="start">
|
|
79
|
+
<Command>
|
|
80
|
+
<CommandInput placeholder={title} />
|
|
81
|
+
<CommandList>
|
|
82
|
+
<CommandEmpty>No results found.</CommandEmpty>
|
|
83
|
+
<CommandGroup>
|
|
84
|
+
{options.map((option) => {
|
|
85
|
+
const isSelected = selectedValues.has(option.value);
|
|
86
|
+
return (
|
|
87
|
+
<CommandItem
|
|
88
|
+
key={option.value}
|
|
89
|
+
onSelect={() => {
|
|
90
|
+
if (isSelected) {
|
|
91
|
+
selectedValues.delete(option.value);
|
|
92
|
+
} else {
|
|
93
|
+
selectedValues.add(option.value);
|
|
94
|
+
}
|
|
95
|
+
const filterValues = Array.from(selectedValues);
|
|
96
|
+
column?.setFilterValue(
|
|
97
|
+
filterValues.length ? filterValues : undefined
|
|
98
|
+
);
|
|
99
|
+
}}
|
|
100
|
+
>
|
|
101
|
+
<div
|
|
102
|
+
className={cn(
|
|
103
|
+
'mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary',
|
|
104
|
+
isSelected
|
|
105
|
+
? 'bg-primary text-primary-foreground'
|
|
106
|
+
: 'opacity-50 [&_svg]:invisible'
|
|
107
|
+
)}
|
|
108
|
+
>
|
|
109
|
+
<Check className={cn('h-4 w-4')} />
|
|
110
|
+
</div>
|
|
111
|
+
{option.icon && (
|
|
112
|
+
<option.icon className="mr-2 h-4 w-4 text-muted-foreground" />
|
|
113
|
+
)}
|
|
114
|
+
<span>{option.label}</span>
|
|
115
|
+
{facets?.get(option.value) && (
|
|
116
|
+
<span className="ml-auto flex h-4 w-4 items-center justify-center font-mono text-xs">
|
|
117
|
+
{facets.get(option.value)}
|
|
118
|
+
</span>
|
|
119
|
+
)}
|
|
120
|
+
</CommandItem>
|
|
121
|
+
);
|
|
122
|
+
})}
|
|
123
|
+
</CommandGroup>
|
|
124
|
+
{selectedValues.size > 0 && (
|
|
125
|
+
<>
|
|
126
|
+
<CommandSeparator />
|
|
127
|
+
<CommandGroup>
|
|
128
|
+
<CommandItem
|
|
129
|
+
onSelect={() => column?.setFilterValue(undefined)}
|
|
130
|
+
className="justify-center text-center"
|
|
131
|
+
>
|
|
132
|
+
Clear filters
|
|
133
|
+
</CommandItem>
|
|
134
|
+
</CommandGroup>
|
|
135
|
+
</>
|
|
136
|
+
)}
|
|
137
|
+
</CommandList>
|
|
138
|
+
</Command>
|
|
139
|
+
</PopoverContent>
|
|
140
|
+
</Popover>
|
|
141
|
+
);
|
|
142
|
+
}
|