create-ej-app 1.0.0
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/LICENSE +21 -0
- package/README.md +5 -0
- package/cli.js +68 -0
- package/package.json +36 -0
- package/template/.env.example +12 -0
- package/template/.eslintrc.cjs +24 -0
- package/template/components.json +16 -0
- package/template/next.config.mjs +18 -0
- package/template/package.json +68 -0
- package/template/postcss.config.cjs +6 -0
- package/template/prettier.config.ts +11 -0
- package/template/prisma/schema.prisma +136 -0
- package/template/public/next.svg +1 -0
- package/template/src/app/(app)/dashboard/page.tsx +19 -0
- package/template/src/app/(app)/layout.tsx +117 -0
- package/template/src/app/(app)/roles/[id]/actions.ts +100 -0
- package/template/src/app/(app)/roles/[id]/page.tsx +36 -0
- package/template/src/app/(app)/roles/[id]/render.tsx +266 -0
- package/template/src/app/(app)/roles/actions.ts +67 -0
- package/template/src/app/(app)/roles/page.tsx +51 -0
- package/template/src/app/(app)/roles/render.tsx +70 -0
- package/template/src/app/(app)/settings/actions.ts +18 -0
- package/template/src/app/(app)/settings/loading.tsx +28 -0
- package/template/src/app/(app)/settings/page.tsx +28 -0
- package/template/src/app/(app)/settings/render.tsx +113 -0
- package/template/src/app/(app)/tasks/[id]/actions.ts +107 -0
- package/template/src/app/(app)/tasks/[id]/page.tsx +55 -0
- package/template/src/app/(app)/tasks/[id]/render.tsx +307 -0
- package/template/src/app/(app)/tasks/actions.ts +75 -0
- package/template/src/app/(app)/tasks/page.tsx +66 -0
- package/template/src/app/(app)/tasks/render.tsx +124 -0
- package/template/src/app/(app)/users/[id]/actions.ts +104 -0
- package/template/src/app/(app)/users/[id]/page.tsx +39 -0
- package/template/src/app/(app)/users/[id]/render.tsx +320 -0
- package/template/src/app/(app)/users/actions.ts +67 -0
- package/template/src/app/(app)/users/page.tsx +61 -0
- package/template/src/app/(app)/users/render.tsx +108 -0
- package/template/src/app/(auth)/layout.tsx +20 -0
- package/template/src/app/(auth)/login/page.tsx +49 -0
- package/template/src/app/(auth)/login/user-auth-form.tsx +152 -0
- package/template/src/app/(marketing)/layout.tsx +42 -0
- package/template/src/app/(marketing)/page.tsx +96 -0
- package/template/src/app/api/auth/[...nextauth]/route.ts +7 -0
- package/template/src/app/favicon.ico +0 -0
- package/template/src/app/globals.css +76 -0
- package/template/src/app/layout.tsx +32 -0
- package/template/src/components/app-nav-command.tsx +160 -0
- package/template/src/components/dark-mode-toggle.tsx +43 -0
- package/template/src/components/data-table/data-table-column-header.tsx +91 -0
- package/template/src/components/data-table/data-table-faceted-filter.tsx +149 -0
- package/template/src/components/data-table/data-table-loading.tsx +77 -0
- package/template/src/components/data-table/data-table-pagination.tsx +94 -0
- package/template/src/components/data-table/data-table-toolbar.tsx +78 -0
- package/template/src/components/data-table/data-table-types.ts +17 -0
- package/template/src/components/data-table/data-table-view-options.tsx +59 -0
- package/template/src/components/data-table/data-table.tsx +301 -0
- package/template/src/components/data-table/simple-data-table.tsx +109 -0
- package/template/src/components/empty-placeholder.tsx +78 -0
- package/template/src/components/heading.tsx +17 -0
- package/template/src/components/icons.tsx +83 -0
- package/template/src/components/main-nav.tsx +64 -0
- package/template/src/components/mobile-nav.tsx +50 -0
- package/template/src/components/shell.tsx +13 -0
- package/template/src/components/side-nav.tsx +51 -0
- package/template/src/components/site-footer.tsx +31 -0
- package/template/src/components/system-info.tsx +36 -0
- package/template/src/components/theme-provider.tsx +13 -0
- package/template/src/components/ui/alert-dialog.tsx +145 -0
- package/template/src/components/ui/avatar.tsx +50 -0
- package/template/src/components/ui/badge.tsx +36 -0
- package/template/src/components/ui/button.tsx +56 -0
- package/template/src/components/ui/card.tsx +79 -0
- package/template/src/components/ui/checkbox.tsx +30 -0
- package/template/src/components/ui/command.tsx +155 -0
- package/template/src/components/ui/dialog.tsx +123 -0
- package/template/src/components/ui/dropdown-menu.tsx +200 -0
- package/template/src/components/ui/form.tsx +176 -0
- package/template/src/components/ui/input.tsx +25 -0
- package/template/src/components/ui/label.tsx +26 -0
- package/template/src/components/ui/popover.tsx +31 -0
- package/template/src/components/ui/select.tsx +121 -0
- package/template/src/components/ui/separator.tsx +31 -0
- package/template/src/components/ui/skeleton.tsx +15 -0
- package/template/src/components/ui/table.tsx +114 -0
- package/template/src/components/ui/textarea.tsx +24 -0
- package/template/src/components/ui/toast.tsx +127 -0
- package/template/src/components/ui/toaster.tsx +35 -0
- package/template/src/components/ui/use-toast.ts +189 -0
- package/template/src/components/user-account-nav.tsx +60 -0
- package/template/src/components/user-avatar.tsx +25 -0
- package/template/src/env.mjs +67 -0
- package/template/src/hooks/use-debounce.ts +17 -0
- package/template/src/hooks/use-lock-body.tsx +12 -0
- package/template/src/lib/auth.ts +183 -0
- package/template/src/lib/cn.ts +4 -0
- package/template/src/lib/db.ts +20 -0
- package/template/src/lib/delay.ts +2 -0
- package/template/src/lib/formatDate.ts +11 -0
- package/template/src/lib/generateLabel.ts +5 -0
- package/template/src/lib/siteConfig.ts +4 -0
- package/template/src/types/UnwrapPromise.ts +1 -0
- package/template/tailwind.config.ts +79 -0
- package/template/tsconfig.json +46 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { ComponentType } from 'react'
|
|
2
|
+
|
|
3
|
+
export type Option = {
|
|
4
|
+
label: string
|
|
5
|
+
value: string
|
|
6
|
+
icon?: ComponentType<{ className?: string }>
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface DataTableSearchableColumn<TData> {
|
|
10
|
+
id: keyof TData
|
|
11
|
+
title: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface DataTableFilterableColumn<TData>
|
|
15
|
+
extends DataTableSearchableColumn<TData> {
|
|
16
|
+
options: Option[]
|
|
17
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { DropdownMenuTrigger } from '@radix-ui/react-dropdown-menu'
|
|
4
|
+
import { type Table } from '@tanstack/react-table'
|
|
5
|
+
|
|
6
|
+
import { Icons } from '@/components/icons'
|
|
7
|
+
import { Button } from '@/components/ui/button'
|
|
8
|
+
import {
|
|
9
|
+
DropdownMenu,
|
|
10
|
+
DropdownMenuCheckboxItem,
|
|
11
|
+
DropdownMenuContent,
|
|
12
|
+
DropdownMenuLabel,
|
|
13
|
+
DropdownMenuSeparator
|
|
14
|
+
} from '@/components/ui/dropdown-menu'
|
|
15
|
+
|
|
16
|
+
interface DataTableViewOptionsProps<TData> {
|
|
17
|
+
table: Table<TData>
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function DataTableViewOptions<TData>({
|
|
21
|
+
table
|
|
22
|
+
}: DataTableViewOptionsProps<TData>) {
|
|
23
|
+
return (
|
|
24
|
+
<DropdownMenu>
|
|
25
|
+
<DropdownMenuTrigger asChild>
|
|
26
|
+
<Button
|
|
27
|
+
variant="outline"
|
|
28
|
+
size="sm"
|
|
29
|
+
className="ml-auto hidden h-8 lg:flex"
|
|
30
|
+
>
|
|
31
|
+
<Icons.mixer className="mr-2 h-4 w-4" />
|
|
32
|
+
View
|
|
33
|
+
</Button>
|
|
34
|
+
</DropdownMenuTrigger>
|
|
35
|
+
<DropdownMenuContent align="end" className="w-[150px]">
|
|
36
|
+
<DropdownMenuLabel>Toggle columns</DropdownMenuLabel>
|
|
37
|
+
<DropdownMenuSeparator />
|
|
38
|
+
{table
|
|
39
|
+
.getAllColumns()
|
|
40
|
+
.filter(
|
|
41
|
+
column =>
|
|
42
|
+
typeof column.accessorFn !== 'undefined' && column.getCanHide()
|
|
43
|
+
)
|
|
44
|
+
.map(column => {
|
|
45
|
+
return (
|
|
46
|
+
<DropdownMenuCheckboxItem
|
|
47
|
+
key={column.id}
|
|
48
|
+
className="capitalize"
|
|
49
|
+
checked={column.getIsVisible()}
|
|
50
|
+
onCheckedChange={value => column.toggleVisibility(!!value)}
|
|
51
|
+
>
|
|
52
|
+
{column.id}
|
|
53
|
+
</DropdownMenuCheckboxItem>
|
|
54
|
+
)
|
|
55
|
+
})}
|
|
56
|
+
</DropdownMenuContent>
|
|
57
|
+
</DropdownMenu>
|
|
58
|
+
)
|
|
59
|
+
}
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
flexRender,
|
|
5
|
+
getCoreRowModel,
|
|
6
|
+
getFacetedRowModel,
|
|
7
|
+
getFacetedUniqueValues,
|
|
8
|
+
getFilteredRowModel,
|
|
9
|
+
getPaginationRowModel,
|
|
10
|
+
getSortedRowModel,
|
|
11
|
+
useReactTable,
|
|
12
|
+
type ColumnDef,
|
|
13
|
+
type ColumnFiltersState,
|
|
14
|
+
type PaginationState,
|
|
15
|
+
type SortingState,
|
|
16
|
+
type VisibilityState
|
|
17
|
+
} from '@tanstack/react-table'
|
|
18
|
+
import { usePathname, useRouter, useSearchParams } from 'next/navigation'
|
|
19
|
+
|
|
20
|
+
import { DataTablePagination } from '@/components/data-table/data-table-pagination'
|
|
21
|
+
import { DataTableToolbar } from '@/components/data-table/data-table-toolbar'
|
|
22
|
+
import {
|
|
23
|
+
Table,
|
|
24
|
+
TableBody,
|
|
25
|
+
TableCell,
|
|
26
|
+
TableHead,
|
|
27
|
+
TableHeader,
|
|
28
|
+
TableRow
|
|
29
|
+
} from '@/components/ui/table'
|
|
30
|
+
import { useDebounce } from '@/hooks/use-debounce'
|
|
31
|
+
import { useCallback, useEffect, useMemo, useState } from 'react'
|
|
32
|
+
import {
|
|
33
|
+
DataTableFilterableColumn,
|
|
34
|
+
DataTableSearchableColumn
|
|
35
|
+
} from './data-table-types'
|
|
36
|
+
|
|
37
|
+
interface DataTableProps<TData, TValue> {
|
|
38
|
+
columns: ColumnDef<TData, TValue>[]
|
|
39
|
+
data: TData[]
|
|
40
|
+
total: number
|
|
41
|
+
filterableColumns?: DataTableFilterableColumn<TData>[]
|
|
42
|
+
searchableColumns?: DataTableSearchableColumn<TData>[]
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function DataTable<TData, TValue>({
|
|
46
|
+
columns,
|
|
47
|
+
data,
|
|
48
|
+
total,
|
|
49
|
+
filterableColumns = [],
|
|
50
|
+
searchableColumns = []
|
|
51
|
+
}: DataTableProps<TData, TValue>) {
|
|
52
|
+
const router = useRouter()
|
|
53
|
+
const pathname = usePathname()
|
|
54
|
+
const searchParams = useSearchParams()
|
|
55
|
+
|
|
56
|
+
// Search params
|
|
57
|
+
const page = searchParams?.get('page') ?? '1'
|
|
58
|
+
const limit = searchParams?.get('limit') ?? '10'
|
|
59
|
+
const sort = searchParams?.get('sort')
|
|
60
|
+
const [column, order] = sort?.split('.') ?? []
|
|
61
|
+
|
|
62
|
+
// Create query string
|
|
63
|
+
const createQueryString = useCallback(
|
|
64
|
+
(params: Record<string, string | number | null>) => {
|
|
65
|
+
const newSearchParams = new URLSearchParams(searchParams?.toString())
|
|
66
|
+
|
|
67
|
+
for (const [key, value] of Object.entries(params)) {
|
|
68
|
+
if (value === null) {
|
|
69
|
+
newSearchParams.delete(key)
|
|
70
|
+
} else {
|
|
71
|
+
newSearchParams.set(key, String(value))
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return newSearchParams.toString()
|
|
76
|
+
},
|
|
77
|
+
[searchParams]
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
// Table states
|
|
81
|
+
const [rowSelection, setRowSelection] = useState({})
|
|
82
|
+
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({})
|
|
83
|
+
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([])
|
|
84
|
+
|
|
85
|
+
// Handle server-side pagination
|
|
86
|
+
const [{ pageIndex, pageSize }, setPagination] = useState<PaginationState>({
|
|
87
|
+
pageIndex: Number(page) - 1,
|
|
88
|
+
pageSize: Number(limit)
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
const pagination = useMemo(
|
|
92
|
+
() => ({
|
|
93
|
+
pageIndex,
|
|
94
|
+
pageSize
|
|
95
|
+
}),
|
|
96
|
+
[pageIndex, pageSize]
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
useEffect(() => {
|
|
100
|
+
setPagination({
|
|
101
|
+
pageIndex: Number(page) - 1,
|
|
102
|
+
pageSize: Number(limit)
|
|
103
|
+
})
|
|
104
|
+
}, [page, limit])
|
|
105
|
+
|
|
106
|
+
useEffect(() => {
|
|
107
|
+
router.push(
|
|
108
|
+
`${pathname}?${createQueryString({
|
|
109
|
+
page: pageIndex + 1,
|
|
110
|
+
limit: pageSize
|
|
111
|
+
})}`
|
|
112
|
+
)
|
|
113
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
114
|
+
}, [pageIndex, pageSize])
|
|
115
|
+
|
|
116
|
+
// Handle server-side sorting
|
|
117
|
+
const [sorting, setSorting] = useState<SortingState>([
|
|
118
|
+
{
|
|
119
|
+
id: column ?? '',
|
|
120
|
+
desc: order === 'desc'
|
|
121
|
+
}
|
|
122
|
+
])
|
|
123
|
+
|
|
124
|
+
useEffect(() => {
|
|
125
|
+
router.push(
|
|
126
|
+
`${pathname}?${createQueryString({
|
|
127
|
+
page,
|
|
128
|
+
sort: sorting[0]?.id
|
|
129
|
+
? `${sorting[0]?.id}.${sorting[0]?.desc ? 'desc' : 'asc'}`
|
|
130
|
+
: null
|
|
131
|
+
})}`
|
|
132
|
+
)
|
|
133
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
134
|
+
}, [sorting])
|
|
135
|
+
|
|
136
|
+
// Handle server-side filtering
|
|
137
|
+
const debouncedSearchableColumnFilters = JSON.parse(
|
|
138
|
+
useDebounce(
|
|
139
|
+
JSON.stringify(
|
|
140
|
+
columnFilters.filter(filter => {
|
|
141
|
+
return searchableColumns.find(column => column.id === filter.id)
|
|
142
|
+
})
|
|
143
|
+
),
|
|
144
|
+
500
|
|
145
|
+
)
|
|
146
|
+
) as ColumnFiltersState
|
|
147
|
+
|
|
148
|
+
const filterableColumnFilters = columnFilters.filter(filter => {
|
|
149
|
+
return filterableColumns.find(column => column.id === filter.id)
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
useEffect(() => {
|
|
153
|
+
for (const column of debouncedSearchableColumnFilters) {
|
|
154
|
+
if (typeof column.value === 'string') {
|
|
155
|
+
router.push(
|
|
156
|
+
`${pathname}?${createQueryString({
|
|
157
|
+
page: 1,
|
|
158
|
+
[column.id]: typeof column.value === 'string' ? column.value : null
|
|
159
|
+
})}`
|
|
160
|
+
)
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
for (const key of searchParams.keys()) {
|
|
165
|
+
if (
|
|
166
|
+
searchableColumns.find(column => column.id === key) &&
|
|
167
|
+
!debouncedSearchableColumnFilters.find(column => column.id === key)
|
|
168
|
+
) {
|
|
169
|
+
router.push(
|
|
170
|
+
`${pathname}?${createQueryString({
|
|
171
|
+
page: 1,
|
|
172
|
+
[key]: null
|
|
173
|
+
})}`
|
|
174
|
+
)
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
178
|
+
}, [JSON.stringify(debouncedSearchableColumnFilters)])
|
|
179
|
+
|
|
180
|
+
useEffect(() => {
|
|
181
|
+
for (const column of filterableColumnFilters) {
|
|
182
|
+
if (typeof column.value === 'object' && Array.isArray(column.value)) {
|
|
183
|
+
router.push(
|
|
184
|
+
`${pathname}?${createQueryString({
|
|
185
|
+
page: 1,
|
|
186
|
+
[column.id]: column.value.join('.')
|
|
187
|
+
})}`
|
|
188
|
+
)
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
for (const key of searchParams.keys()) {
|
|
193
|
+
if (
|
|
194
|
+
filterableColumns.find(column => column.id === key) &&
|
|
195
|
+
!filterableColumnFilters.find(column => column.id === key)
|
|
196
|
+
) {
|
|
197
|
+
router.push(
|
|
198
|
+
`${pathname}?${createQueryString({
|
|
199
|
+
page: 1,
|
|
200
|
+
[key]: null
|
|
201
|
+
})}`
|
|
202
|
+
)
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
206
|
+
}, [JSON.stringify(filterableColumnFilters)])
|
|
207
|
+
|
|
208
|
+
const pageCount = useMemo(
|
|
209
|
+
() => Math.ceil(total / pageSize),
|
|
210
|
+
[total, pageSize]
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
const table = useReactTable({
|
|
214
|
+
data,
|
|
215
|
+
columns,
|
|
216
|
+
pageCount,
|
|
217
|
+
state: {
|
|
218
|
+
pagination,
|
|
219
|
+
sorting,
|
|
220
|
+
columnVisibility,
|
|
221
|
+
rowSelection,
|
|
222
|
+
columnFilters
|
|
223
|
+
},
|
|
224
|
+
enableRowSelection: true,
|
|
225
|
+
onRowSelectionChange: setRowSelection,
|
|
226
|
+
onPaginationChange: setPagination,
|
|
227
|
+
onSortingChange: setSorting,
|
|
228
|
+
onColumnFiltersChange: setColumnFilters,
|
|
229
|
+
onColumnVisibilityChange: setColumnVisibility,
|
|
230
|
+
getCoreRowModel: getCoreRowModel(),
|
|
231
|
+
getFilteredRowModel: getFilteredRowModel(),
|
|
232
|
+
getPaginationRowModel: getPaginationRowModel(),
|
|
233
|
+
getSortedRowModel: getSortedRowModel(),
|
|
234
|
+
getFacetedRowModel: getFacetedRowModel(),
|
|
235
|
+
getFacetedUniqueValues: getFacetedUniqueValues(),
|
|
236
|
+
manualPagination: true,
|
|
237
|
+
manualSorting: true,
|
|
238
|
+
manualFiltering: true
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
return (
|
|
242
|
+
<div className="w-full space-y-4 overflow-auto">
|
|
243
|
+
<DataTableToolbar
|
|
244
|
+
table={table}
|
|
245
|
+
filterableColumns={filterableColumns}
|
|
246
|
+
searchableColumns={searchableColumns}
|
|
247
|
+
/>
|
|
248
|
+
<div className="rounded-md border">
|
|
249
|
+
<Table>
|
|
250
|
+
<TableHeader>
|
|
251
|
+
{table.getHeaderGroups().map(headerGroup => (
|
|
252
|
+
<TableRow key={headerGroup.id}>
|
|
253
|
+
{headerGroup.headers.map(header => {
|
|
254
|
+
return (
|
|
255
|
+
<TableHead key={header.id}>
|
|
256
|
+
{header.isPlaceholder
|
|
257
|
+
? null
|
|
258
|
+
: flexRender(
|
|
259
|
+
header.column.columnDef.header,
|
|
260
|
+
header.getContext()
|
|
261
|
+
)}
|
|
262
|
+
</TableHead>
|
|
263
|
+
)
|
|
264
|
+
})}
|
|
265
|
+
</TableRow>
|
|
266
|
+
))}
|
|
267
|
+
</TableHeader>
|
|
268
|
+
<TableBody>
|
|
269
|
+
{table.getRowModel().rows?.length ? (
|
|
270
|
+
table.getRowModel().rows.map(row => (
|
|
271
|
+
<TableRow
|
|
272
|
+
key={row.id}
|
|
273
|
+
data-state={row.getIsSelected() && 'selected'}
|
|
274
|
+
>
|
|
275
|
+
{row.getVisibleCells().map(cell => (
|
|
276
|
+
<TableCell key={cell.id}>
|
|
277
|
+
{flexRender(
|
|
278
|
+
cell.column.columnDef.cell,
|
|
279
|
+
cell.getContext()
|
|
280
|
+
)}
|
|
281
|
+
</TableCell>
|
|
282
|
+
))}
|
|
283
|
+
</TableRow>
|
|
284
|
+
))
|
|
285
|
+
) : (
|
|
286
|
+
<TableRow>
|
|
287
|
+
<TableCell
|
|
288
|
+
colSpan={columns.length}
|
|
289
|
+
className="h-24 text-center"
|
|
290
|
+
>
|
|
291
|
+
No results.
|
|
292
|
+
</TableCell>
|
|
293
|
+
</TableRow>
|
|
294
|
+
)}
|
|
295
|
+
</TableBody>
|
|
296
|
+
</Table>
|
|
297
|
+
</div>
|
|
298
|
+
<DataTablePagination table={table} />
|
|
299
|
+
</div>
|
|
300
|
+
)
|
|
301
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
ColumnDef,
|
|
5
|
+
flexRender,
|
|
6
|
+
getCoreRowModel,
|
|
7
|
+
getPaginationRowModel,
|
|
8
|
+
useReactTable
|
|
9
|
+
} from '@tanstack/react-table'
|
|
10
|
+
|
|
11
|
+
import { Button } from '@/components/ui/button'
|
|
12
|
+
import {
|
|
13
|
+
Table,
|
|
14
|
+
TableBody,
|
|
15
|
+
TableCell,
|
|
16
|
+
TableHead,
|
|
17
|
+
TableHeader,
|
|
18
|
+
TableRow
|
|
19
|
+
} from '@/components/ui/table'
|
|
20
|
+
|
|
21
|
+
interface DataTableProps<TData, TValue> {
|
|
22
|
+
columns: ColumnDef<TData, TValue>[]
|
|
23
|
+
data: TData[]
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function SimpleDataTable<TData, TValue>({
|
|
27
|
+
columns,
|
|
28
|
+
data
|
|
29
|
+
}: DataTableProps<TData, TValue>) {
|
|
30
|
+
const table = useReactTable({
|
|
31
|
+
data,
|
|
32
|
+
columns,
|
|
33
|
+
getCoreRowModel: getCoreRowModel(),
|
|
34
|
+
getPaginationRowModel: getPaginationRowModel()
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<div>
|
|
39
|
+
<div className="rounded-md border">
|
|
40
|
+
<Table>
|
|
41
|
+
<TableHeader>
|
|
42
|
+
{table.getHeaderGroups().map(headerGroup => (
|
|
43
|
+
<TableRow key={headerGroup.id}>
|
|
44
|
+
{headerGroup.headers.map(header => {
|
|
45
|
+
return (
|
|
46
|
+
<TableHead key={header.id}>
|
|
47
|
+
{header.isPlaceholder
|
|
48
|
+
? null
|
|
49
|
+
: flexRender(
|
|
50
|
+
header.column.columnDef.header,
|
|
51
|
+
header.getContext()
|
|
52
|
+
)}
|
|
53
|
+
</TableHead>
|
|
54
|
+
)
|
|
55
|
+
})}
|
|
56
|
+
</TableRow>
|
|
57
|
+
))}
|
|
58
|
+
</TableHeader>
|
|
59
|
+
<TableBody>
|
|
60
|
+
{table.getRowModel().rows?.length ? (
|
|
61
|
+
table.getRowModel().rows.map(row => (
|
|
62
|
+
<TableRow
|
|
63
|
+
key={row.id}
|
|
64
|
+
data-state={row.getIsSelected() && 'selected'}
|
|
65
|
+
>
|
|
66
|
+
{row.getVisibleCells().map(cell => (
|
|
67
|
+
<TableCell key={cell.id}>
|
|
68
|
+
{flexRender(
|
|
69
|
+
cell.column.columnDef.cell,
|
|
70
|
+
cell.getContext()
|
|
71
|
+
)}
|
|
72
|
+
</TableCell>
|
|
73
|
+
))}
|
|
74
|
+
</TableRow>
|
|
75
|
+
))
|
|
76
|
+
) : (
|
|
77
|
+
<TableRow>
|
|
78
|
+
<TableCell
|
|
79
|
+
colSpan={columns.length}
|
|
80
|
+
className="h-24 text-center"
|
|
81
|
+
>
|
|
82
|
+
No results.
|
|
83
|
+
</TableCell>
|
|
84
|
+
</TableRow>
|
|
85
|
+
)}
|
|
86
|
+
</TableBody>
|
|
87
|
+
</Table>
|
|
88
|
+
</div>
|
|
89
|
+
<div className="flex items-center justify-end space-x-2 py-4">
|
|
90
|
+
<Button
|
|
91
|
+
variant="outline"
|
|
92
|
+
size="sm"
|
|
93
|
+
onClick={() => table.previousPage()}
|
|
94
|
+
disabled={!table.getCanPreviousPage()}
|
|
95
|
+
>
|
|
96
|
+
Previous
|
|
97
|
+
</Button>
|
|
98
|
+
<Button
|
|
99
|
+
variant="outline"
|
|
100
|
+
size="sm"
|
|
101
|
+
onClick={() => table.nextPage()}
|
|
102
|
+
disabled={!table.getCanNextPage()}
|
|
103
|
+
>
|
|
104
|
+
Next
|
|
105
|
+
</Button>
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
)
|
|
109
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
|
|
3
|
+
import { Icons } from '@/components/icons'
|
|
4
|
+
import { cn } from '@/lib/cn'
|
|
5
|
+
|
|
6
|
+
interface EmptyPlaceholderProps extends React.HTMLAttributes<HTMLDivElement> {}
|
|
7
|
+
|
|
8
|
+
export function EmptyPlaceholder({
|
|
9
|
+
className,
|
|
10
|
+
children,
|
|
11
|
+
...props
|
|
12
|
+
}: EmptyPlaceholderProps) {
|
|
13
|
+
return (
|
|
14
|
+
<div
|
|
15
|
+
className={cn(
|
|
16
|
+
'flex min-h-[400px] flex-col items-center justify-center rounded-md border border-dashed p-8 text-center animate-in fade-in-50',
|
|
17
|
+
className
|
|
18
|
+
)}
|
|
19
|
+
{...props}
|
|
20
|
+
>
|
|
21
|
+
<div className="mx-auto flex max-w-[420px] flex-col items-center justify-center text-center">
|
|
22
|
+
{children}
|
|
23
|
+
</div>
|
|
24
|
+
</div>
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface EmptyPlaceholderIconProps
|
|
29
|
+
extends Partial<React.SVGProps<SVGSVGElement>> {
|
|
30
|
+
name: keyof typeof Icons
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
EmptyPlaceholder.Icon = function EmptyPlaceHolderIcon({
|
|
34
|
+
name,
|
|
35
|
+
className
|
|
36
|
+
}: EmptyPlaceholderIconProps) {
|
|
37
|
+
const Icon = Icons[name]
|
|
38
|
+
|
|
39
|
+
if (!Icon) {
|
|
40
|
+
return null
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<div className="flex h-20 w-20 items-center justify-center rounded-full bg-muted">
|
|
45
|
+
<Icon className={cn('h-10 w-10', className)} />
|
|
46
|
+
</div>
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
interface EmptyPlacholderTitleProps
|
|
51
|
+
extends React.HTMLAttributes<HTMLHeadingElement> {}
|
|
52
|
+
|
|
53
|
+
EmptyPlaceholder.Title = function EmptyPlaceholderTitle({
|
|
54
|
+
className,
|
|
55
|
+
...props
|
|
56
|
+
}: EmptyPlacholderTitleProps) {
|
|
57
|
+
return (
|
|
58
|
+
<h2 className={cn('mt-6 text-xl font-semibold', className)} {...props} />
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
interface EmptyPlacholderDescriptionProps
|
|
63
|
+
extends React.HTMLAttributes<HTMLParagraphElement> {}
|
|
64
|
+
|
|
65
|
+
EmptyPlaceholder.Description = function EmptyPlaceholderDescription({
|
|
66
|
+
className,
|
|
67
|
+
...props
|
|
68
|
+
}: EmptyPlacholderDescriptionProps) {
|
|
69
|
+
return (
|
|
70
|
+
<p
|
|
71
|
+
className={cn(
|
|
72
|
+
'mb-8 mt-2 text-center text-sm font-normal leading-6 text-muted-foreground',
|
|
73
|
+
className
|
|
74
|
+
)}
|
|
75
|
+
{...props}
|
|
76
|
+
/>
|
|
77
|
+
)
|
|
78
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { FC, ReactNode } from 'react'
|
|
2
|
+
|
|
3
|
+
export const Heading: FC<{
|
|
4
|
+
heading: string
|
|
5
|
+
text?: string
|
|
6
|
+
children?: ReactNode
|
|
7
|
+
}> = ({ heading, text, children }) => {
|
|
8
|
+
return (
|
|
9
|
+
<div className="flex items-center justify-between px-2">
|
|
10
|
+
<div className="grid gap-1">
|
|
11
|
+
<h1 className="font-heading text-3xl md:text-4xl">{heading}</h1>
|
|
12
|
+
{text && <p className="text-lg text-muted-foreground">{text}</p>}
|
|
13
|
+
</div>
|
|
14
|
+
{children}
|
|
15
|
+
</div>
|
|
16
|
+
)
|
|
17
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ArrowDown,
|
|
3
|
+
ArrowRight,
|
|
4
|
+
ArrowUp,
|
|
5
|
+
ArrowUpDown,
|
|
6
|
+
Biohazard,
|
|
7
|
+
Check,
|
|
8
|
+
ChevronLeft,
|
|
9
|
+
ChevronRight,
|
|
10
|
+
ChevronsLeft,
|
|
11
|
+
ChevronsRight,
|
|
12
|
+
ClipboardList,
|
|
13
|
+
Download,
|
|
14
|
+
Edit,
|
|
15
|
+
EyeOff,
|
|
16
|
+
Github,
|
|
17
|
+
Laptop,
|
|
18
|
+
LayoutDashboard,
|
|
19
|
+
Loader2,
|
|
20
|
+
LogIn,
|
|
21
|
+
LogOut,
|
|
22
|
+
LucideIcon,
|
|
23
|
+
LucideProps,
|
|
24
|
+
Moon,
|
|
25
|
+
Plus,
|
|
26
|
+
PlusCircle,
|
|
27
|
+
Save,
|
|
28
|
+
Settings,
|
|
29
|
+
Settings2,
|
|
30
|
+
Sun,
|
|
31
|
+
Trash,
|
|
32
|
+
User,
|
|
33
|
+
X
|
|
34
|
+
} from 'lucide-react'
|
|
35
|
+
|
|
36
|
+
export type Icon = LucideIcon
|
|
37
|
+
|
|
38
|
+
export const Icons = {
|
|
39
|
+
logo: Biohazard,
|
|
40
|
+
sun: Sun,
|
|
41
|
+
moon: Moon,
|
|
42
|
+
laptop: Laptop,
|
|
43
|
+
chevronLeft: ChevronLeft,
|
|
44
|
+
arrowRight: ArrowRight,
|
|
45
|
+
github: Github,
|
|
46
|
+
google: ({ ...props }: LucideProps) => (
|
|
47
|
+
<svg
|
|
48
|
+
aria-hidden="true"
|
|
49
|
+
focusable="false"
|
|
50
|
+
data-prefix="fab"
|
|
51
|
+
data-icon="google"
|
|
52
|
+
role="img"
|
|
53
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
54
|
+
viewBox="0 0 30 30"
|
|
55
|
+
{...props}
|
|
56
|
+
>
|
|
57
|
+
<path d="M 15.003906 3 C 8.3749062 3 3 8.373 3 15 C 3 21.627 8.3749062 27 15.003906 27 C 25.013906 27 27.269078 17.707 26.330078 13 L 25 13 L 22.732422 13 L 15 13 L 15 17 L 22.738281 17 C 21.848702 20.448251 18.725955 23 15 23 C 10.582 23 7 19.418 7 15 C 7 10.582 10.582 7 15 7 C 17.009 7 18.839141 7.74575 20.244141 8.96875 L 23.085938 6.1289062 C 20.951937 4.1849063 18.116906 3 15.003906 3 z"></path>
|
|
58
|
+
</svg>
|
|
59
|
+
),
|
|
60
|
+
login: LogIn,
|
|
61
|
+
logout: LogOut,
|
|
62
|
+
user: User,
|
|
63
|
+
spinner: Loader2,
|
|
64
|
+
settings: Settings,
|
|
65
|
+
close: X,
|
|
66
|
+
add: Plus,
|
|
67
|
+
edit: Edit,
|
|
68
|
+
save: Save,
|
|
69
|
+
delete: Trash,
|
|
70
|
+
download: Download,
|
|
71
|
+
dashboard: LayoutDashboard,
|
|
72
|
+
task: ClipboardList,
|
|
73
|
+
arrowUp: ArrowUp,
|
|
74
|
+
arrowDown: ArrowDown,
|
|
75
|
+
arrowUpDown: ArrowUpDown,
|
|
76
|
+
eyeOff: EyeOff,
|
|
77
|
+
plusCircle: PlusCircle,
|
|
78
|
+
check: Check,
|
|
79
|
+
chevronsLeft: ChevronsLeft,
|
|
80
|
+
chevronsRight: ChevronsRight,
|
|
81
|
+
chevronRight: ChevronRight,
|
|
82
|
+
mixer: Settings2
|
|
83
|
+
}
|