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.
Files changed (36) hide show
  1. package/dist/index.js +43 -486
  2. package/package.json +1 -1
  3. package/templates/template-vite/_env.example +8 -0
  4. package/templates/template-vite/package.json +5 -2
  5. package/templates/template-vite/src/components/data-table/data-table-column-header.tsx +62 -0
  6. package/templates/template-vite/src/components/data-table/data-table-faceted-filter.tsx +129 -0
  7. package/templates/template-vite/src/components/data-table/data-table-pagination.tsx +80 -0
  8. package/templates/template-vite/src/components/data-table/data-table-toolbar.tsx +66 -0
  9. package/templates/template-vite/src/components/data-table/data-table-view-options.tsx +46 -0
  10. package/templates/template-vite/src/components/data-table/data-table.tsx +63 -0
  11. package/templates/template-vite/src/components/layout/sidebar.tsx +2 -1
  12. package/templates/template-vite/src/components/ui/alert-dialog.tsx +106 -0
  13. package/templates/template-vite/src/components/ui/command.tsx +118 -0
  14. package/templates/template-vite/src/components/ui/popover.tsx +28 -0
  15. package/templates/template-vite/src/components/ui/table.tsx +77 -0
  16. package/templates/template-vite/src/features/dashboard/components/overview-chart.tsx +61 -0
  17. package/templates/template-vite/src/features/dashboard/components/recent-activity.tsx +37 -0
  18. package/templates/template-vite/src/features/dashboard/components/stats-cards.tsx +37 -0
  19. package/templates/template-vite/src/features/users/components/user-delete-confirmation-dialog.tsx +48 -0
  20. package/templates/template-vite/src/features/users/components/user-form-dialog.tsx +143 -0
  21. package/templates/template-vite/src/features/users/components/users-table-columns.tsx +154 -0
  22. package/templates/template-vite/src/features/users/components/users-table.tsx +143 -0
  23. package/templates/template-vite/src/features/users/data/users-mock-data.ts +55 -0
  24. package/templates/template-vite/src/features/users/schemas/user-form-schema.ts +14 -0
  25. package/templates/template-vite/src/features/users/types/user.ts +12 -0
  26. package/templates/template-vite/src/lib/sentry.ts +28 -0
  27. package/templates/template-vite/src/main.tsx +3 -0
  28. package/templates/template-vite/src/routes/_authenticated/dashboard.tsx +12 -6
  29. package/templates/template-vite/src/routes/_authenticated/users.tsx +16 -0
  30. package/templates/template-vite/vite.config.ts +8 -0
  31. package/templates/template-vite/optional/charts/deps.json +0 -7
  32. package/templates/template-vite/optional/dark-mode/deps.json +0 -5
  33. package/templates/template-vite/optional/dnd/deps.json +0 -8
  34. package/templates/template-vite/optional/editor/deps.json +0 -10
  35. package/templates/template-vite/optional/i18n/deps.json +0 -7
  36. 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
+ }
@@ -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
+ }