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,118 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
import { type DialogProps } from '@radix-ui/react-dialog'
|
|
3
|
+
import { Command as CommandPrimitive } from 'cmdk'
|
|
4
|
+
import { Search } from 'lucide-react'
|
|
5
|
+
|
|
6
|
+
import { cn } from '@/lib/utils'
|
|
7
|
+
import { Dialog, DialogContent } from '@/components/ui/dialog'
|
|
8
|
+
|
|
9
|
+
const Command = React.forwardRef<
|
|
10
|
+
React.ElementRef<typeof CommandPrimitive>,
|
|
11
|
+
React.ComponentPropsWithoutRef<typeof CommandPrimitive>
|
|
12
|
+
>(({ className, ...props }, ref) => (
|
|
13
|
+
<CommandPrimitive
|
|
14
|
+
ref={ref}
|
|
15
|
+
className={cn('flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground', className)}
|
|
16
|
+
{...props}
|
|
17
|
+
/>
|
|
18
|
+
))
|
|
19
|
+
Command.displayName = CommandPrimitive.displayName
|
|
20
|
+
|
|
21
|
+
const CommandDialog = ({ children, ...props }: DialogProps) => (
|
|
22
|
+
<Dialog {...props}>
|
|
23
|
+
<DialogContent className='overflow-hidden p-0'>
|
|
24
|
+
<Command className='[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5'>
|
|
25
|
+
{children}
|
|
26
|
+
</Command>
|
|
27
|
+
</DialogContent>
|
|
28
|
+
</Dialog>
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
const CommandInput = React.forwardRef<
|
|
32
|
+
React.ElementRef<typeof CommandPrimitive.Input>,
|
|
33
|
+
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
|
|
34
|
+
>(({ className, ...props }, ref) => (
|
|
35
|
+
<div className='flex items-center border-b px-3' cmdk-input-wrapper=''>
|
|
36
|
+
<Search className='mr-2 h-4 w-4 shrink-0 opacity-50' />
|
|
37
|
+
<CommandPrimitive.Input
|
|
38
|
+
ref={ref}
|
|
39
|
+
className={cn(
|
|
40
|
+
'flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50',
|
|
41
|
+
className
|
|
42
|
+
)}
|
|
43
|
+
{...props}
|
|
44
|
+
/>
|
|
45
|
+
</div>
|
|
46
|
+
))
|
|
47
|
+
CommandInput.displayName = CommandPrimitive.Input.displayName
|
|
48
|
+
|
|
49
|
+
const CommandList = React.forwardRef<
|
|
50
|
+
React.ElementRef<typeof CommandPrimitive.List>,
|
|
51
|
+
React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
|
|
52
|
+
>(({ className, ...props }, ref) => (
|
|
53
|
+
<CommandPrimitive.List ref={ref} className={cn('max-h-[300px] overflow-y-auto overflow-x-hidden', className)} {...props} />
|
|
54
|
+
))
|
|
55
|
+
CommandList.displayName = CommandPrimitive.List.displayName
|
|
56
|
+
|
|
57
|
+
const CommandEmpty = React.forwardRef<
|
|
58
|
+
React.ElementRef<typeof CommandPrimitive.Empty>,
|
|
59
|
+
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
|
|
60
|
+
>((props, ref) => (
|
|
61
|
+
<CommandPrimitive.Empty ref={ref} className='py-6 text-center text-sm' {...props} />
|
|
62
|
+
))
|
|
63
|
+
CommandEmpty.displayName = CommandPrimitive.Empty.displayName
|
|
64
|
+
|
|
65
|
+
const CommandGroup = React.forwardRef<
|
|
66
|
+
React.ElementRef<typeof CommandPrimitive.Group>,
|
|
67
|
+
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
|
|
68
|
+
>(({ className, ...props }, ref) => (
|
|
69
|
+
<CommandPrimitive.Group
|
|
70
|
+
ref={ref}
|
|
71
|
+
className={cn(
|
|
72
|
+
'overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground',
|
|
73
|
+
className
|
|
74
|
+
)}
|
|
75
|
+
{...props}
|
|
76
|
+
/>
|
|
77
|
+
))
|
|
78
|
+
CommandGroup.displayName = CommandPrimitive.Group.displayName
|
|
79
|
+
|
|
80
|
+
const CommandItem = React.forwardRef<
|
|
81
|
+
React.ElementRef<typeof CommandPrimitive.Item>,
|
|
82
|
+
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
|
|
83
|
+
>(({ className, ...props }, ref) => (
|
|
84
|
+
<CommandPrimitive.Item
|
|
85
|
+
ref={ref}
|
|
86
|
+
className={cn(
|
|
87
|
+
'relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50',
|
|
88
|
+
className
|
|
89
|
+
)}
|
|
90
|
+
{...props}
|
|
91
|
+
/>
|
|
92
|
+
))
|
|
93
|
+
CommandItem.displayName = CommandPrimitive.Item.displayName
|
|
94
|
+
|
|
95
|
+
const CommandSeparator = React.forwardRef<
|
|
96
|
+
React.ElementRef<typeof CommandPrimitive.Separator>,
|
|
97
|
+
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
|
|
98
|
+
>(({ className, ...props }, ref) => (
|
|
99
|
+
<CommandPrimitive.Separator ref={ref} className={cn('-mx-1 h-px bg-border', className)} {...props} />
|
|
100
|
+
))
|
|
101
|
+
CommandSeparator.displayName = CommandPrimitive.Separator.displayName
|
|
102
|
+
|
|
103
|
+
const CommandShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => (
|
|
104
|
+
<span className={cn('ml-auto text-xs tracking-widest text-muted-foreground', className)} {...props} />
|
|
105
|
+
)
|
|
106
|
+
CommandShortcut.displayName = 'CommandShortcut'
|
|
107
|
+
|
|
108
|
+
export {
|
|
109
|
+
Command,
|
|
110
|
+
CommandDialog,
|
|
111
|
+
CommandEmpty,
|
|
112
|
+
CommandGroup,
|
|
113
|
+
CommandInput,
|
|
114
|
+
CommandItem,
|
|
115
|
+
CommandList,
|
|
116
|
+
CommandSeparator,
|
|
117
|
+
CommandShortcut,
|
|
118
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
import * as PopoverPrimitive from '@radix-ui/react-popover'
|
|
3
|
+
import { cn } from '@/lib/utils'
|
|
4
|
+
|
|
5
|
+
const Popover = PopoverPrimitive.Root
|
|
6
|
+
const PopoverTrigger = PopoverPrimitive.Trigger
|
|
7
|
+
const PopoverAnchor = PopoverPrimitive.Anchor
|
|
8
|
+
|
|
9
|
+
const PopoverContent = React.forwardRef<
|
|
10
|
+
React.ElementRef<typeof PopoverPrimitive.Content>,
|
|
11
|
+
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
|
|
12
|
+
>(({ className, align = 'center', sideOffset = 4, ...props }, ref) => (
|
|
13
|
+
<PopoverPrimitive.Portal>
|
|
14
|
+
<PopoverPrimitive.Content
|
|
15
|
+
ref={ref}
|
|
16
|
+
align={align}
|
|
17
|
+
sideOffset={sideOffset}
|
|
18
|
+
className={cn(
|
|
19
|
+
'z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none 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-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|
|
20
|
+
className
|
|
21
|
+
)}
|
|
22
|
+
{...props}
|
|
23
|
+
/>
|
|
24
|
+
</PopoverPrimitive.Portal>
|
|
25
|
+
))
|
|
26
|
+
PopoverContent.displayName = PopoverPrimitive.Content.displayName
|
|
27
|
+
|
|
28
|
+
export { Popover, PopoverAnchor, PopoverContent, PopoverTrigger }
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
import { cn } from '@/lib/utils'
|
|
3
|
+
|
|
4
|
+
const Table = React.forwardRef<HTMLTableElement, React.HTMLAttributes<HTMLTableElement>>(
|
|
5
|
+
({ className, ...props }, ref) => (
|
|
6
|
+
<div className='relative w-full overflow-auto'>
|
|
7
|
+
<table ref={ref} className={cn('w-full caption-bottom text-sm', className)} {...props} />
|
|
8
|
+
</div>
|
|
9
|
+
)
|
|
10
|
+
)
|
|
11
|
+
Table.displayName = 'Table'
|
|
12
|
+
|
|
13
|
+
const TableHeader = React.forwardRef<HTMLTableSectionElement, React.HTMLAttributes<HTMLTableSectionElement>>(
|
|
14
|
+
({ className, ...props }, ref) => (
|
|
15
|
+
<thead ref={ref} className={cn('[&_tr]:border-b', className)} {...props} />
|
|
16
|
+
)
|
|
17
|
+
)
|
|
18
|
+
TableHeader.displayName = 'TableHeader'
|
|
19
|
+
|
|
20
|
+
const TableBody = React.forwardRef<HTMLTableSectionElement, React.HTMLAttributes<HTMLTableSectionElement>>(
|
|
21
|
+
({ className, ...props }, ref) => (
|
|
22
|
+
<tbody ref={ref} className={cn('[&_tr:last-child]:border-0', className)} {...props} />
|
|
23
|
+
)
|
|
24
|
+
)
|
|
25
|
+
TableBody.displayName = 'TableBody'
|
|
26
|
+
|
|
27
|
+
const TableFooter = React.forwardRef<HTMLTableSectionElement, React.HTMLAttributes<HTMLTableSectionElement>>(
|
|
28
|
+
({ className, ...props }, ref) => (
|
|
29
|
+
<tfoot ref={ref} className={cn('border-t bg-muted/50 font-medium [&>tr]:last:border-b-0', className)} {...props} />
|
|
30
|
+
)
|
|
31
|
+
)
|
|
32
|
+
TableFooter.displayName = 'TableFooter'
|
|
33
|
+
|
|
34
|
+
const TableRow = React.forwardRef<HTMLTableRowElement, React.HTMLAttributes<HTMLTableRowElement>>(
|
|
35
|
+
({ className, ...props }, ref) => (
|
|
36
|
+
<tr
|
|
37
|
+
ref={ref}
|
|
38
|
+
className={cn('border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted', className)}
|
|
39
|
+
{...props}
|
|
40
|
+
/>
|
|
41
|
+
)
|
|
42
|
+
)
|
|
43
|
+
TableRow.displayName = 'TableRow'
|
|
44
|
+
|
|
45
|
+
const TableHead = React.forwardRef<HTMLTableCellElement, React.ThHTMLAttributes<HTMLTableCellElement>>(
|
|
46
|
+
({ className, ...props }, ref) => (
|
|
47
|
+
<th
|
|
48
|
+
ref={ref}
|
|
49
|
+
className={cn(
|
|
50
|
+
'h-10 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]',
|
|
51
|
+
className
|
|
52
|
+
)}
|
|
53
|
+
{...props}
|
|
54
|
+
/>
|
|
55
|
+
)
|
|
56
|
+
)
|
|
57
|
+
TableHead.displayName = 'TableHead'
|
|
58
|
+
|
|
59
|
+
const TableCell = React.forwardRef<HTMLTableCellElement, React.TdHTMLAttributes<HTMLTableCellElement>>(
|
|
60
|
+
({ className, ...props }, ref) => (
|
|
61
|
+
<td
|
|
62
|
+
ref={ref}
|
|
63
|
+
className={cn('p-4 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]', className)}
|
|
64
|
+
{...props}
|
|
65
|
+
/>
|
|
66
|
+
)
|
|
67
|
+
)
|
|
68
|
+
TableCell.displayName = 'TableCell'
|
|
69
|
+
|
|
70
|
+
const TableCaption = React.forwardRef<HTMLTableCaptionElement, React.HTMLAttributes<HTMLTableCaptionElement>>(
|
|
71
|
+
({ className, ...props }, ref) => (
|
|
72
|
+
<caption ref={ref} className={cn('mt-4 text-sm text-muted-foreground', className)} {...props} />
|
|
73
|
+
)
|
|
74
|
+
)
|
|
75
|
+
TableCaption.displayName = 'TableCaption'
|
|
76
|
+
|
|
77
|
+
export { Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, TableRow }
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { Area, AreaChart, CartesianGrid, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts'
|
|
2
|
+
|
|
3
|
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
|
4
|
+
|
|
5
|
+
const data = [
|
|
6
|
+
{ month: 'Jan', revenue: 18000, users: 980 },
|
|
7
|
+
{ month: 'Feb', revenue: 22000, users: 1200 },
|
|
8
|
+
{ month: 'Mar', revenue: 19500, users: 1050 },
|
|
9
|
+
{ month: 'Apr', revenue: 28000, users: 1540 },
|
|
10
|
+
{ month: 'May', revenue: 24000, users: 1320 },
|
|
11
|
+
{ month: 'Jun', revenue: 32000, users: 1780 },
|
|
12
|
+
{ month: 'Jul', revenue: 35000, users: 1960 },
|
|
13
|
+
{ month: 'Aug', revenue: 29000, users: 1590 },
|
|
14
|
+
{ month: 'Sep', revenue: 38000, users: 2100 },
|
|
15
|
+
{ month: 'Oct', revenue: 42000, users: 2300 },
|
|
16
|
+
{ month: 'Nov', revenue: 39000, users: 2150 },
|
|
17
|
+
{ month: 'Dec', revenue: 45000, users: 2480 },
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
export function OverviewChart() {
|
|
21
|
+
return (
|
|
22
|
+
<Card>
|
|
23
|
+
<CardHeader>
|
|
24
|
+
<CardTitle>Overview</CardTitle>
|
|
25
|
+
<CardDescription>Monthly revenue and user growth</CardDescription>
|
|
26
|
+
</CardHeader>
|
|
27
|
+
<CardContent>
|
|
28
|
+
<ResponsiveContainer width='100%' height={300}>
|
|
29
|
+
<AreaChart data={data} margin={{ top: 10, right: 30, left: 0, bottom: 0 }}>
|
|
30
|
+
<defs>
|
|
31
|
+
<linearGradient id='colorRevenue' x1='0' y1='0' x2='0' y2='1'>
|
|
32
|
+
<stop offset='5%' stopColor='hsl(var(--primary))' stopOpacity={0.2} />
|
|
33
|
+
<stop offset='95%' stopColor='hsl(var(--primary))' stopOpacity={0} />
|
|
34
|
+
</linearGradient>
|
|
35
|
+
</defs>
|
|
36
|
+
<CartesianGrid strokeDasharray='3 3' className='stroke-muted' />
|
|
37
|
+
<XAxis dataKey='month' className='text-xs' tick={{ fill: 'hsl(var(--muted-foreground))' }} />
|
|
38
|
+
<YAxis className='text-xs' tick={{ fill: 'hsl(var(--muted-foreground))' }} />
|
|
39
|
+
<Tooltip
|
|
40
|
+
contentStyle={{
|
|
41
|
+
background: 'hsl(var(--card))',
|
|
42
|
+
border: '1px solid hsl(var(--border))',
|
|
43
|
+
borderRadius: '8px',
|
|
44
|
+
color: 'hsl(var(--card-foreground))',
|
|
45
|
+
}}
|
|
46
|
+
/>
|
|
47
|
+
<Area
|
|
48
|
+
type='monotone'
|
|
49
|
+
dataKey='revenue'
|
|
50
|
+
stroke='hsl(var(--primary))'
|
|
51
|
+
fillOpacity={1}
|
|
52
|
+
fill='url(#colorRevenue)'
|
|
53
|
+
strokeWidth={2}
|
|
54
|
+
name='Revenue ($)'
|
|
55
|
+
/>
|
|
56
|
+
</AreaChart>
|
|
57
|
+
</ResponsiveContainer>
|
|
58
|
+
</CardContent>
|
|
59
|
+
</Card>
|
|
60
|
+
)
|
|
61
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { Avatar, AvatarFallback } from '@/components/ui/avatar'
|
|
2
|
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
|
3
|
+
|
|
4
|
+
const recentUsers = [
|
|
5
|
+
{ name: 'Alice Johnson', email: 'alice@example.com', amount: '+$250.00' },
|
|
6
|
+
{ name: 'Bob Smith', email: 'bob@example.com', amount: '+$180.00' },
|
|
7
|
+
{ name: 'Carol White', email: 'carol@example.com', amount: '+$320.00' },
|
|
8
|
+
{ name: 'David Brown', email: 'david@example.com', amount: '+$90.00' },
|
|
9
|
+
{ name: 'Eve Davis', email: 'eve@example.com', amount: '+$430.00' },
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
export function RecentActivity() {
|
|
13
|
+
return (
|
|
14
|
+
<Card>
|
|
15
|
+
<CardHeader>
|
|
16
|
+
<CardTitle>Recent Activity</CardTitle>
|
|
17
|
+
<CardDescription>5 new users this month</CardDescription>
|
|
18
|
+
</CardHeader>
|
|
19
|
+
<CardContent>
|
|
20
|
+
<div className='space-y-4'>
|
|
21
|
+
{recentUsers.map((user) => (
|
|
22
|
+
<div key={user.email} className='flex items-center gap-4'>
|
|
23
|
+
<Avatar className='h-9 w-9'>
|
|
24
|
+
<AvatarFallback>{user.name.split(' ').map((n) => n[0]).join('')}</AvatarFallback>
|
|
25
|
+
</Avatar>
|
|
26
|
+
<div className='min-w-0 flex-1'>
|
|
27
|
+
<p className='truncate text-sm font-medium'>{user.name}</p>
|
|
28
|
+
<p className='truncate text-xs text-muted-foreground'>{user.email}</p>
|
|
29
|
+
</div>
|
|
30
|
+
<span className='text-sm font-medium text-green-600'>{user.amount}</span>
|
|
31
|
+
</div>
|
|
32
|
+
))}
|
|
33
|
+
</div>
|
|
34
|
+
</CardContent>
|
|
35
|
+
</Card>
|
|
36
|
+
)
|
|
37
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { Activity, DollarSign, TrendingUp, Users } from 'lucide-react'
|
|
2
|
+
|
|
3
|
+
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
|
4
|
+
|
|
5
|
+
interface StatCard {
|
|
6
|
+
title: string
|
|
7
|
+
value: string
|
|
8
|
+
description: string
|
|
9
|
+
icon: React.ComponentType<{ className?: string }>
|
|
10
|
+
trend: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const stats: StatCard[] = [
|
|
14
|
+
{ title: 'Total Users', value: '12,345', description: '+12% from last month', icon: Users, trend: 'up' },
|
|
15
|
+
{ title: 'Active Sessions', value: '2,891', description: '+8% from last month', icon: Activity, trend: 'up' },
|
|
16
|
+
{ title: 'Revenue', value: '$45,231', description: '+20.1% from last month', icon: DollarSign, trend: 'up' },
|
|
17
|
+
{ title: 'Growth Rate', value: '+18.2%', description: '+4% from last month', icon: TrendingUp, trend: 'up' },
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
export function StatsCards() {
|
|
21
|
+
return (
|
|
22
|
+
<div className='grid gap-4 md:grid-cols-2 lg:grid-cols-4'>
|
|
23
|
+
{stats.map((stat) => (
|
|
24
|
+
<Card key={stat.title}>
|
|
25
|
+
<CardHeader className='flex flex-row items-center justify-between space-y-0 pb-2'>
|
|
26
|
+
<CardTitle className='text-sm font-medium'>{stat.title}</CardTitle>
|
|
27
|
+
<stat.icon className='h-4 w-4 text-muted-foreground' />
|
|
28
|
+
</CardHeader>
|
|
29
|
+
<CardContent>
|
|
30
|
+
<div className='text-2xl font-bold'>{stat.value}</div>
|
|
31
|
+
<p className='text-xs text-muted-foreground'>{stat.description}</p>
|
|
32
|
+
</CardContent>
|
|
33
|
+
</Card>
|
|
34
|
+
))}
|
|
35
|
+
</div>
|
|
36
|
+
)
|
|
37
|
+
}
|
package/templates/template-vite/src/features/users/components/user-delete-confirmation-dialog.tsx
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AlertDialog,
|
|
3
|
+
AlertDialogAction,
|
|
4
|
+
AlertDialogCancel,
|
|
5
|
+
AlertDialogContent,
|
|
6
|
+
AlertDialogDescription,
|
|
7
|
+
AlertDialogFooter,
|
|
8
|
+
AlertDialogHeader,
|
|
9
|
+
AlertDialogTitle,
|
|
10
|
+
} from '@/components/ui/alert-dialog'
|
|
11
|
+
import type { User } from '../types/user'
|
|
12
|
+
|
|
13
|
+
interface UserDeleteConfirmationDialogProps {
|
|
14
|
+
open: boolean
|
|
15
|
+
onOpenChange: (open: boolean) => void
|
|
16
|
+
user: User | null
|
|
17
|
+
onConfirm: () => void
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function UserDeleteConfirmationDialog({
|
|
21
|
+
open,
|
|
22
|
+
onOpenChange,
|
|
23
|
+
user,
|
|
24
|
+
onConfirm,
|
|
25
|
+
}: UserDeleteConfirmationDialogProps) {
|
|
26
|
+
return (
|
|
27
|
+
<AlertDialog open={open} onOpenChange={onOpenChange}>
|
|
28
|
+
<AlertDialogContent>
|
|
29
|
+
<AlertDialogHeader>
|
|
30
|
+
<AlertDialogTitle>Delete User</AlertDialogTitle>
|
|
31
|
+
<AlertDialogDescription>
|
|
32
|
+
Are you sure you want to delete{' '}
|
|
33
|
+
<span className='font-medium text-foreground'>{user?.name}</span>? This action cannot be undone.
|
|
34
|
+
</AlertDialogDescription>
|
|
35
|
+
</AlertDialogHeader>
|
|
36
|
+
<AlertDialogFooter>
|
|
37
|
+
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
|
38
|
+
<AlertDialogAction
|
|
39
|
+
onClick={onConfirm}
|
|
40
|
+
className='bg-destructive text-destructive-foreground hover:bg-destructive/90'
|
|
41
|
+
>
|
|
42
|
+
Delete
|
|
43
|
+
</AlertDialogAction>
|
|
44
|
+
</AlertDialogFooter>
|
|
45
|
+
</AlertDialogContent>
|
|
46
|
+
</AlertDialog>
|
|
47
|
+
)
|
|
48
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { useEffect } from 'react'
|
|
2
|
+
import { useForm } from 'react-hook-form'
|
|
3
|
+
import { zodResolver } from '@hookform/resolvers/zod'
|
|
4
|
+
|
|
5
|
+
import { Button } from '@/components/ui/button'
|
|
6
|
+
import {
|
|
7
|
+
Dialog,
|
|
8
|
+
DialogContent,
|
|
9
|
+
DialogDescription,
|
|
10
|
+
DialogFooter,
|
|
11
|
+
DialogHeader,
|
|
12
|
+
DialogTitle,
|
|
13
|
+
} from '@/components/ui/dialog'
|
|
14
|
+
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'
|
|
15
|
+
import { Input } from '@/components/ui/input'
|
|
16
|
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
|
17
|
+
import { userFormSchema, type UserFormValues } from '../schemas/user-form-schema'
|
|
18
|
+
import type { User } from '../types/user'
|
|
19
|
+
|
|
20
|
+
interface UserFormDialogProps {
|
|
21
|
+
open: boolean
|
|
22
|
+
onOpenChange: (open: boolean) => void
|
|
23
|
+
user: User | null
|
|
24
|
+
onSubmit: (values: Omit<User, 'id' | 'createdAt'>) => void
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function UserFormDialog({ open, onOpenChange, user, onSubmit }: UserFormDialogProps) {
|
|
28
|
+
const isEditing = !!user
|
|
29
|
+
|
|
30
|
+
const form = useForm<UserFormValues>({
|
|
31
|
+
resolver: zodResolver(userFormSchema),
|
|
32
|
+
defaultValues: { name: '', email: '', role: 'user', status: 'active' },
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
// Reset form when dialog opens or user changes
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
if (open) {
|
|
38
|
+
form.reset(
|
|
39
|
+
user
|
|
40
|
+
? { name: user.name, email: user.email, role: user.role, status: user.status }
|
|
41
|
+
: { name: '', email: '', role: 'user', status: 'active' }
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
}, [open, user, form])
|
|
45
|
+
|
|
46
|
+
const handleSubmit = (values: UserFormValues) => {
|
|
47
|
+
onSubmit(values)
|
|
48
|
+
form.reset()
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
53
|
+
<DialogContent className='sm:max-w-[425px]'>
|
|
54
|
+
<DialogHeader>
|
|
55
|
+
<DialogTitle>{isEditing ? 'Edit User' : 'Add User'}</DialogTitle>
|
|
56
|
+
<DialogDescription>
|
|
57
|
+
{isEditing ? 'Update the user details below.' : 'Fill in the details to create a new user.'}
|
|
58
|
+
</DialogDescription>
|
|
59
|
+
</DialogHeader>
|
|
60
|
+
<Form {...form}>
|
|
61
|
+
<form onSubmit={form.handleSubmit(handleSubmit)} className='space-y-4'>
|
|
62
|
+
<FormField
|
|
63
|
+
control={form.control}
|
|
64
|
+
name='name'
|
|
65
|
+
render={({ field }) => (
|
|
66
|
+
<FormItem>
|
|
67
|
+
<FormLabel>Name</FormLabel>
|
|
68
|
+
<FormControl>
|
|
69
|
+
<Input placeholder='Enter full name' {...field} />
|
|
70
|
+
</FormControl>
|
|
71
|
+
<FormMessage />
|
|
72
|
+
</FormItem>
|
|
73
|
+
)}
|
|
74
|
+
/>
|
|
75
|
+
<FormField
|
|
76
|
+
control={form.control}
|
|
77
|
+
name='email'
|
|
78
|
+
render={({ field }) => (
|
|
79
|
+
<FormItem>
|
|
80
|
+
<FormLabel>Email</FormLabel>
|
|
81
|
+
<FormControl>
|
|
82
|
+
<Input type='email' placeholder='Enter email address' {...field} />
|
|
83
|
+
</FormControl>
|
|
84
|
+
<FormMessage />
|
|
85
|
+
</FormItem>
|
|
86
|
+
)}
|
|
87
|
+
/>
|
|
88
|
+
<FormField
|
|
89
|
+
control={form.control}
|
|
90
|
+
name='role'
|
|
91
|
+
render={({ field }) => (
|
|
92
|
+
<FormItem>
|
|
93
|
+
<FormLabel>Role</FormLabel>
|
|
94
|
+
<Select onValueChange={field.onChange} value={field.value}>
|
|
95
|
+
<FormControl>
|
|
96
|
+
<SelectTrigger>
|
|
97
|
+
<SelectValue placeholder='Select a role' />
|
|
98
|
+
</SelectTrigger>
|
|
99
|
+
</FormControl>
|
|
100
|
+
<SelectContent>
|
|
101
|
+
<SelectItem value='admin'>Admin</SelectItem>
|
|
102
|
+
<SelectItem value='manager'>Manager</SelectItem>
|
|
103
|
+
<SelectItem value='user'>User</SelectItem>
|
|
104
|
+
</SelectContent>
|
|
105
|
+
</Select>
|
|
106
|
+
<FormMessage />
|
|
107
|
+
</FormItem>
|
|
108
|
+
)}
|
|
109
|
+
/>
|
|
110
|
+
<FormField
|
|
111
|
+
control={form.control}
|
|
112
|
+
name='status'
|
|
113
|
+
render={({ field }) => (
|
|
114
|
+
<FormItem>
|
|
115
|
+
<FormLabel>Status</FormLabel>
|
|
116
|
+
<Select onValueChange={field.onChange} value={field.value}>
|
|
117
|
+
<FormControl>
|
|
118
|
+
<SelectTrigger>
|
|
119
|
+
<SelectValue placeholder='Select a status' />
|
|
120
|
+
</SelectTrigger>
|
|
121
|
+
</FormControl>
|
|
122
|
+
<SelectContent>
|
|
123
|
+
<SelectItem value='active'>Active</SelectItem>
|
|
124
|
+
<SelectItem value='inactive'>Inactive</SelectItem>
|
|
125
|
+
<SelectItem value='pending'>Pending</SelectItem>
|
|
126
|
+
</SelectContent>
|
|
127
|
+
</Select>
|
|
128
|
+
<FormMessage />
|
|
129
|
+
</FormItem>
|
|
130
|
+
)}
|
|
131
|
+
/>
|
|
132
|
+
<DialogFooter>
|
|
133
|
+
<Button type='button' variant='outline' onClick={() => onOpenChange(false)}>
|
|
134
|
+
Cancel
|
|
135
|
+
</Button>
|
|
136
|
+
<Button type='submit'>{isEditing ? 'Save changes' : 'Create user'}</Button>
|
|
137
|
+
</DialogFooter>
|
|
138
|
+
</form>
|
|
139
|
+
</Form>
|
|
140
|
+
</DialogContent>
|
|
141
|
+
</Dialog>
|
|
142
|
+
)
|
|
143
|
+
}
|