create-ej-app 1.0.0 → 2.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.
Files changed (198) hide show
  1. package/cli.js +21 -3
  2. package/package.json +1 -1
  3. package/template/{.env.example → advanced/.env.example} +4 -0
  4. package/template/{components.json → advanced/components.json} +1 -1
  5. package/template/{package.json → advanced/package.json} +11 -0
  6. package/template/{prisma → advanced/prisma}/schema.prisma +18 -1
  7. package/template/{src → advanced/src}/app/(app)/tasks/[id]/actions.ts +46 -4
  8. package/template/{src → advanced/src}/app/(app)/tasks/[id]/render.tsx +136 -25
  9. package/template/{src → advanced/src}/app/(app)/tasks/actions.ts +0 -6
  10. package/template/{src → advanced/src}/app/(app)/tasks/page.tsx +0 -3
  11. package/template/{src → advanced/src}/app/(app)/tasks/render.tsx +0 -10
  12. package/template/advanced/src/app/api/upload/route.ts +45 -0
  13. package/template/{src → advanced/src}/app/layout.tsx +1 -1
  14. package/template/{src → advanced/src}/env.mjs +10 -2
  15. package/template/advanced/src/styles/editor.css +61 -0
  16. package/template/{tailwind.config.ts → advanced/tailwind.config.ts} +1 -1
  17. package/template/base/.env.example +16 -0
  18. package/template/base/.eslintrc.cjs +24 -0
  19. package/template/base/components.json +16 -0
  20. package/template/base/next.config.mjs +18 -0
  21. package/template/base/package.json +79 -0
  22. package/template/base/postcss.config.cjs +6 -0
  23. package/template/base/prettier.config.ts +11 -0
  24. package/template/base/prisma/schema.prisma +115 -0
  25. package/template/base/public/next.svg +1 -0
  26. package/template/base/src/app/(app)/dashboard/page.tsx +19 -0
  27. package/template/base/src/app/(app)/layout.tsx +90 -0
  28. package/template/base/src/app/(app)/settings/actions.ts +18 -0
  29. package/template/base/src/app/(app)/settings/loading.tsx +28 -0
  30. package/template/base/src/app/(app)/settings/page.tsx +28 -0
  31. package/template/base/src/app/(app)/settings/render.tsx +113 -0
  32. package/template/base/src/app/(app)/tasks/[id]/actions.ts +149 -0
  33. package/template/base/src/app/(app)/tasks/[id]/page.tsx +25 -0
  34. package/template/base/src/app/(app)/tasks/[id]/render.tsx +413 -0
  35. package/template/base/src/app/(app)/tasks/actions.ts +69 -0
  36. package/template/base/src/app/(app)/tasks/page.tsx +53 -0
  37. package/template/base/src/app/(app)/tasks/render.tsx +107 -0
  38. package/template/base/src/app/(app)/users/[id]/actions.ts +83 -0
  39. package/template/base/src/app/(app)/users/[id]/page.tsx +32 -0
  40. package/template/base/src/app/(app)/users/[id]/render.tsx +240 -0
  41. package/template/base/src/app/(app)/users/actions.ts +55 -0
  42. package/template/base/src/app/(app)/users/page.tsx +49 -0
  43. package/template/base/src/app/(app)/users/render.tsx +80 -0
  44. package/template/base/src/app/(auth)/layout.tsx +20 -0
  45. package/template/base/src/app/(auth)/login/page.tsx +49 -0
  46. package/template/base/src/app/(auth)/login/user-auth-form.tsx +152 -0
  47. package/template/base/src/app/(marketing)/layout.tsx +42 -0
  48. package/template/base/src/app/(marketing)/page.tsx +96 -0
  49. package/template/base/src/app/api/auth/[...nextauth]/route.ts +7 -0
  50. package/template/base/src/app/api/upload/route.ts +45 -0
  51. package/template/base/src/app/favicon.ico +0 -0
  52. package/template/base/src/app/layout.tsx +32 -0
  53. package/template/base/src/components/app-nav-command.tsx +160 -0
  54. package/template/base/src/components/dark-mode-toggle.tsx +43 -0
  55. package/template/base/src/components/data-table/data-table-column-header.tsx +91 -0
  56. package/template/base/src/components/data-table/data-table-faceted-filter.tsx +149 -0
  57. package/template/base/src/components/data-table/data-table-loading.tsx +77 -0
  58. package/template/base/src/components/data-table/data-table-pagination.tsx +94 -0
  59. package/template/base/src/components/data-table/data-table-toolbar.tsx +78 -0
  60. package/template/base/src/components/data-table/data-table-types.ts +17 -0
  61. package/template/base/src/components/data-table/data-table-view-options.tsx +59 -0
  62. package/template/base/src/components/data-table/data-table.tsx +301 -0
  63. package/template/base/src/components/data-table/simple-data-table.tsx +109 -0
  64. package/template/base/src/components/empty-placeholder.tsx +78 -0
  65. package/template/base/src/components/heading.tsx +17 -0
  66. package/template/base/src/components/icons.tsx +83 -0
  67. package/template/base/src/components/main-nav.tsx +64 -0
  68. package/template/base/src/components/mobile-nav.tsx +50 -0
  69. package/template/base/src/components/shell.tsx +13 -0
  70. package/template/base/src/components/side-nav.tsx +51 -0
  71. package/template/base/src/components/site-footer.tsx +31 -0
  72. package/template/base/src/components/system-info.tsx +36 -0
  73. package/template/base/src/components/theme-provider.tsx +13 -0
  74. package/template/base/src/components/ui/alert-dialog.tsx +145 -0
  75. package/template/base/src/components/ui/avatar.tsx +50 -0
  76. package/template/base/src/components/ui/badge.tsx +36 -0
  77. package/template/base/src/components/ui/button.tsx +56 -0
  78. package/template/base/src/components/ui/card.tsx +79 -0
  79. package/template/base/src/components/ui/checkbox.tsx +30 -0
  80. package/template/base/src/components/ui/command.tsx +155 -0
  81. package/template/base/src/components/ui/dialog.tsx +123 -0
  82. package/template/base/src/components/ui/dropdown-menu.tsx +200 -0
  83. package/template/base/src/components/ui/form.tsx +176 -0
  84. package/template/base/src/components/ui/input.tsx +25 -0
  85. package/template/base/src/components/ui/label.tsx +26 -0
  86. package/template/base/src/components/ui/popover.tsx +31 -0
  87. package/template/base/src/components/ui/select.tsx +121 -0
  88. package/template/base/src/components/ui/separator.tsx +31 -0
  89. package/template/base/src/components/ui/skeleton.tsx +15 -0
  90. package/template/base/src/components/ui/table.tsx +114 -0
  91. package/template/base/src/components/ui/textarea.tsx +24 -0
  92. package/template/base/src/components/ui/toast.tsx +127 -0
  93. package/template/base/src/components/ui/toaster.tsx +35 -0
  94. package/template/base/src/components/ui/use-toast.ts +189 -0
  95. package/template/base/src/components/user-account-nav.tsx +60 -0
  96. package/template/base/src/components/user-avatar.tsx +25 -0
  97. package/template/base/src/env.mjs +75 -0
  98. package/template/base/src/hooks/use-debounce.ts +17 -0
  99. package/template/base/src/hooks/use-lock-body.tsx +12 -0
  100. package/template/base/src/lib/auth.ts +79 -0
  101. package/template/base/src/lib/cn.ts +4 -0
  102. package/template/base/src/lib/db.ts +20 -0
  103. package/template/base/src/lib/delay.ts +2 -0
  104. package/template/base/src/lib/formatDate.ts +11 -0
  105. package/template/base/src/lib/generateLabel.ts +5 -0
  106. package/template/base/src/lib/siteConfig.ts +4 -0
  107. package/template/base/src/styles/editor.css +61 -0
  108. package/template/base/src/styles/globals.css +76 -0
  109. package/template/base/src/types/UnwrapPromise.ts +1 -0
  110. package/template/base/tailwind.config.ts +79 -0
  111. package/template/base/tsconfig.json +46 -0
  112. /package/template/{.eslintrc.cjs → advanced/.eslintrc.cjs} +0 -0
  113. /package/template/{next.config.mjs → advanced/next.config.mjs} +0 -0
  114. /package/template/{postcss.config.cjs → advanced/postcss.config.cjs} +0 -0
  115. /package/template/{prettier.config.ts → advanced/prettier.config.ts} +0 -0
  116. /package/template/{public → advanced/public}/next.svg +0 -0
  117. /package/template/{src → advanced/src}/app/(app)/dashboard/page.tsx +0 -0
  118. /package/template/{src → advanced/src}/app/(app)/layout.tsx +0 -0
  119. /package/template/{src → advanced/src}/app/(app)/roles/[id]/actions.ts +0 -0
  120. /package/template/{src → advanced/src}/app/(app)/roles/[id]/page.tsx +0 -0
  121. /package/template/{src → advanced/src}/app/(app)/roles/[id]/render.tsx +0 -0
  122. /package/template/{src → advanced/src}/app/(app)/roles/actions.ts +0 -0
  123. /package/template/{src → advanced/src}/app/(app)/roles/page.tsx +0 -0
  124. /package/template/{src → advanced/src}/app/(app)/roles/render.tsx +0 -0
  125. /package/template/{src → advanced/src}/app/(app)/settings/actions.ts +0 -0
  126. /package/template/{src → advanced/src}/app/(app)/settings/loading.tsx +0 -0
  127. /package/template/{src → advanced/src}/app/(app)/settings/page.tsx +0 -0
  128. /package/template/{src → advanced/src}/app/(app)/settings/render.tsx +0 -0
  129. /package/template/{src → advanced/src}/app/(app)/tasks/[id]/page.tsx +0 -0
  130. /package/template/{src → advanced/src}/app/(app)/users/[id]/actions.ts +0 -0
  131. /package/template/{src → advanced/src}/app/(app)/users/[id]/page.tsx +0 -0
  132. /package/template/{src → advanced/src}/app/(app)/users/[id]/render.tsx +0 -0
  133. /package/template/{src → advanced/src}/app/(app)/users/actions.ts +0 -0
  134. /package/template/{src → advanced/src}/app/(app)/users/page.tsx +0 -0
  135. /package/template/{src → advanced/src}/app/(app)/users/render.tsx +0 -0
  136. /package/template/{src → advanced/src}/app/(auth)/layout.tsx +0 -0
  137. /package/template/{src → advanced/src}/app/(auth)/login/page.tsx +0 -0
  138. /package/template/{src → advanced/src}/app/(auth)/login/user-auth-form.tsx +0 -0
  139. /package/template/{src → advanced/src}/app/(marketing)/layout.tsx +0 -0
  140. /package/template/{src → advanced/src}/app/(marketing)/page.tsx +0 -0
  141. /package/template/{src → advanced/src}/app/api/auth/[...nextauth]/route.ts +0 -0
  142. /package/template/{src → advanced/src}/app/favicon.ico +0 -0
  143. /package/template/{src → advanced/src}/components/app-nav-command.tsx +0 -0
  144. /package/template/{src → advanced/src}/components/dark-mode-toggle.tsx +0 -0
  145. /package/template/{src → advanced/src}/components/data-table/data-table-column-header.tsx +0 -0
  146. /package/template/{src → advanced/src}/components/data-table/data-table-faceted-filter.tsx +0 -0
  147. /package/template/{src → advanced/src}/components/data-table/data-table-loading.tsx +0 -0
  148. /package/template/{src → advanced/src}/components/data-table/data-table-pagination.tsx +0 -0
  149. /package/template/{src → advanced/src}/components/data-table/data-table-toolbar.tsx +0 -0
  150. /package/template/{src → advanced/src}/components/data-table/data-table-types.ts +0 -0
  151. /package/template/{src → advanced/src}/components/data-table/data-table-view-options.tsx +0 -0
  152. /package/template/{src → advanced/src}/components/data-table/data-table.tsx +0 -0
  153. /package/template/{src → advanced/src}/components/data-table/simple-data-table.tsx +0 -0
  154. /package/template/{src → advanced/src}/components/empty-placeholder.tsx +0 -0
  155. /package/template/{src → advanced/src}/components/heading.tsx +0 -0
  156. /package/template/{src → advanced/src}/components/icons.tsx +0 -0
  157. /package/template/{src → advanced/src}/components/main-nav.tsx +0 -0
  158. /package/template/{src → advanced/src}/components/mobile-nav.tsx +0 -0
  159. /package/template/{src → advanced/src}/components/shell.tsx +0 -0
  160. /package/template/{src → advanced/src}/components/side-nav.tsx +0 -0
  161. /package/template/{src → advanced/src}/components/site-footer.tsx +0 -0
  162. /package/template/{src → advanced/src}/components/system-info.tsx +0 -0
  163. /package/template/{src → advanced/src}/components/theme-provider.tsx +0 -0
  164. /package/template/{src → advanced/src}/components/ui/alert-dialog.tsx +0 -0
  165. /package/template/{src → advanced/src}/components/ui/avatar.tsx +0 -0
  166. /package/template/{src → advanced/src}/components/ui/badge.tsx +0 -0
  167. /package/template/{src → advanced/src}/components/ui/button.tsx +0 -0
  168. /package/template/{src → advanced/src}/components/ui/card.tsx +0 -0
  169. /package/template/{src → advanced/src}/components/ui/checkbox.tsx +0 -0
  170. /package/template/{src → advanced/src}/components/ui/command.tsx +0 -0
  171. /package/template/{src → advanced/src}/components/ui/dialog.tsx +0 -0
  172. /package/template/{src → advanced/src}/components/ui/dropdown-menu.tsx +0 -0
  173. /package/template/{src → advanced/src}/components/ui/form.tsx +0 -0
  174. /package/template/{src → advanced/src}/components/ui/input.tsx +0 -0
  175. /package/template/{src → advanced/src}/components/ui/label.tsx +0 -0
  176. /package/template/{src → advanced/src}/components/ui/popover.tsx +0 -0
  177. /package/template/{src → advanced/src}/components/ui/select.tsx +0 -0
  178. /package/template/{src → advanced/src}/components/ui/separator.tsx +0 -0
  179. /package/template/{src → advanced/src}/components/ui/skeleton.tsx +0 -0
  180. /package/template/{src → advanced/src}/components/ui/table.tsx +0 -0
  181. /package/template/{src → advanced/src}/components/ui/textarea.tsx +0 -0
  182. /package/template/{src → advanced/src}/components/ui/toast.tsx +0 -0
  183. /package/template/{src → advanced/src}/components/ui/toaster.tsx +0 -0
  184. /package/template/{src → advanced/src}/components/ui/use-toast.ts +0 -0
  185. /package/template/{src → advanced/src}/components/user-account-nav.tsx +0 -0
  186. /package/template/{src → advanced/src}/components/user-avatar.tsx +0 -0
  187. /package/template/{src → advanced/src}/hooks/use-debounce.ts +0 -0
  188. /package/template/{src → advanced/src}/hooks/use-lock-body.tsx +0 -0
  189. /package/template/{src → advanced/src}/lib/auth.ts +0 -0
  190. /package/template/{src → advanced/src}/lib/cn.ts +0 -0
  191. /package/template/{src → advanced/src}/lib/db.ts +0 -0
  192. /package/template/{src → advanced/src}/lib/delay.ts +0 -0
  193. /package/template/{src → advanced/src}/lib/formatDate.ts +0 -0
  194. /package/template/{src → advanced/src}/lib/generateLabel.ts +0 -0
  195. /package/template/{src → advanced/src}/lib/siteConfig.ts +0 -0
  196. /package/template/{src/app → advanced/src/styles}/globals.css +0 -0
  197. /package/template/{src → advanced/src}/types/UnwrapPromise.ts +0 -0
  198. /package/template/{tsconfig.json → advanced/tsconfig.json} +0 -0
@@ -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
+ }
@@ -0,0 +1,64 @@
1
+ 'use client'
2
+
3
+ import Link from 'next/link'
4
+ import { useSelectedLayoutSegment } from 'next/navigation'
5
+ import { FC, useState } from 'react'
6
+
7
+ import { Icons } from '@/components/icons'
8
+ import { MobileNav } from '@/components/mobile-nav'
9
+ import { cn } from '@/lib/cn'
10
+ import { siteConfig } from '@/lib/siteConfig'
11
+
12
+ type MainNavItem = {
13
+ title: string
14
+ href: string
15
+ disabled?: boolean
16
+ }
17
+
18
+ type MainNavProps = {
19
+ logoLink?: string
20
+ items?: MainNavItem[]
21
+ }
22
+
23
+ export const MainNav: FC<MainNavProps> = ({ logoLink = '/', items }) => {
24
+ const [showMobileMenu, setShowMobileMenu] = useState(false)
25
+ const segment = useSelectedLayoutSegment()
26
+
27
+ return (
28
+ <div className="flex md:gap-10">
29
+ <Link href={logoLink} className="hidden items-center space-x-2 md:flex">
30
+ <Icons.logo />
31
+ <span className="hidden font-bold sm:inline-block">
32
+ {siteConfig.name}
33
+ </span>
34
+ </Link>
35
+ {items?.length ? (
36
+ <nav className="hidden gap-6 md:flex">
37
+ {items?.map((item, i) => (
38
+ <Link
39
+ key={i}
40
+ href={item.disabled ? '#' : item.href}
41
+ className={cn(
42
+ 'flex items-center text-lg font-medium transition-colors hover:text-foreground/80 sm:text-sm',
43
+ item.href.startsWith(`/${segment}`)
44
+ ? 'text-foreground'
45
+ : 'text-foreground/60',
46
+ item.disabled && 'cursor-not-allowed opacity-80'
47
+ )}
48
+ >
49
+ {item.title}
50
+ </Link>
51
+ ))}
52
+ </nav>
53
+ ) : undefined}
54
+ <button
55
+ className="flex items-center space-x-2 md:hidden"
56
+ onClick={() => setShowMobileMenu(prev => !prev)}
57
+ >
58
+ {showMobileMenu ? <Icons.close /> : <Icons.logo />}
59
+ <span className="font-bold">Menu</span>
60
+ </button>
61
+ {showMobileMenu && items?.length && <MobileNav items={items} />}
62
+ </div>
63
+ )
64
+ }
@@ -0,0 +1,50 @@
1
+ 'use client'
2
+
3
+ import Link from 'next/link'
4
+
5
+ import { Icons } from '@/components/icons'
6
+ import { useLockBody } from '@/hooks/use-lock-body'
7
+ import { cn } from '@/lib/cn'
8
+ import { siteConfig } from '@/lib/siteConfig'
9
+ import { FC } from 'react'
10
+
11
+ interface MobileNavProps {
12
+ items: {
13
+ title: string
14
+ href: string
15
+ disabled?: boolean
16
+ }[]
17
+ }
18
+
19
+ export const MobileNav: FC<MobileNavProps> = ({ items }) => {
20
+ useLockBody()
21
+
22
+ return (
23
+ <div
24
+ className={cn(
25
+ 'fixed inset-0 top-16 z-50 grid h-[calc(100vh-4rem)] grid-flow-row auto-rows-max overflow-auto p-6 pb-32 shadow-md animate-in slide-in-from-bottom-80 md:hidden'
26
+ )}
27
+ >
28
+ <div className="relative z-20 grid gap-6 rounded-md bg-popover p-4 text-popover-foreground shadow-md">
29
+ <Link href="/" className="flex items-center space-x-2">
30
+ <Icons.logo />
31
+ <span className="font-bold">{siteConfig.name}</span>
32
+ </Link>
33
+ <nav className="grid grid-flow-row auto-rows-max text-sm">
34
+ {items.map((item, index) => (
35
+ <Link
36
+ key={index}
37
+ href={item.disabled ? '#' : item.href}
38
+ className={cn(
39
+ 'flex w-full items-center rounded-md p-2 text-sm font-medium hover:underline',
40
+ item.disabled && 'cursor-not-allowed opacity-60'
41
+ )}
42
+ >
43
+ {item.title}
44
+ </Link>
45
+ ))}
46
+ </nav>
47
+ </div>
48
+ </div>
49
+ )
50
+ }