doo-boilerplate 0.1.16 → 0.2.2
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/dist/index.js +43 -486
- package/package.json +1 -1
- package/templates/template-vite/_env.example +8 -0
- package/templates/template-vite/package.json +5 -2
- package/templates/template-vite/src/components/data-table/data-table-column-header.tsx +62 -0
- package/templates/template-vite/src/components/data-table/data-table-faceted-filter.tsx +129 -0
- package/templates/template-vite/src/components/data-table/data-table-pagination.tsx +80 -0
- package/templates/template-vite/src/components/data-table/data-table-toolbar.tsx +66 -0
- package/templates/template-vite/src/components/data-table/data-table-view-options.tsx +46 -0
- package/templates/template-vite/src/components/data-table/data-table.tsx +63 -0
- package/templates/template-vite/src/components/layout/sidebar.tsx +2 -1
- package/templates/template-vite/src/components/ui/alert-dialog.tsx +106 -0
- package/templates/template-vite/src/components/ui/command.tsx +118 -0
- package/templates/template-vite/src/components/ui/popover.tsx +28 -0
- package/templates/template-vite/src/components/ui/table.tsx +77 -0
- package/templates/template-vite/src/features/dashboard/components/overview-chart.tsx +61 -0
- package/templates/template-vite/src/features/dashboard/components/recent-activity.tsx +37 -0
- package/templates/template-vite/src/features/dashboard/components/stats-cards.tsx +37 -0
- package/templates/template-vite/src/features/users/components/user-delete-confirmation-dialog.tsx +48 -0
- package/templates/template-vite/src/features/users/components/user-form-dialog.tsx +143 -0
- package/templates/template-vite/src/features/users/components/users-table-columns.tsx +154 -0
- package/templates/template-vite/src/features/users/components/users-table.tsx +143 -0
- package/templates/template-vite/src/features/users/data/users-mock-data.ts +55 -0
- package/templates/template-vite/src/features/users/schemas/user-form-schema.ts +14 -0
- package/templates/template-vite/src/features/users/types/user.ts +12 -0
- package/templates/template-vite/src/lib/sentry.ts +28 -0
- package/templates/template-vite/src/main.tsx +3 -0
- package/templates/template-vite/src/routes/_authenticated/dashboard.tsx +12 -6
- package/templates/template-vite/src/routes/_authenticated/users.tsx +16 -0
- package/templates/template-vite/vite.config.ts +8 -0
- package/templates/template-vite/optional/charts/deps.json +0 -7
- package/templates/template-vite/optional/dark-mode/deps.json +0 -5
- package/templates/template-vite/optional/dnd/deps.json +0 -8
- package/templates/template-vite/optional/editor/deps.json +0 -10
- package/templates/template-vite/optional/i18n/deps.json +0 -7
- package/templates/template-vite/optional/sentry/deps.json +0 -6
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
"docker:scan": "bash scripts/trivy-scan.sh"
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|
|
22
|
+
"@sentry/react": "^9.0.0",
|
|
22
23
|
"react": "^19.2.0",
|
|
23
24
|
"react-dom": "^19.2.0",
|
|
24
25
|
"i18next": "^25.2.1",
|
|
@@ -56,7 +57,8 @@
|
|
|
56
57
|
"@radix-ui/react-switch": "^1.1.3",
|
|
57
58
|
"@radix-ui/react-tabs": "^1.1.3",
|
|
58
59
|
"@radix-ui/react-tooltip": "^1.1.8",
|
|
59
|
-
"cmdk": "^1.1.1"
|
|
60
|
+
"cmdk": "^1.1.1",
|
|
61
|
+
"recharts": "^2.15.0"
|
|
60
62
|
},
|
|
61
63
|
"devDependencies": {
|
|
62
64
|
"@types/react": "^19",
|
|
@@ -83,7 +85,8 @@
|
|
|
83
85
|
"husky": "^9.1.7",
|
|
84
86
|
"lint-staged": "^15.2.11",
|
|
85
87
|
"knip": "^5.64.2",
|
|
86
|
-
"swagger-typescript-api": "^13.2.7"
|
|
88
|
+
"swagger-typescript-api": "^13.2.7",
|
|
89
|
+
"@sentry/vite-plugin": "^3.0.0"
|
|
87
90
|
},
|
|
88
91
|
"lint-staged": {
|
|
89
92
|
"*.{js,ts,tsx,css}": ["eslint --fix", "prettier --write"]
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
import { type Column } from '@tanstack/react-table'
|
|
3
|
+
import { ArrowDown, ArrowUp, ChevronsUpDown, EyeOff } from 'lucide-react'
|
|
4
|
+
|
|
5
|
+
import { cn } from '@/lib/utils'
|
|
6
|
+
import { Button } from '@/components/ui/button'
|
|
7
|
+
import {
|
|
8
|
+
DropdownMenu,
|
|
9
|
+
DropdownMenuContent,
|
|
10
|
+
DropdownMenuItem,
|
|
11
|
+
DropdownMenuSeparator,
|
|
12
|
+
DropdownMenuTrigger,
|
|
13
|
+
} from '@/components/ui/dropdown-menu'
|
|
14
|
+
|
|
15
|
+
interface DataTableColumnHeaderProps<TData, TValue> extends React.HTMLAttributes<HTMLDivElement> {
|
|
16
|
+
column: Column<TData, TValue>
|
|
17
|
+
title: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function DataTableColumnHeader<TData, TValue>({
|
|
21
|
+
column,
|
|
22
|
+
title,
|
|
23
|
+
className,
|
|
24
|
+
}: DataTableColumnHeaderProps<TData, TValue>) {
|
|
25
|
+
if (!column.getCanSort()) {
|
|
26
|
+
return <div className={cn(className)}>{title}</div>
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<div className={cn('flex items-center space-x-2', className)}>
|
|
31
|
+
<DropdownMenu>
|
|
32
|
+
<DropdownMenuTrigger asChild>
|
|
33
|
+
<Button variant='ghost' size='sm' className='-ml-3 h-8 data-[state=open]:bg-accent'>
|
|
34
|
+
<span>{title}</span>
|
|
35
|
+
{column.getIsSorted() === 'desc' ? (
|
|
36
|
+
<ArrowDown className='ml-2 h-4 w-4' />
|
|
37
|
+
) : column.getIsSorted() === 'asc' ? (
|
|
38
|
+
<ArrowUp className='ml-2 h-4 w-4' />
|
|
39
|
+
) : (
|
|
40
|
+
<ChevronsUpDown className='ml-2 h-4 w-4' />
|
|
41
|
+
)}
|
|
42
|
+
</Button>
|
|
43
|
+
</DropdownMenuTrigger>
|
|
44
|
+
<DropdownMenuContent align='start'>
|
|
45
|
+
<DropdownMenuItem onClick={() => column.toggleSorting(false)}>
|
|
46
|
+
<ArrowUp className='mr-2 h-3.5 w-3.5 text-muted-foreground/70' />
|
|
47
|
+
Asc
|
|
48
|
+
</DropdownMenuItem>
|
|
49
|
+
<DropdownMenuItem onClick={() => column.toggleSorting(true)}>
|
|
50
|
+
<ArrowDown className='mr-2 h-3.5 w-3.5 text-muted-foreground/70' />
|
|
51
|
+
Desc
|
|
52
|
+
</DropdownMenuItem>
|
|
53
|
+
<DropdownMenuSeparator />
|
|
54
|
+
<DropdownMenuItem onClick={() => column.toggleVisibility(false)}>
|
|
55
|
+
<EyeOff className='mr-2 h-3.5 w-3.5 text-muted-foreground/70' />
|
|
56
|
+
Hide
|
|
57
|
+
</DropdownMenuItem>
|
|
58
|
+
</DropdownMenuContent>
|
|
59
|
+
</DropdownMenu>
|
|
60
|
+
</div>
|
|
61
|
+
)
|
|
62
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
import { type Column } from '@tanstack/react-table'
|
|
3
|
+
import { Check, PlusCircle } from 'lucide-react'
|
|
4
|
+
|
|
5
|
+
import { cn } from '@/lib/utils'
|
|
6
|
+
import { Badge } from '@/components/ui/badge'
|
|
7
|
+
import { Button } from '@/components/ui/button'
|
|
8
|
+
import {
|
|
9
|
+
Command,
|
|
10
|
+
CommandEmpty,
|
|
11
|
+
CommandGroup,
|
|
12
|
+
CommandInput,
|
|
13
|
+
CommandItem,
|
|
14
|
+
CommandList,
|
|
15
|
+
CommandSeparator,
|
|
16
|
+
} from '@/components/ui/command'
|
|
17
|
+
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
|
|
18
|
+
import { Separator } from '@/components/ui/separator'
|
|
19
|
+
|
|
20
|
+
interface FacetedFilterOption {
|
|
21
|
+
label: string
|
|
22
|
+
value: string
|
|
23
|
+
icon?: React.ComponentType<{ className?: string }>
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface DataTableFacetedFilterProps<TData, TValue> {
|
|
27
|
+
column?: Column<TData, TValue>
|
|
28
|
+
title?: string
|
|
29
|
+
options: FacetedFilterOption[]
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function DataTableFacetedFilter<TData, TValue>({
|
|
33
|
+
column,
|
|
34
|
+
title,
|
|
35
|
+
options,
|
|
36
|
+
}: DataTableFacetedFilterProps<TData, TValue>) {
|
|
37
|
+
const facets = column?.getFacetedUniqueValues()
|
|
38
|
+
const selectedValues = new Set(column?.getFilterValue() as string[])
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<Popover>
|
|
42
|
+
<PopoverTrigger asChild>
|
|
43
|
+
<Button variant='outline' size='sm' className='h-8 border-dashed'>
|
|
44
|
+
<PlusCircle className='mr-2 h-4 w-4' />
|
|
45
|
+
{title}
|
|
46
|
+
{selectedValues.size > 0 && (
|
|
47
|
+
<>
|
|
48
|
+
<Separator orientation='vertical' className='mx-2 h-4' />
|
|
49
|
+
<Badge variant='secondary' className='rounded-sm px-1 font-normal lg:hidden'>
|
|
50
|
+
{selectedValues.size}
|
|
51
|
+
</Badge>
|
|
52
|
+
<div className='hidden space-x-1 lg:flex'>
|
|
53
|
+
{selectedValues.size > 2 ? (
|
|
54
|
+
<Badge variant='secondary' className='rounded-sm px-1 font-normal'>
|
|
55
|
+
{selectedValues.size} selected
|
|
56
|
+
</Badge>
|
|
57
|
+
) : (
|
|
58
|
+
options
|
|
59
|
+
.filter((option) => selectedValues.has(option.value))
|
|
60
|
+
.map((option) => (
|
|
61
|
+
<Badge key={option.value} variant='secondary' className='rounded-sm px-1 font-normal'>
|
|
62
|
+
{option.label}
|
|
63
|
+
</Badge>
|
|
64
|
+
))
|
|
65
|
+
)}
|
|
66
|
+
</div>
|
|
67
|
+
</>
|
|
68
|
+
)}
|
|
69
|
+
</Button>
|
|
70
|
+
</PopoverTrigger>
|
|
71
|
+
<PopoverContent className='w-[200px] p-0' align='start'>
|
|
72
|
+
<Command>
|
|
73
|
+
<CommandInput placeholder={title} />
|
|
74
|
+
<CommandList>
|
|
75
|
+
<CommandEmpty>No results found.</CommandEmpty>
|
|
76
|
+
<CommandGroup>
|
|
77
|
+
{options.map((option) => {
|
|
78
|
+
const isSelected = selectedValues.has(option.value)
|
|
79
|
+
return (
|
|
80
|
+
<CommandItem
|
|
81
|
+
key={option.value}
|
|
82
|
+
onSelect={() => {
|
|
83
|
+
if (isSelected) {
|
|
84
|
+
selectedValues.delete(option.value)
|
|
85
|
+
} else {
|
|
86
|
+
selectedValues.add(option.value)
|
|
87
|
+
}
|
|
88
|
+
const filterValues = Array.from(selectedValues)
|
|
89
|
+
column?.setFilterValue(filterValues.length ? filterValues : undefined)
|
|
90
|
+
}}
|
|
91
|
+
>
|
|
92
|
+
<div
|
|
93
|
+
className={cn(
|
|
94
|
+
'mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary',
|
|
95
|
+
isSelected ? 'bg-primary text-primary-foreground' : 'opacity-50 [&_svg]:invisible'
|
|
96
|
+
)}
|
|
97
|
+
>
|
|
98
|
+
<Check className='h-4 w-4' />
|
|
99
|
+
</div>
|
|
100
|
+
{option.icon && <option.icon className='mr-2 h-4 w-4 text-muted-foreground' />}
|
|
101
|
+
<span>{option.label}</span>
|
|
102
|
+
{facets?.get(option.value) && (
|
|
103
|
+
<span className='ml-auto flex h-4 w-4 items-center justify-center font-mono text-xs'>
|
|
104
|
+
{facets.get(option.value)}
|
|
105
|
+
</span>
|
|
106
|
+
)}
|
|
107
|
+
</CommandItem>
|
|
108
|
+
)
|
|
109
|
+
})}
|
|
110
|
+
</CommandGroup>
|
|
111
|
+
{selectedValues.size > 0 && (
|
|
112
|
+
<>
|
|
113
|
+
<CommandSeparator />
|
|
114
|
+
<CommandGroup>
|
|
115
|
+
<CommandItem
|
|
116
|
+
onSelect={() => column?.setFilterValue(undefined)}
|
|
117
|
+
className='justify-center text-center'
|
|
118
|
+
>
|
|
119
|
+
Clear filters
|
|
120
|
+
</CommandItem>
|
|
121
|
+
</CommandGroup>
|
|
122
|
+
</>
|
|
123
|
+
)}
|
|
124
|
+
</CommandList>
|
|
125
|
+
</Command>
|
|
126
|
+
</PopoverContent>
|
|
127
|
+
</Popover>
|
|
128
|
+
)
|
|
129
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { type Table } from '@tanstack/react-table'
|
|
2
|
+
import { ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight } from 'lucide-react'
|
|
3
|
+
|
|
4
|
+
import { Button } from '@/components/ui/button'
|
|
5
|
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
|
6
|
+
|
|
7
|
+
interface DataTablePaginationProps<TData> {
|
|
8
|
+
table: Table<TData>
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function DataTablePagination<TData>({ table }: DataTablePaginationProps<TData>) {
|
|
12
|
+
return (
|
|
13
|
+
<div className='flex items-center justify-between px-2'>
|
|
14
|
+
<div className='flex-1 text-sm text-muted-foreground'>
|
|
15
|
+
{table.getFilteredSelectedRowModel().rows.length} of {table.getFilteredRowModel().rows.length} row(s) selected.
|
|
16
|
+
</div>
|
|
17
|
+
<div className='flex items-center space-x-6 lg:space-x-8'>
|
|
18
|
+
<div className='flex items-center space-x-2'>
|
|
19
|
+
<p className='text-sm font-medium'>Rows per page</p>
|
|
20
|
+
<Select
|
|
21
|
+
value={`${table.getState().pagination.pageSize}`}
|
|
22
|
+
onValueChange={(value) => table.setPageSize(Number(value))}
|
|
23
|
+
>
|
|
24
|
+
<SelectTrigger className='h-8 w-[70px]'>
|
|
25
|
+
<SelectValue placeholder={table.getState().pagination.pageSize} />
|
|
26
|
+
</SelectTrigger>
|
|
27
|
+
<SelectContent side='top'>
|
|
28
|
+
{[10, 20, 30, 40, 50].map((pageSize) => (
|
|
29
|
+
<SelectItem key={pageSize} value={`${pageSize}`}>
|
|
30
|
+
{pageSize}
|
|
31
|
+
</SelectItem>
|
|
32
|
+
))}
|
|
33
|
+
</SelectContent>
|
|
34
|
+
</Select>
|
|
35
|
+
</div>
|
|
36
|
+
<div className='flex w-[100px] items-center justify-center text-sm font-medium'>
|
|
37
|
+
Page {table.getState().pagination.pageIndex + 1} of {table.getPageCount()}
|
|
38
|
+
</div>
|
|
39
|
+
<div className='flex items-center space-x-2'>
|
|
40
|
+
<Button
|
|
41
|
+
variant='outline'
|
|
42
|
+
className='hidden h-8 w-8 p-0 lg:flex'
|
|
43
|
+
onClick={() => table.setPageIndex(0)}
|
|
44
|
+
disabled={!table.getCanPreviousPage()}
|
|
45
|
+
>
|
|
46
|
+
<span className='sr-only'>Go to first page</span>
|
|
47
|
+
<ChevronsLeft className='h-4 w-4' />
|
|
48
|
+
</Button>
|
|
49
|
+
<Button
|
|
50
|
+
variant='outline'
|
|
51
|
+
className='h-8 w-8 p-0'
|
|
52
|
+
onClick={() => table.previousPage()}
|
|
53
|
+
disabled={!table.getCanPreviousPage()}
|
|
54
|
+
>
|
|
55
|
+
<span className='sr-only'>Go to previous page</span>
|
|
56
|
+
<ChevronLeft className='h-4 w-4' />
|
|
57
|
+
</Button>
|
|
58
|
+
<Button
|
|
59
|
+
variant='outline'
|
|
60
|
+
className='h-8 w-8 p-0'
|
|
61
|
+
onClick={() => table.nextPage()}
|
|
62
|
+
disabled={!table.getCanNextPage()}
|
|
63
|
+
>
|
|
64
|
+
<span className='sr-only'>Go to next page</span>
|
|
65
|
+
<ChevronRight className='h-4 w-4' />
|
|
66
|
+
</Button>
|
|
67
|
+
<Button
|
|
68
|
+
variant='outline'
|
|
69
|
+
className='hidden h-8 w-8 p-0 lg:flex'
|
|
70
|
+
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
|
|
71
|
+
disabled={!table.getCanNextPage()}
|
|
72
|
+
>
|
|
73
|
+
<span className='sr-only'>Go to last page</span>
|
|
74
|
+
<ChevronsRight className='h-4 w-4' />
|
|
75
|
+
</Button>
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
)
|
|
80
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
import { type Table } from '@tanstack/react-table'
|
|
3
|
+
import { X } from 'lucide-react'
|
|
4
|
+
|
|
5
|
+
import { Button } from '@/components/ui/button'
|
|
6
|
+
import { Input } from '@/components/ui/input'
|
|
7
|
+
import { DataTableViewOptions } from './data-table-view-options'
|
|
8
|
+
import { DataTableFacetedFilter } from './data-table-faceted-filter'
|
|
9
|
+
|
|
10
|
+
interface FilterOption {
|
|
11
|
+
label: string
|
|
12
|
+
value: string
|
|
13
|
+
icon?: React.ComponentType<{ className?: string }>
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface ColumnFilter {
|
|
17
|
+
columnId: string
|
|
18
|
+
title: string
|
|
19
|
+
options: FilterOption[]
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface DataTableToolbarProps<TData> {
|
|
23
|
+
table: Table<TData>
|
|
24
|
+
searchColumn?: string
|
|
25
|
+
searchPlaceholder?: string
|
|
26
|
+
filters?: ColumnFilter[]
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function DataTableToolbar<TData>({
|
|
30
|
+
table,
|
|
31
|
+
searchColumn = 'name',
|
|
32
|
+
searchPlaceholder = 'Search...',
|
|
33
|
+
filters,
|
|
34
|
+
}: DataTableToolbarProps<TData>) {
|
|
35
|
+
const isFiltered = table.getState().columnFilters.length > 0
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<div className='flex items-center justify-between'>
|
|
39
|
+
<div className='flex flex-1 items-center space-x-2'>
|
|
40
|
+
<Input
|
|
41
|
+
placeholder={searchPlaceholder}
|
|
42
|
+
value={(table.getColumn(searchColumn)?.getFilterValue() as string) ?? ''}
|
|
43
|
+
onChange={(event) => table.getColumn(searchColumn)?.setFilterValue(event.target.value)}
|
|
44
|
+
className='h-8 w-[150px] lg:w-[250px]'
|
|
45
|
+
/>
|
|
46
|
+
{filters?.map(
|
|
47
|
+
(filter) =>
|
|
48
|
+
table.getColumn(filter.columnId) && (
|
|
49
|
+
<DataTableFacetedFilter
|
|
50
|
+
key={filter.columnId}
|
|
51
|
+
column={table.getColumn(filter.columnId)}
|
|
52
|
+
title={filter.title}
|
|
53
|
+
options={filter.options}
|
|
54
|
+
/>
|
|
55
|
+
)
|
|
56
|
+
)}
|
|
57
|
+
{isFiltered && (
|
|
58
|
+
<Button variant='ghost' onClick={() => table.resetColumnFilters()} className='h-8 px-2 lg:px-3'>
|
|
59
|
+
Reset <X className='ml-2 h-4 w-4' />
|
|
60
|
+
</Button>
|
|
61
|
+
)}
|
|
62
|
+
</div>
|
|
63
|
+
<DataTableViewOptions table={table} />
|
|
64
|
+
</div>
|
|
65
|
+
)
|
|
66
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { type Table } from '@tanstack/react-table'
|
|
2
|
+
import { Settings2 } from 'lucide-react'
|
|
3
|
+
|
|
4
|
+
import { Button } from '@/components/ui/button'
|
|
5
|
+
import {
|
|
6
|
+
DropdownMenu,
|
|
7
|
+
DropdownMenuCheckboxItem,
|
|
8
|
+
DropdownMenuContent,
|
|
9
|
+
DropdownMenuLabel,
|
|
10
|
+
DropdownMenuSeparator,
|
|
11
|
+
DropdownMenuTrigger,
|
|
12
|
+
} from '@/components/ui/dropdown-menu'
|
|
13
|
+
|
|
14
|
+
interface DataTableViewOptionsProps<TData> {
|
|
15
|
+
table: Table<TData>
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function DataTableViewOptions<TData>({ table }: DataTableViewOptionsProps<TData>) {
|
|
19
|
+
return (
|
|
20
|
+
<DropdownMenu>
|
|
21
|
+
<DropdownMenuTrigger asChild>
|
|
22
|
+
<Button variant='outline' size='sm' className='ml-auto hidden h-8 lg:flex'>
|
|
23
|
+
<Settings2 className='mr-2 h-4 w-4' />
|
|
24
|
+
View
|
|
25
|
+
</Button>
|
|
26
|
+
</DropdownMenuTrigger>
|
|
27
|
+
<DropdownMenuContent align='end' className='w-[150px]'>
|
|
28
|
+
<DropdownMenuLabel>Toggle columns</DropdownMenuLabel>
|
|
29
|
+
<DropdownMenuSeparator />
|
|
30
|
+
{table
|
|
31
|
+
.getAllColumns()
|
|
32
|
+
.filter((column) => typeof column.accessorFn !== 'undefined' && column.getCanHide())
|
|
33
|
+
.map((column) => (
|
|
34
|
+
<DropdownMenuCheckboxItem
|
|
35
|
+
key={column.id}
|
|
36
|
+
className='capitalize'
|
|
37
|
+
checked={column.getIsVisible()}
|
|
38
|
+
onCheckedChange={(value) => column.toggleVisibility(!!value)}
|
|
39
|
+
>
|
|
40
|
+
{column.id}
|
|
41
|
+
</DropdownMenuCheckboxItem>
|
|
42
|
+
))}
|
|
43
|
+
</DropdownMenuContent>
|
|
44
|
+
</DropdownMenu>
|
|
45
|
+
)
|
|
46
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { flexRender, type ColumnDef, type Table as TanstackTable } from '@tanstack/react-table'
|
|
2
|
+
|
|
3
|
+
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
|
|
4
|
+
import { Skeleton } from '@/components/ui/skeleton'
|
|
5
|
+
import { DataTablePagination } from './data-table-pagination'
|
|
6
|
+
|
|
7
|
+
interface DataTableProps<TData> {
|
|
8
|
+
table: TanstackTable<TData>
|
|
9
|
+
columns: ColumnDef<TData>[]
|
|
10
|
+
isLoading?: boolean
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function DataTable<TData>({ table, columns, isLoading }: DataTableProps<TData>) {
|
|
14
|
+
return (
|
|
15
|
+
<div className='space-y-4'>
|
|
16
|
+
<div className='rounded-md border'>
|
|
17
|
+
<Table>
|
|
18
|
+
<TableHeader>
|
|
19
|
+
{table.getHeaderGroups().map((headerGroup) => (
|
|
20
|
+
<TableRow key={headerGroup.id}>
|
|
21
|
+
{headerGroup.headers.map((header) => (
|
|
22
|
+
<TableHead key={header.id} colSpan={header.colSpan}>
|
|
23
|
+
{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
|
|
24
|
+
</TableHead>
|
|
25
|
+
))}
|
|
26
|
+
</TableRow>
|
|
27
|
+
))}
|
|
28
|
+
</TableHeader>
|
|
29
|
+
<TableBody>
|
|
30
|
+
{isLoading ? (
|
|
31
|
+
Array.from({ length: 5 }).map((_, i) => (
|
|
32
|
+
<TableRow key={i}>
|
|
33
|
+
{columns.map((_, j) => (
|
|
34
|
+
<TableCell key={j}>
|
|
35
|
+
<Skeleton className='h-4 w-full' />
|
|
36
|
+
</TableCell>
|
|
37
|
+
))}
|
|
38
|
+
</TableRow>
|
|
39
|
+
))
|
|
40
|
+
) : table.getRowModel().rows.length ? (
|
|
41
|
+
table.getRowModel().rows.map((row) => (
|
|
42
|
+
<TableRow key={row.id} data-state={row.getIsSelected() && 'selected'}>
|
|
43
|
+
{row.getVisibleCells().map((cell) => (
|
|
44
|
+
<TableCell key={cell.id}>
|
|
45
|
+
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
|
46
|
+
</TableCell>
|
|
47
|
+
))}
|
|
48
|
+
</TableRow>
|
|
49
|
+
))
|
|
50
|
+
) : (
|
|
51
|
+
<TableRow>
|
|
52
|
+
<TableCell colSpan={columns.length} className='h-24 text-center text-muted-foreground'>
|
|
53
|
+
No results.
|
|
54
|
+
</TableCell>
|
|
55
|
+
</TableRow>
|
|
56
|
+
)}
|
|
57
|
+
</TableBody>
|
|
58
|
+
</Table>
|
|
59
|
+
</div>
|
|
60
|
+
<DataTablePagination table={table} />
|
|
61
|
+
</div>
|
|
62
|
+
)
|
|
63
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useState } from 'react'
|
|
2
2
|
import { Link } from '@tanstack/react-router'
|
|
3
|
-
import { LayoutDashboard, Settings, User, ChevronLeft, ChevronRight } from 'lucide-react'
|
|
3
|
+
import { LayoutDashboard, Settings, User, Users, ChevronLeft, ChevronRight } from 'lucide-react'
|
|
4
4
|
|
|
5
5
|
import { cn } from '@/lib/utils'
|
|
6
6
|
import { Button } from '@/components/ui/button'
|
|
@@ -9,6 +9,7 @@ import { siteConfig } from '@/config/site'
|
|
|
9
9
|
|
|
10
10
|
const navItems = [
|
|
11
11
|
{ to: '/dashboard', label: 'Dashboard', icon: LayoutDashboard },
|
|
12
|
+
{ to: '/users', label: 'Users', icon: Users },
|
|
12
13
|
{ to: '/profile', label: 'Profile', icon: User },
|
|
13
14
|
{ to: '/settings', label: 'Settings', icon: Settings },
|
|
14
15
|
] as const
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog'
|
|
3
|
+
|
|
4
|
+
import { cn } from '@/lib/utils'
|
|
5
|
+
import { buttonVariants } from '@/components/ui/button'
|
|
6
|
+
|
|
7
|
+
const AlertDialog = AlertDialogPrimitive.Root
|
|
8
|
+
const AlertDialogTrigger = AlertDialogPrimitive.Trigger
|
|
9
|
+
const AlertDialogPortal = AlertDialogPrimitive.Portal
|
|
10
|
+
|
|
11
|
+
const AlertDialogOverlay = React.forwardRef<
|
|
12
|
+
React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
|
|
13
|
+
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
|
|
14
|
+
>(({ className, ...props }, ref) => (
|
|
15
|
+
<AlertDialogPrimitive.Overlay
|
|
16
|
+
className={cn(
|
|
17
|
+
'fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
|
|
18
|
+
className
|
|
19
|
+
)}
|
|
20
|
+
{...props}
|
|
21
|
+
ref={ref}
|
|
22
|
+
/>
|
|
23
|
+
))
|
|
24
|
+
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
|
|
25
|
+
|
|
26
|
+
const AlertDialogContent = React.forwardRef<
|
|
27
|
+
React.ElementRef<typeof AlertDialogPrimitive.Content>,
|
|
28
|
+
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
|
|
29
|
+
>(({ className, ...props }, ref) => (
|
|
30
|
+
<AlertDialogPortal>
|
|
31
|
+
<AlertDialogOverlay />
|
|
32
|
+
<AlertDialogPrimitive.Content
|
|
33
|
+
ref={ref}
|
|
34
|
+
className={cn(
|
|
35
|
+
'fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg',
|
|
36
|
+
className
|
|
37
|
+
)}
|
|
38
|
+
{...props}
|
|
39
|
+
/>
|
|
40
|
+
</AlertDialogPortal>
|
|
41
|
+
))
|
|
42
|
+
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
|
|
43
|
+
|
|
44
|
+
const AlertDialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
|
45
|
+
<div className={cn('flex flex-col space-y-2 text-center sm:text-left', className)} {...props} />
|
|
46
|
+
)
|
|
47
|
+
AlertDialogHeader.displayName = 'AlertDialogHeader'
|
|
48
|
+
|
|
49
|
+
const AlertDialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
|
50
|
+
<div className={cn('flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2', className)} {...props} />
|
|
51
|
+
)
|
|
52
|
+
AlertDialogFooter.displayName = 'AlertDialogFooter'
|
|
53
|
+
|
|
54
|
+
const AlertDialogTitle = React.forwardRef<
|
|
55
|
+
React.ElementRef<typeof AlertDialogPrimitive.Title>,
|
|
56
|
+
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
|
|
57
|
+
>(({ className, ...props }, ref) => (
|
|
58
|
+
<AlertDialogPrimitive.Title ref={ref} className={cn('text-lg font-semibold', className)} {...props} />
|
|
59
|
+
))
|
|
60
|
+
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
|
|
61
|
+
|
|
62
|
+
const AlertDialogDescription = React.forwardRef<
|
|
63
|
+
React.ElementRef<typeof AlertDialogPrimitive.Description>,
|
|
64
|
+
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
|
|
65
|
+
>(({ className, ...props }, ref) => (
|
|
66
|
+
<AlertDialogPrimitive.Description
|
|
67
|
+
ref={ref}
|
|
68
|
+
className={cn('text-sm text-muted-foreground', className)}
|
|
69
|
+
{...props}
|
|
70
|
+
/>
|
|
71
|
+
))
|
|
72
|
+
AlertDialogDescription.displayName = AlertDialogPrimitive.Description.displayName
|
|
73
|
+
|
|
74
|
+
const AlertDialogAction = React.forwardRef<
|
|
75
|
+
React.ElementRef<typeof AlertDialogPrimitive.Action>,
|
|
76
|
+
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
|
|
77
|
+
>(({ className, ...props }, ref) => (
|
|
78
|
+
<AlertDialogPrimitive.Action ref={ref} className={cn(buttonVariants(), className)} {...props} />
|
|
79
|
+
))
|
|
80
|
+
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
|
|
81
|
+
|
|
82
|
+
const AlertDialogCancel = React.forwardRef<
|
|
83
|
+
React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
|
|
84
|
+
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
|
|
85
|
+
>(({ className, ...props }, ref) => (
|
|
86
|
+
<AlertDialogPrimitive.Cancel
|
|
87
|
+
ref={ref}
|
|
88
|
+
className={cn(buttonVariants({ variant: 'outline' }), 'mt-2 sm:mt-0', className)}
|
|
89
|
+
{...props}
|
|
90
|
+
/>
|
|
91
|
+
))
|
|
92
|
+
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
|
|
93
|
+
|
|
94
|
+
export {
|
|
95
|
+
AlertDialog,
|
|
96
|
+
AlertDialogPortal,
|
|
97
|
+
AlertDialogOverlay,
|
|
98
|
+
AlertDialogTrigger,
|
|
99
|
+
AlertDialogContent,
|
|
100
|
+
AlertDialogHeader,
|
|
101
|
+
AlertDialogFooter,
|
|
102
|
+
AlertDialogTitle,
|
|
103
|
+
AlertDialogDescription,
|
|
104
|
+
AlertDialogAction,
|
|
105
|
+
AlertDialogCancel,
|
|
106
|
+
}
|