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
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { type ColumnDef } from '@tanstack/react-table'
|
|
2
|
+
import { MoreHorizontal } from 'lucide-react'
|
|
3
|
+
|
|
4
|
+
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
|
5
|
+
import { Badge } from '@/components/ui/badge'
|
|
6
|
+
import { Button } from '@/components/ui/button'
|
|
7
|
+
import { Checkbox } from '@/components/ui/checkbox'
|
|
8
|
+
import {
|
|
9
|
+
DropdownMenu,
|
|
10
|
+
DropdownMenuContent,
|
|
11
|
+
DropdownMenuItem,
|
|
12
|
+
DropdownMenuLabel,
|
|
13
|
+
DropdownMenuSeparator,
|
|
14
|
+
DropdownMenuTrigger,
|
|
15
|
+
} from '@/components/ui/dropdown-menu'
|
|
16
|
+
import { DataTableColumnHeader } from '@/components/data-table/data-table-column-header'
|
|
17
|
+
import type { User } from '../types/user'
|
|
18
|
+
|
|
19
|
+
/** Get initials from a full name for avatar fallback */
|
|
20
|
+
function getInitials(name: string): string {
|
|
21
|
+
return name
|
|
22
|
+
.split(' ')
|
|
23
|
+
.map((n) => n[0])
|
|
24
|
+
.slice(0, 2)
|
|
25
|
+
.join('')
|
|
26
|
+
.toUpperCase()
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Map role value to badge variant */
|
|
30
|
+
const roleBadgeVariant: Record<User['role'], 'default' | 'secondary' | 'outline'> = {
|
|
31
|
+
admin: 'default',
|
|
32
|
+
manager: 'secondary',
|
|
33
|
+
user: 'outline',
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Map status value to badge variant */
|
|
37
|
+
const statusBadgeVariant: Record<User['status'], 'success' | 'destructive' | 'warning'> = {
|
|
38
|
+
active: 'success',
|
|
39
|
+
inactive: 'destructive',
|
|
40
|
+
pending: 'warning',
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface GetColumnsOptions {
|
|
44
|
+
onEdit: (user: User) => void
|
|
45
|
+
onDelete: (user: User) => void
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function getUsersTableColumns({ onEdit, onDelete }: GetColumnsOptions): ColumnDef<User>[] {
|
|
49
|
+
return [
|
|
50
|
+
{
|
|
51
|
+
id: 'select',
|
|
52
|
+
header: ({ table }) => (
|
|
53
|
+
<Checkbox
|
|
54
|
+
checked={table.getIsAllPageRowsSelected() || (table.getIsSomePageRowsSelected() && 'indeterminate')}
|
|
55
|
+
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
|
|
56
|
+
aria-label='Select all'
|
|
57
|
+
/>
|
|
58
|
+
),
|
|
59
|
+
cell: ({ row }) => (
|
|
60
|
+
<Checkbox
|
|
61
|
+
checked={row.getIsSelected()}
|
|
62
|
+
onCheckedChange={(value) => row.toggleSelected(!!value)}
|
|
63
|
+
aria-label='Select row'
|
|
64
|
+
/>
|
|
65
|
+
),
|
|
66
|
+
enableSorting: false,
|
|
67
|
+
enableHiding: false,
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
accessorKey: 'name',
|
|
71
|
+
header: ({ column }) => <DataTableColumnHeader column={column} title='Name' />,
|
|
72
|
+
cell: ({ row }) => {
|
|
73
|
+
const user = row.original
|
|
74
|
+
return (
|
|
75
|
+
<div className='flex items-center gap-3'>
|
|
76
|
+
<Avatar className='h-8 w-8'>
|
|
77
|
+
{user.avatar && <AvatarImage src={user.avatar} alt={user.name} />}
|
|
78
|
+
<AvatarFallback className='text-xs'>{getInitials(user.name)}</AvatarFallback>
|
|
79
|
+
</Avatar>
|
|
80
|
+
<span className='font-medium'>{user.name}</span>
|
|
81
|
+
</div>
|
|
82
|
+
)
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
accessorKey: 'email',
|
|
87
|
+
header: ({ column }) => <DataTableColumnHeader column={column} title='Email' />,
|
|
88
|
+
cell: ({ row }) => <span className='text-muted-foreground'>{row.getValue('email')}</span>,
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
accessorKey: 'role',
|
|
92
|
+
header: ({ column }) => <DataTableColumnHeader column={column} title='Role' />,
|
|
93
|
+
cell: ({ row }) => {
|
|
94
|
+
const role = row.getValue<User['role']>('role')
|
|
95
|
+
return (
|
|
96
|
+
<Badge variant={roleBadgeVariant[role]} className='capitalize'>
|
|
97
|
+
{role}
|
|
98
|
+
</Badge>
|
|
99
|
+
)
|
|
100
|
+
},
|
|
101
|
+
filterFn: 'arrIncludesSome',
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
accessorKey: 'status',
|
|
105
|
+
header: ({ column }) => <DataTableColumnHeader column={column} title='Status' />,
|
|
106
|
+
cell: ({ row }) => {
|
|
107
|
+
const status = row.getValue<User['status']>('status')
|
|
108
|
+
return (
|
|
109
|
+
<Badge variant={statusBadgeVariant[status]} className='capitalize'>
|
|
110
|
+
{status}
|
|
111
|
+
</Badge>
|
|
112
|
+
)
|
|
113
|
+
},
|
|
114
|
+
filterFn: 'arrIncludesSome',
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
accessorKey: 'createdAt',
|
|
118
|
+
header: ({ column }) => <DataTableColumnHeader column={column} title='Created At' />,
|
|
119
|
+
cell: ({ row }) => (
|
|
120
|
+
<span className='text-muted-foreground'>
|
|
121
|
+
{new Date(row.getValue<string>('createdAt')).toLocaleDateString()}
|
|
122
|
+
</span>
|
|
123
|
+
),
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
id: 'actions',
|
|
127
|
+
enableHiding: false,
|
|
128
|
+
cell: ({ row }) => {
|
|
129
|
+
const user = row.original
|
|
130
|
+
return (
|
|
131
|
+
<DropdownMenu>
|
|
132
|
+
<DropdownMenuTrigger asChild>
|
|
133
|
+
<Button variant='ghost' className='h-8 w-8 p-0'>
|
|
134
|
+
<span className='sr-only'>Open menu</span>
|
|
135
|
+
<MoreHorizontal className='h-4 w-4' />
|
|
136
|
+
</Button>
|
|
137
|
+
</DropdownMenuTrigger>
|
|
138
|
+
<DropdownMenuContent align='end'>
|
|
139
|
+
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
|
140
|
+
<DropdownMenuSeparator />
|
|
141
|
+
<DropdownMenuItem onClick={() => onEdit(user)}>Edit</DropdownMenuItem>
|
|
142
|
+
<DropdownMenuItem
|
|
143
|
+
onClick={() => onDelete(user)}
|
|
144
|
+
className='text-destructive focus:text-destructive'
|
|
145
|
+
>
|
|
146
|
+
Delete
|
|
147
|
+
</DropdownMenuItem>
|
|
148
|
+
</DropdownMenuContent>
|
|
149
|
+
</DropdownMenu>
|
|
150
|
+
)
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
]
|
|
154
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
import {
|
|
3
|
+
getCoreRowModel,
|
|
4
|
+
getFacetedRowModel,
|
|
5
|
+
getFacetedUniqueValues,
|
|
6
|
+
getFilteredRowModel,
|
|
7
|
+
getPaginationRowModel,
|
|
8
|
+
getSortedRowModel,
|
|
9
|
+
useReactTable,
|
|
10
|
+
type ColumnFiltersState,
|
|
11
|
+
type SortingState,
|
|
12
|
+
type VisibilityState,
|
|
13
|
+
} from '@tanstack/react-table'
|
|
14
|
+
import { PlusCircle } from 'lucide-react'
|
|
15
|
+
|
|
16
|
+
import { Button } from '@/components/ui/button'
|
|
17
|
+
import { DataTable } from '@/components/data-table/data-table'
|
|
18
|
+
import { DataTableToolbar } from '@/components/data-table/data-table-toolbar'
|
|
19
|
+
import { mockUsers } from '../data/users-mock-data'
|
|
20
|
+
import { getUsersTableColumns } from './users-table-columns'
|
|
21
|
+
import { UserFormDialog } from './user-form-dialog'
|
|
22
|
+
import { UserDeleteConfirmationDialog } from './user-delete-confirmation-dialog'
|
|
23
|
+
import type { User } from '../types/user'
|
|
24
|
+
|
|
25
|
+
/** Filter options for role and status faceted filters */
|
|
26
|
+
const roleFilterOptions = [
|
|
27
|
+
{ label: 'Admin', value: 'admin' },
|
|
28
|
+
{ label: 'Manager', value: 'manager' },
|
|
29
|
+
{ label: 'User', value: 'user' },
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
const statusFilterOptions = [
|
|
33
|
+
{ label: 'Active', value: 'active' },
|
|
34
|
+
{ label: 'Inactive', value: 'inactive' },
|
|
35
|
+
{ label: 'Pending', value: 'pending' },
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
const toolbarFilters = [
|
|
39
|
+
{ columnId: 'role', title: 'Role', options: roleFilterOptions },
|
|
40
|
+
{ columnId: 'status', title: 'Status', options: statusFilterOptions },
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
export function UsersTable() {
|
|
44
|
+
const [data, setData] = useState<User[]>(mockUsers)
|
|
45
|
+
const [sorting, setSorting] = useState<SortingState>([])
|
|
46
|
+
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([])
|
|
47
|
+
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({})
|
|
48
|
+
const [rowSelection, setRowSelection] = useState({})
|
|
49
|
+
|
|
50
|
+
// Dialog state
|
|
51
|
+
const [formDialogOpen, setFormDialogOpen] = useState(false)
|
|
52
|
+
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false)
|
|
53
|
+
const [selectedUser, setSelectedUser] = useState<User | null>(null)
|
|
54
|
+
|
|
55
|
+
const handleEdit = (user: User) => {
|
|
56
|
+
setSelectedUser(user)
|
|
57
|
+
setFormDialogOpen(true)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const handleDelete = (user: User) => {
|
|
61
|
+
setSelectedUser(user)
|
|
62
|
+
setDeleteDialogOpen(true)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const handleAddNew = () => {
|
|
66
|
+
setSelectedUser(null)
|
|
67
|
+
setFormDialogOpen(true)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const handleFormSubmit = (values: Omit<User, 'id' | 'createdAt'>) => {
|
|
71
|
+
if (selectedUser) {
|
|
72
|
+
// Edit existing user
|
|
73
|
+
setData((prev) => prev.map((u) => (u.id === selectedUser.id ? { ...u, ...values } : u)))
|
|
74
|
+
} else {
|
|
75
|
+
// Add new user
|
|
76
|
+
const newUser: User = {
|
|
77
|
+
...values,
|
|
78
|
+
id: String(Date.now()),
|
|
79
|
+
createdAt: new Date().toISOString().split('T')[0],
|
|
80
|
+
}
|
|
81
|
+
setData((prev) => [newUser, ...prev])
|
|
82
|
+
}
|
|
83
|
+
setFormDialogOpen(false)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const handleConfirmDelete = () => {
|
|
87
|
+
if (selectedUser) {
|
|
88
|
+
setData((prev) => prev.filter((u) => u.id !== selectedUser.id))
|
|
89
|
+
}
|
|
90
|
+
setDeleteDialogOpen(false)
|
|
91
|
+
setSelectedUser(null)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const columns = getUsersTableColumns({ onEdit: handleEdit, onDelete: handleDelete })
|
|
95
|
+
|
|
96
|
+
const table = useReactTable({
|
|
97
|
+
data,
|
|
98
|
+
columns,
|
|
99
|
+
state: { sorting, columnFilters, columnVisibility, rowSelection },
|
|
100
|
+
onSortingChange: setSorting,
|
|
101
|
+
onColumnFiltersChange: setColumnFilters,
|
|
102
|
+
onColumnVisibilityChange: setColumnVisibility,
|
|
103
|
+
onRowSelectionChange: setRowSelection,
|
|
104
|
+
getCoreRowModel: getCoreRowModel(),
|
|
105
|
+
getFilteredRowModel: getFilteredRowModel(),
|
|
106
|
+
getSortedRowModel: getSortedRowModel(),
|
|
107
|
+
getPaginationRowModel: getPaginationRowModel(),
|
|
108
|
+
getFacetedRowModel: getFacetedRowModel(),
|
|
109
|
+
getFacetedUniqueValues: getFacetedUniqueValues(),
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
return (
|
|
113
|
+
<div className='space-y-4'>
|
|
114
|
+
<div className='flex items-center justify-between'>
|
|
115
|
+
<DataTableToolbar
|
|
116
|
+
table={table}
|
|
117
|
+
searchColumn='name'
|
|
118
|
+
searchPlaceholder='Search users...'
|
|
119
|
+
filters={toolbarFilters}
|
|
120
|
+
/>
|
|
121
|
+
<Button size='sm' className='ml-4' onClick={handleAddNew}>
|
|
122
|
+
<PlusCircle className='mr-2 h-4 w-4' />
|
|
123
|
+
Add User
|
|
124
|
+
</Button>
|
|
125
|
+
</div>
|
|
126
|
+
<DataTable table={table} columns={columns} />
|
|
127
|
+
|
|
128
|
+
<UserFormDialog
|
|
129
|
+
open={formDialogOpen}
|
|
130
|
+
onOpenChange={setFormDialogOpen}
|
|
131
|
+
user={selectedUser}
|
|
132
|
+
onSubmit={handleFormSubmit}
|
|
133
|
+
/>
|
|
134
|
+
|
|
135
|
+
<UserDeleteConfirmationDialog
|
|
136
|
+
open={deleteDialogOpen}
|
|
137
|
+
onOpenChange={setDeleteDialogOpen}
|
|
138
|
+
user={selectedUser}
|
|
139
|
+
onConfirm={handleConfirmDelete}
|
|
140
|
+
/>
|
|
141
|
+
</div>
|
|
142
|
+
)
|
|
143
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { User } from '../types/user'
|
|
2
|
+
|
|
3
|
+
/** 50 mock users covering all roles (admin/manager/user) and statuses (active/inactive/pending) */
|
|
4
|
+
export const mockUsers: User[] = [
|
|
5
|
+
{ id: '1', name: 'Alice Johnson', email: 'alice.johnson@example.com', role: 'admin', status: 'active', createdAt: '2023-01-15' },
|
|
6
|
+
{ id: '2', name: 'Bob Smith', email: 'bob.smith@example.com', role: 'manager', status: 'active', createdAt: '2023-01-20' },
|
|
7
|
+
{ id: '3', name: 'Carol Williams', email: 'carol.williams@example.com', role: 'user', status: 'active', createdAt: '2023-02-05' },
|
|
8
|
+
{ id: '4', name: 'David Brown', email: 'david.brown@example.com', role: 'user', status: 'inactive', createdAt: '2023-02-10' },
|
|
9
|
+
{ id: '5', name: 'Eva Martinez', email: 'eva.martinez@example.com', role: 'manager', status: 'pending', createdAt: '2023-02-18' },
|
|
10
|
+
{ id: '6', name: 'Frank Davis', email: 'frank.davis@example.com', role: 'user', status: 'active', createdAt: '2023-03-01' },
|
|
11
|
+
{ id: '7', name: 'Grace Wilson', email: 'grace.wilson@example.com', role: 'admin', status: 'active', createdAt: '2023-03-12' },
|
|
12
|
+
{ id: '8', name: 'Henry Taylor', email: 'henry.taylor@example.com', role: 'user', status: 'pending', createdAt: '2023-03-20' },
|
|
13
|
+
{ id: '9', name: 'Iris Anderson', email: 'iris.anderson@example.com', role: 'manager', status: 'active', createdAt: '2023-04-02' },
|
|
14
|
+
{ id: '10', name: 'Jack Thomas', email: 'jack.thomas@example.com', role: 'user', status: 'inactive', createdAt: '2023-04-15' },
|
|
15
|
+
{ id: '11', name: 'Karen Jackson', email: 'karen.jackson@example.com', role: 'user', status: 'active', createdAt: '2023-04-22' },
|
|
16
|
+
{ id: '12', name: 'Leo White', email: 'leo.white@example.com', role: 'manager', status: 'inactive', createdAt: '2023-05-03' },
|
|
17
|
+
{ id: '13', name: 'Mia Harris', email: 'mia.harris@example.com', role: 'user', status: 'active', createdAt: '2023-05-14' },
|
|
18
|
+
{ id: '14', name: 'Nathan Martin', email: 'nathan.martin@example.com', role: 'admin', status: 'active', createdAt: '2023-05-25' },
|
|
19
|
+
{ id: '15', name: 'Olivia Garcia', email: 'olivia.garcia@example.com', role: 'user', status: 'pending', createdAt: '2023-06-08' },
|
|
20
|
+
{ id: '16', name: 'Paul Rodriguez', email: 'paul.rodriguez@example.com', role: 'user', status: 'active', createdAt: '2023-06-15' },
|
|
21
|
+
{ id: '17', name: 'Quinn Lewis', email: 'quinn.lewis@example.com', role: 'manager', status: 'active', createdAt: '2023-06-28' },
|
|
22
|
+
{ id: '18', name: 'Rachel Lee', email: 'rachel.lee@example.com', role: 'user', status: 'inactive', createdAt: '2023-07-05' },
|
|
23
|
+
{ id: '19', name: 'Sam Walker', email: 'sam.walker@example.com', role: 'user', status: 'active', createdAt: '2023-07-18' },
|
|
24
|
+
{ id: '20', name: 'Tina Hall', email: 'tina.hall@example.com', role: 'admin', status: 'pending', createdAt: '2023-07-30' },
|
|
25
|
+
{ id: '21', name: 'Uma Allen', email: 'uma.allen@example.com', role: 'user', status: 'active', createdAt: '2023-08-10' },
|
|
26
|
+
{ id: '22', name: 'Victor Young', email: 'victor.young@example.com', role: 'manager', status: 'active', createdAt: '2023-08-22' },
|
|
27
|
+
{ id: '23', name: 'Wendy Hernandez', email: 'wendy.hernandez@example.com', role: 'user', status: 'active', createdAt: '2023-09-01' },
|
|
28
|
+
{ id: '24', name: 'Xander King', email: 'xander.king@example.com', role: 'user', status: 'inactive', createdAt: '2023-09-14' },
|
|
29
|
+
{ id: '25', name: 'Yara Wright', email: 'yara.wright@example.com', role: 'manager', status: 'pending', createdAt: '2023-09-25' },
|
|
30
|
+
{ id: '26', name: 'Zoe Lopez', email: 'zoe.lopez@example.com', role: 'user', status: 'active', createdAt: '2023-10-05' },
|
|
31
|
+
{ id: '27', name: 'Aaron Hill', email: 'aaron.hill@example.com', role: 'admin', status: 'active', createdAt: '2023-10-18' },
|
|
32
|
+
{ id: '28', name: 'Bella Scott', email: 'bella.scott@example.com', role: 'user', status: 'active', createdAt: '2023-10-30' },
|
|
33
|
+
{ id: '29', name: 'Carlos Green', email: 'carlos.green@example.com', role: 'user', status: 'pending', createdAt: '2023-11-08' },
|
|
34
|
+
{ id: '30', name: 'Diana Adams', email: 'diana.adams@example.com', role: 'manager', status: 'active', createdAt: '2023-11-20' },
|
|
35
|
+
{ id: '31', name: 'Ethan Baker', email: 'ethan.baker@example.com', role: 'user', status: 'inactive', createdAt: '2023-12-01' },
|
|
36
|
+
{ id: '32', name: 'Fiona Gonzalez', email: 'fiona.gonzalez@example.com', role: 'user', status: 'active', createdAt: '2023-12-12' },
|
|
37
|
+
{ id: '33', name: 'George Nelson', email: 'george.nelson@example.com', role: 'manager', status: 'active', createdAt: '2023-12-24' },
|
|
38
|
+
{ id: '34', name: 'Hannah Carter', email: 'hannah.carter@example.com', role: 'admin', status: 'active', createdAt: '2024-01-05' },
|
|
39
|
+
{ id: '35', name: 'Ian Mitchell', email: 'ian.mitchell@example.com', role: 'user', status: 'pending', createdAt: '2024-01-18' },
|
|
40
|
+
{ id: '36', name: 'Julia Perez', email: 'julia.perez@example.com', role: 'user', status: 'active', createdAt: '2024-01-30' },
|
|
41
|
+
{ id: '37', name: 'Kevin Roberts', email: 'kevin.roberts@example.com', role: 'manager', status: 'inactive', createdAt: '2024-02-10' },
|
|
42
|
+
{ id: '38', name: 'Laura Turner', email: 'laura.turner@example.com', role: 'user', status: 'active', createdAt: '2024-02-22' },
|
|
43
|
+
{ id: '39', name: 'Mike Phillips', email: 'mike.phillips@example.com', role: 'user', status: 'active', createdAt: '2024-03-05' },
|
|
44
|
+
{ id: '40', name: 'Nina Campbell', email: 'nina.campbell@example.com', role: 'admin', status: 'active', createdAt: '2024-03-15' },
|
|
45
|
+
{ id: '41', name: 'Oscar Parker', email: 'oscar.parker@example.com', role: 'user', status: 'inactive', createdAt: '2024-03-28' },
|
|
46
|
+
{ id: '42', name: 'Penny Evans', email: 'penny.evans@example.com', role: 'manager', status: 'active', createdAt: '2024-04-08' },
|
|
47
|
+
{ id: '43', name: 'Rick Edwards', email: 'rick.edwards@example.com', role: 'user', status: 'pending', createdAt: '2024-04-20' },
|
|
48
|
+
{ id: '44', name: 'Sara Collins', email: 'sara.collins@example.com', role: 'user', status: 'active', createdAt: '2024-05-01' },
|
|
49
|
+
{ id: '45', name: 'Tom Stewart', email: 'tom.stewart@example.com', role: 'manager', status: 'active', createdAt: '2024-05-14' },
|
|
50
|
+
{ id: '46', name: 'Ursula Morris', email: 'ursula.morris@example.com', role: 'user', status: 'active', createdAt: '2024-05-25' },
|
|
51
|
+
{ id: '47', name: 'Vince Rogers', email: 'vince.rogers@example.com', role: 'admin', status: 'inactive', createdAt: '2024-06-05' },
|
|
52
|
+
{ id: '48', name: 'Wanda Reed', email: 'wanda.reed@example.com', role: 'user', status: 'active', createdAt: '2024-06-18' },
|
|
53
|
+
{ id: '49', name: 'Xavier Cook', email: 'xavier.cook@example.com', role: 'user', status: 'pending', createdAt: '2024-06-28' },
|
|
54
|
+
{ id: '50', name: 'Yvonne Morgan', email: 'yvonne.morgan@example.com', role: 'manager', status: 'active', createdAt: '2024-07-10' },
|
|
55
|
+
]
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
|
|
3
|
+
export const userFormSchema = z.object({
|
|
4
|
+
name: z.string().min(2, 'Name must be at least 2 characters').max(100, 'Name must be at most 100 characters'),
|
|
5
|
+
email: z.string().email('Please enter a valid email address'),
|
|
6
|
+
role: z.enum(['admin', 'manager', 'user'], {
|
|
7
|
+
required_error: 'Please select a role',
|
|
8
|
+
}),
|
|
9
|
+
status: z.enum(['active', 'inactive', 'pending'], {
|
|
10
|
+
required_error: 'Please select a status',
|
|
11
|
+
}),
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
export type UserFormValues = z.infer<typeof userFormSchema>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export type UserRole = 'admin' | 'manager' | 'user'
|
|
2
|
+
export type UserStatus = 'active' | 'inactive' | 'pending'
|
|
3
|
+
|
|
4
|
+
export interface User {
|
|
5
|
+
id: string
|
|
6
|
+
name: string
|
|
7
|
+
email: string
|
|
8
|
+
role: UserRole
|
|
9
|
+
status: UserStatus
|
|
10
|
+
createdAt: string
|
|
11
|
+
avatar?: string
|
|
12
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import * as Sentry from '@sentry/react'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Initialize Sentry error tracking.
|
|
5
|
+
* Only runs if VITE_SENTRY_DSN is set.
|
|
6
|
+
*/
|
|
7
|
+
export function initSentry() {
|
|
8
|
+
const dsn = import.meta.env.VITE_SENTRY_DSN
|
|
9
|
+
if (!dsn) return
|
|
10
|
+
|
|
11
|
+
Sentry.init({
|
|
12
|
+
dsn,
|
|
13
|
+
environment: import.meta.env.MODE,
|
|
14
|
+
// Capture 10% of transactions for performance monitoring
|
|
15
|
+
tracesSampleRate: import.meta.env.PROD ? 0.1 : 1.0,
|
|
16
|
+
// Only send errors in production and staging
|
|
17
|
+
enabled: import.meta.env.PROD || import.meta.env.MODE === 'staging',
|
|
18
|
+
integrations: [
|
|
19
|
+
Sentry.browserTracingIntegration(),
|
|
20
|
+
Sentry.replayIntegration({
|
|
21
|
+
maskAllText: false,
|
|
22
|
+
blockAllMedia: false,
|
|
23
|
+
}),
|
|
24
|
+
],
|
|
25
|
+
replaysSessionSampleRate: 0.1,
|
|
26
|
+
replaysOnErrorSampleRate: 1.0,
|
|
27
|
+
})
|
|
28
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { initSentry } from '@/lib/sentry'
|
|
1
2
|
import '@/lib/i18n'
|
|
2
3
|
import { StrictMode } from 'react'
|
|
3
4
|
import { createRoot } from 'react-dom/client'
|
|
@@ -11,6 +12,8 @@ import { queryClient } from './lib/query-client'
|
|
|
11
12
|
import { ThemeProvider } from './context/theme-provider'
|
|
12
13
|
import './styles/globals.css'
|
|
13
14
|
|
|
15
|
+
initSentry()
|
|
16
|
+
|
|
14
17
|
const router = createRouter({
|
|
15
18
|
routeTree,
|
|
16
19
|
context: { queryClient },
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { createFileRoute } from '@tanstack/react-router'
|
|
2
2
|
|
|
3
3
|
import { PageLayout } from '@/components/layout/page-layout'
|
|
4
|
+
import { OverviewChart } from '@/features/dashboard/components/overview-chart'
|
|
5
|
+
import { RecentActivity } from '@/features/dashboard/components/recent-activity'
|
|
6
|
+
import { StatsCards } from '@/features/dashboard/components/stats-cards'
|
|
4
7
|
|
|
5
8
|
export const Route = createFileRoute('/_authenticated/dashboard')({
|
|
6
9
|
component: DashboardPage,
|
|
@@ -9,13 +12,16 @@ export const Route = createFileRoute('/_authenticated/dashboard')({
|
|
|
9
12
|
function DashboardPage() {
|
|
10
13
|
return (
|
|
11
14
|
<PageLayout title='Dashboard' description='Welcome to your portal'>
|
|
12
|
-
<div className='
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
<
|
|
15
|
+
<div className='space-y-6'>
|
|
16
|
+
<StatsCards />
|
|
17
|
+
<div className='grid gap-6 lg:grid-cols-7'>
|
|
18
|
+
<div className='lg:col-span-4'>
|
|
19
|
+
<OverviewChart />
|
|
17
20
|
</div>
|
|
18
|
-
|
|
21
|
+
<div className='lg:col-span-3'>
|
|
22
|
+
<RecentActivity />
|
|
23
|
+
</div>
|
|
24
|
+
</div>
|
|
19
25
|
</div>
|
|
20
26
|
</PageLayout>
|
|
21
27
|
)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { createFileRoute } from '@tanstack/react-router'
|
|
2
|
+
|
|
3
|
+
import { PageLayout } from '@/components/layout/page-layout'
|
|
4
|
+
import { UsersTable } from '@/features/users/components/users-table'
|
|
5
|
+
|
|
6
|
+
export const Route = createFileRoute('/_authenticated/users')({
|
|
7
|
+
component: UsersPage,
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
function UsersPage() {
|
|
11
|
+
return (
|
|
12
|
+
<PageLayout title='User Management' description='Manage your application users'>
|
|
13
|
+
<UsersTable />
|
|
14
|
+
</PageLayout>
|
|
15
|
+
)
|
|
16
|
+
}
|
|
@@ -2,6 +2,7 @@ import { defineConfig } from 'vite'
|
|
|
2
2
|
import react from '@vitejs/plugin-react-swc'
|
|
3
3
|
import tailwindcss from '@tailwindcss/vite'
|
|
4
4
|
import { TanStackRouterVite } from '@tanstack/router-plugin/vite'
|
|
5
|
+
import { sentryVitePlugin } from '@sentry/vite-plugin'
|
|
5
6
|
import { resolve } from 'path'
|
|
6
7
|
|
|
7
8
|
export default defineConfig({
|
|
@@ -9,11 +10,18 @@ export default defineConfig({
|
|
|
9
10
|
TanStackRouterVite({ autoCodeSplitting: true }),
|
|
10
11
|
react(),
|
|
11
12
|
tailwindcss(),
|
|
13
|
+
// Only upload source maps in production builds with SENTRY_AUTH_TOKEN set
|
|
14
|
+
...(process.env.SENTRY_AUTH_TOKEN ? [sentryVitePlugin({
|
|
15
|
+
org: process.env.SENTRY_ORG,
|
|
16
|
+
project: process.env.SENTRY_PROJECT,
|
|
17
|
+
authToken: process.env.SENTRY_AUTH_TOKEN,
|
|
18
|
+
})] : []),
|
|
12
19
|
],
|
|
13
20
|
resolve: {
|
|
14
21
|
alias: { '@': resolve(__dirname, './src') },
|
|
15
22
|
},
|
|
16
23
|
build: {
|
|
24
|
+
sourcemap: true,
|
|
17
25
|
target: 'esnext',
|
|
18
26
|
rolldownOptions: {
|
|
19
27
|
output: {
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"dependencies": {
|
|
3
|
-
"@tiptap/react": "^2.11.7",
|
|
4
|
-
"@tiptap/starter-kit": "^2.11.7",
|
|
5
|
-
"@tiptap/extension-link": "^2.11.7",
|
|
6
|
-
"@tiptap/extension-image": "^2.11.7",
|
|
7
|
-
"@tiptap/extension-placeholder": "^2.11.7",
|
|
8
|
-
"@tiptap/extension-underline": "^2.11.7"
|
|
9
|
-
}
|
|
10
|
-
}
|