@betterstart/cli 0.1.69 → 0.1.71
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/chunk-E4HZYXQ2.js +36 -0
- package/dist/chunk-E4HZYXQ2.js.map +1 -0
- package/dist/cli.js +799 -4586
- package/dist/cli.js.map +1 -1
- package/dist/reader-2T45D7JZ.js +7 -0
- package/package.json +1 -1
- package/templates/init/api/auth-route.ts +3 -0
- package/templates/init/api/upload-route.ts +74 -0
- package/templates/init/cms-globals.css +200 -0
- package/templates/init/components/data-table/data-table-pagination.tsx +90 -0
- package/templates/init/components/data-table/data-table-toolbar.tsx +93 -0
- package/templates/init/components/data-table/data-table.tsx +188 -0
- package/templates/init/components/layout/cms-header.tsx +32 -0
- package/templates/init/components/layout/cms-nav-link.tsx +25 -0
- package/templates/init/components/layout/cms-providers.tsx +33 -0
- package/templates/init/components/layout/cms-search.tsx +25 -0
- package/templates/init/components/layout/cms-sidebar.tsx +192 -0
- package/templates/init/components/layout/cms-sign-out.tsx +30 -0
- package/templates/init/components/shared/delete-dialog.tsx +75 -0
- package/templates/init/components/shared/page-header.tsx +23 -0
- package/templates/init/components/shared/status-badge.tsx +43 -0
- package/templates/init/data/navigation.ts +39 -0
- package/templates/init/db/client.ts +8 -0
- package/templates/init/db/schema.ts +88 -0
- package/{dist/chunk-6JCWMKSY.js → templates/init/drizzle.config.ts} +2 -11
- package/templates/init/hooks/use-cms-theme.tsx +78 -0
- package/templates/init/hooks/use-editor-image-upload.ts +82 -0
- package/templates/init/hooks/use-local-storage.ts +46 -0
- package/templates/init/hooks/use-mobile.ts +19 -0
- package/templates/init/hooks/use-upload.ts +177 -0
- package/templates/init/hooks/use-users.ts +13 -0
- package/templates/init/lib/actions/form-settings.ts +126 -0
- package/templates/init/lib/actions/profile.ts +62 -0
- package/templates/init/lib/actions/upload.ts +153 -0
- package/templates/init/lib/actions/users.ts +145 -0
- package/templates/init/lib/auth/auth-client.ts +12 -0
- package/templates/init/lib/auth/auth.ts +43 -0
- package/templates/init/lib/auth/middleware.ts +44 -0
- package/templates/init/lib/markdown/cached.ts +7 -0
- package/templates/init/lib/markdown/format.ts +55 -0
- package/templates/init/lib/markdown/render.ts +182 -0
- package/templates/init/lib/r2.ts +55 -0
- package/templates/init/pages/account-layout.tsx +63 -0
- package/templates/init/pages/authenticated-layout.tsx +26 -0
- package/templates/init/pages/cms-layout.tsx +16 -0
- package/templates/init/pages/dashboard-page.tsx +91 -0
- package/templates/init/pages/login-form.tsx +117 -0
- package/templates/init/pages/login-page.tsx +17 -0
- package/templates/init/pages/profile/profile-form.tsx +361 -0
- package/templates/init/pages/profile/profile-page.tsx +34 -0
- package/templates/init/pages/users/columns.tsx +241 -0
- package/templates/init/pages/users/create-user-dialog.tsx +116 -0
- package/templates/init/pages/users/edit-role-dialog.tsx +92 -0
- package/templates/init/pages/users/users-page-content.tsx +29 -0
- package/templates/init/pages/users/users-page.tsx +19 -0
- package/templates/init/pages/users/users-table.tsx +219 -0
- package/templates/init/types/auth.ts +78 -0
- package/templates/init/types/index.ts +79 -0
- package/templates/init/types/table-meta.ts +16 -0
- package/templates/init/utils/cn.ts +6 -0
- package/templates/init/utils/mailchimp.ts +39 -0
- package/templates/init/utils/seo.ts +90 -0
- package/templates/init/utils/validation.ts +105 -0
- package/templates/init/utils/webhook.ts +28 -0
- package/templates/ui/alert-dialog.tsx +46 -28
- package/templates/ui/avatar.tsx +37 -20
- package/templates/ui/button.tsx +3 -3
- package/templates/ui/card.tsx +30 -18
- package/templates/ui/dialog.tsx +46 -22
- package/templates/ui/dropdown-menu.tsx +1 -1
- package/templates/ui/input.tsx +1 -1
- package/templates/ui/select.tsx +42 -34
- package/templates/ui/sidebar.tsx +13 -13
- package/templates/ui/table.tsx +2 -2
- package/dist/chunk-6JCWMKSY.js.map +0 -1
- package/dist/drizzle-config-EDKOEZ6G.js +0 -7
- /package/dist/{drizzle-config-EDKOEZ6G.js.map → reader-2T45D7JZ.js.map} +0 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { getSession } from '@cms/auth/middleware'
|
|
2
|
+
import { PageHeader } from '@cms/components/shared/page-header'
|
|
3
|
+
import { UserRole } from '@cms/types/auth'
|
|
4
|
+
import { redirect } from 'next/navigation'
|
|
5
|
+
import { connection } from 'next/server'
|
|
6
|
+
import { ProfileForm } from './profile-form'
|
|
7
|
+
|
|
8
|
+
export default async function ProfilePage() {
|
|
9
|
+
await connection()
|
|
10
|
+
const session = await getSession()
|
|
11
|
+
|
|
12
|
+
if (!session?.user) {
|
|
13
|
+
redirect('/cms/login')
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const showPageHeader =
|
|
17
|
+
session.user.role === UserRole.ADMIN ||
|
|
18
|
+
session.user.role === UserRole.EDITOR
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<>
|
|
22
|
+
{showPageHeader && <PageHeader title="Profile" />}
|
|
23
|
+
<main className="container mx-auto max-w-2xl p-6 pb-20">
|
|
24
|
+
<ProfileForm
|
|
25
|
+
user={{
|
|
26
|
+
name: session.user.name,
|
|
27
|
+
email: session.user.email,
|
|
28
|
+
image: session.user.image ?? null,
|
|
29
|
+
}}
|
|
30
|
+
/>
|
|
31
|
+
</main>
|
|
32
|
+
</>
|
|
33
|
+
)
|
|
34
|
+
}
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { deleteUser } from '@cms/actions/users'
|
|
4
|
+
import {
|
|
5
|
+
AlertDialog,
|
|
6
|
+
AlertDialogAction,
|
|
7
|
+
AlertDialogCancel,
|
|
8
|
+
AlertDialogContent,
|
|
9
|
+
AlertDialogDescription,
|
|
10
|
+
AlertDialogFooter,
|
|
11
|
+
AlertDialogHeader,
|
|
12
|
+
AlertDialogTitle,
|
|
13
|
+
AlertDialogTrigger,
|
|
14
|
+
} from '@cms/components/ui/alert-dialog'
|
|
15
|
+
import { Avatar, AvatarFallback } from '@cms/components/ui/avatar'
|
|
16
|
+
import { Badge } from '@cms/components/ui/badge'
|
|
17
|
+
import { Button } from '@cms/components/ui/button'
|
|
18
|
+
import {
|
|
19
|
+
DropdownMenu,
|
|
20
|
+
DropdownMenuContent,
|
|
21
|
+
DropdownMenuItem,
|
|
22
|
+
DropdownMenuLabel,
|
|
23
|
+
DropdownMenuSeparator,
|
|
24
|
+
DropdownMenuTrigger,
|
|
25
|
+
} from '@cms/components/ui/dropdown-menu'
|
|
26
|
+
import type { UserData } from '@cms/types/auth'
|
|
27
|
+
import { useQueryClient } from '@tanstack/react-query'
|
|
28
|
+
import type { ColumnDef } from '@tanstack/react-table'
|
|
29
|
+
import { ArrowUpDown, Edit, MoreHorizontal, Trash } from 'lucide-react'
|
|
30
|
+
import React from 'react'
|
|
31
|
+
import { toast } from 'sonner'
|
|
32
|
+
import { EditRoleDialog } from './edit-role-dialog'
|
|
33
|
+
|
|
34
|
+
function getInitials(nameOrEmail: string): string {
|
|
35
|
+
const parts = nameOrEmail.includes('@')
|
|
36
|
+
? nameOrEmail.split('@')[0].split(/[._-]/)
|
|
37
|
+
: nameOrEmail.split(' ')
|
|
38
|
+
return parts
|
|
39
|
+
.filter(Boolean)
|
|
40
|
+
.slice(0, 2)
|
|
41
|
+
.map((p) => p[0].toUpperCase())
|
|
42
|
+
.join('')
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function DeleteUserAction({
|
|
46
|
+
userId,
|
|
47
|
+
userName,
|
|
48
|
+
isCurrentUser,
|
|
49
|
+
}: {
|
|
50
|
+
userId: string
|
|
51
|
+
userName: string
|
|
52
|
+
isCurrentUser: boolean
|
|
53
|
+
}) {
|
|
54
|
+
const [open, setOpen] = React.useState(false)
|
|
55
|
+
const [isPending, startTransition] = React.useTransition()
|
|
56
|
+
const queryClient = useQueryClient()
|
|
57
|
+
|
|
58
|
+
if (isCurrentUser) return null
|
|
59
|
+
|
|
60
|
+
const handleDelete = () => {
|
|
61
|
+
startTransition(async () => {
|
|
62
|
+
try {
|
|
63
|
+
const result = await deleteUser(userId)
|
|
64
|
+
|
|
65
|
+
if (result.success) {
|
|
66
|
+
toast.success('User deleted successfully')
|
|
67
|
+
queryClient.refetchQueries({ queryKey: ['users'] })
|
|
68
|
+
setOpen(false)
|
|
69
|
+
} else {
|
|
70
|
+
toast.error(result.error || 'Failed to delete user')
|
|
71
|
+
}
|
|
72
|
+
} catch (error) {
|
|
73
|
+
toast.error('An error occurred')
|
|
74
|
+
console.error(error)
|
|
75
|
+
}
|
|
76
|
+
})
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<AlertDialog open={open} onOpenChange={setOpen}>
|
|
81
|
+
<AlertDialogTrigger asChild>
|
|
82
|
+
<DropdownMenuItem className="text-destructive" onSelect={(e) => e.preventDefault()}>
|
|
83
|
+
<Trash className="size-4 mr-2" />
|
|
84
|
+
Delete user
|
|
85
|
+
</DropdownMenuItem>
|
|
86
|
+
</AlertDialogTrigger>
|
|
87
|
+
<AlertDialogContent>
|
|
88
|
+
<AlertDialogHeader>
|
|
89
|
+
<AlertDialogTitle>Are you sure?</AlertDialogTitle>
|
|
90
|
+
<AlertDialogDescription>
|
|
91
|
+
This action cannot be undone. This will permanently delete <strong>{userName}</strong>{' '}
|
|
92
|
+
and all of their data.
|
|
93
|
+
</AlertDialogDescription>
|
|
94
|
+
</AlertDialogHeader>
|
|
95
|
+
<AlertDialogFooter>
|
|
96
|
+
<AlertDialogCancel disabled={isPending}>Cancel</AlertDialogCancel>
|
|
97
|
+
<AlertDialogAction
|
|
98
|
+
onClick={(e) => {
|
|
99
|
+
e.preventDefault()
|
|
100
|
+
handleDelete()
|
|
101
|
+
}}
|
|
102
|
+
disabled={isPending}
|
|
103
|
+
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
|
104
|
+
>
|
|
105
|
+
{isPending ? 'Deleting...' : 'Delete'}
|
|
106
|
+
</AlertDialogAction>
|
|
107
|
+
</AlertDialogFooter>
|
|
108
|
+
</AlertDialogContent>
|
|
109
|
+
</AlertDialog>
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export const columns: ColumnDef<UserData>[] = [
|
|
114
|
+
{
|
|
115
|
+
accessorKey: 'email',
|
|
116
|
+
header: ({ column }) => (
|
|
117
|
+
<Button
|
|
118
|
+
variant="ghost"
|
|
119
|
+
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
|
|
120
|
+
className="hover:bg-muted/50 px-0!"
|
|
121
|
+
>
|
|
122
|
+
User
|
|
123
|
+
<ArrowUpDown className="size-4" />
|
|
124
|
+
</Button>
|
|
125
|
+
),
|
|
126
|
+
cell: ({ row }) => {
|
|
127
|
+
const email = row.getValue('email') as string
|
|
128
|
+
const name = row.original.name
|
|
129
|
+
return (
|
|
130
|
+
<div className="flex items-center gap-3">
|
|
131
|
+
<Avatar className="size-8">
|
|
132
|
+
<AvatarFallback>{getInitials(name || email)}</AvatarFallback>
|
|
133
|
+
</Avatar>
|
|
134
|
+
<div className="flex flex-col">
|
|
135
|
+
<div className="font-medium">{name || 'N/A'}</div>
|
|
136
|
+
<div className="text-muted-foreground text-sm">{email}</div>
|
|
137
|
+
</div>
|
|
138
|
+
</div>
|
|
139
|
+
)
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
accessorKey: 'emailVerified',
|
|
144
|
+
header: 'Status',
|
|
145
|
+
cell: ({ row }) => {
|
|
146
|
+
const verified = row.getValue('emailVerified') as boolean
|
|
147
|
+
return (
|
|
148
|
+
<Badge variant={verified ? 'outline' : 'secondary'}>
|
|
149
|
+
{verified ? 'Verified' : 'Unverified'}
|
|
150
|
+
</Badge>
|
|
151
|
+
)
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
accessorKey: 'role',
|
|
156
|
+
header: 'Role',
|
|
157
|
+
cell: ({ row, table }) => {
|
|
158
|
+
const role = row.getValue('role') as string
|
|
159
|
+
const currentUser = table.options.meta?.currentUser
|
|
160
|
+
const isCurrentUser = currentUser?.email === row.original.email
|
|
161
|
+
|
|
162
|
+
if (isCurrentUser) {
|
|
163
|
+
return (
|
|
164
|
+
<Badge variant="outline" className="capitalize">
|
|
165
|
+
{role} (you)
|
|
166
|
+
</Badge>
|
|
167
|
+
)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return (
|
|
171
|
+
<EditRoleDialog
|
|
172
|
+
userId={row.original.id}
|
|
173
|
+
currentRole={row.original.role}
|
|
174
|
+
userName={row.original.name}
|
|
175
|
+
>
|
|
176
|
+
<Badge variant="outline" className="capitalize cursor-pointer">
|
|
177
|
+
{role}
|
|
178
|
+
<Edit className="size-3 ml-1" strokeWidth={2} />
|
|
179
|
+
</Badge>
|
|
180
|
+
</EditRoleDialog>
|
|
181
|
+
)
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
accessorKey: 'createdAt',
|
|
186
|
+
header: ({ column }) => (
|
|
187
|
+
<Button
|
|
188
|
+
variant="ghost"
|
|
189
|
+
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
|
|
190
|
+
className="hover:bg-muted/50"
|
|
191
|
+
>
|
|
192
|
+
Joined
|
|
193
|
+
<ArrowUpDown className="size-4" />
|
|
194
|
+
</Button>
|
|
195
|
+
),
|
|
196
|
+
cell: ({ row }) => {
|
|
197
|
+
const date = new Date(row.getValue('createdAt'))
|
|
198
|
+
return (
|
|
199
|
+
<div className="text-sm">
|
|
200
|
+
{date.toLocaleDateString('en-US', {
|
|
201
|
+
month: 'short',
|
|
202
|
+
day: 'numeric',
|
|
203
|
+
year: 'numeric',
|
|
204
|
+
})}
|
|
205
|
+
</div>
|
|
206
|
+
)
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
id: 'actions',
|
|
211
|
+
cell: ({ row, table }) => {
|
|
212
|
+
const currentUser = table.options.meta?.currentUser
|
|
213
|
+
const isCurrentUser = currentUser?.email === row.original.email
|
|
214
|
+
|
|
215
|
+
return (
|
|
216
|
+
<div className="flex justify-end">
|
|
217
|
+
<DropdownMenu>
|
|
218
|
+
<DropdownMenuTrigger asChild>
|
|
219
|
+
<Button variant="ghost" className="size-8 p-0">
|
|
220
|
+
<span className="sr-only">Open menu</span>
|
|
221
|
+
<MoreHorizontal className="size-4" />
|
|
222
|
+
</Button>
|
|
223
|
+
</DropdownMenuTrigger>
|
|
224
|
+
<DropdownMenuContent align="end">
|
|
225
|
+
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
|
226
|
+
<DropdownMenuItem onClick={() => navigator.clipboard.writeText(row.original.id)}>
|
|
227
|
+
Copy user ID
|
|
228
|
+
</DropdownMenuItem>
|
|
229
|
+
<DropdownMenuSeparator />
|
|
230
|
+
<DeleteUserAction
|
|
231
|
+
userId={row.original.id}
|
|
232
|
+
userName={row.original.name}
|
|
233
|
+
isCurrentUser={isCurrentUser}
|
|
234
|
+
/>
|
|
235
|
+
</DropdownMenuContent>
|
|
236
|
+
</DropdownMenu>
|
|
237
|
+
</div>
|
|
238
|
+
)
|
|
239
|
+
},
|
|
240
|
+
},
|
|
241
|
+
]
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { createUser } from '@cms/actions/users'
|
|
4
|
+
import { Button } from '@cms/components/ui/button'
|
|
5
|
+
import {
|
|
6
|
+
Dialog,
|
|
7
|
+
DialogContent,
|
|
8
|
+
DialogDescription,
|
|
9
|
+
DialogHeader,
|
|
10
|
+
DialogTitle,
|
|
11
|
+
DialogTrigger,
|
|
12
|
+
} from '@cms/components/ui/dialog'
|
|
13
|
+
import { Input } from '@cms/components/ui/input'
|
|
14
|
+
import { Label } from '@cms/components/ui/label'
|
|
15
|
+
import { useQueryClient } from '@tanstack/react-query'
|
|
16
|
+
import { Loader2, UserPlus } from 'lucide-react'
|
|
17
|
+
import * as React from 'react'
|
|
18
|
+
import { toast } from 'sonner'
|
|
19
|
+
|
|
20
|
+
export function CreateUserDialog() {
|
|
21
|
+
const [open, setOpen] = React.useState(false)
|
|
22
|
+
const [isPending, startTransition] = React.useTransition()
|
|
23
|
+
const queryClient = useQueryClient()
|
|
24
|
+
|
|
25
|
+
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
|
26
|
+
e.preventDefault()
|
|
27
|
+
const formData = new FormData(e.currentTarget)
|
|
28
|
+
const email = formData.get('email') as string
|
|
29
|
+
const name = formData.get('name') as string
|
|
30
|
+
const password = formData.get('password') as string
|
|
31
|
+
|
|
32
|
+
startTransition(async () => {
|
|
33
|
+
try {
|
|
34
|
+
const result = await createUser({ email, name, password })
|
|
35
|
+
if (result.success) {
|
|
36
|
+
toast.success('User created successfully')
|
|
37
|
+
queryClient.refetchQueries({ queryKey: ['users'] })
|
|
38
|
+
setOpen(false)
|
|
39
|
+
} else {
|
|
40
|
+
toast.error(result.error || 'Failed to create user')
|
|
41
|
+
}
|
|
42
|
+
} catch {
|
|
43
|
+
toast.error('An unexpected error occurred')
|
|
44
|
+
}
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<Dialog open={open} onOpenChange={setOpen}>
|
|
50
|
+
<DialogTrigger asChild>
|
|
51
|
+
<Button>
|
|
52
|
+
<UserPlus className="size-3.5 -ml-0.5" strokeWidth={2} />
|
|
53
|
+
Create User
|
|
54
|
+
</Button>
|
|
55
|
+
</DialogTrigger>
|
|
56
|
+
<DialogContent className="sm:max-w-[425px]">
|
|
57
|
+
<DialogHeader>
|
|
58
|
+
<DialogTitle>Create New User</DialogTitle>
|
|
59
|
+
<DialogDescription>
|
|
60
|
+
Add a new user to the CMS. They can sign in with these credentials.
|
|
61
|
+
</DialogDescription>
|
|
62
|
+
</DialogHeader>
|
|
63
|
+
<form onSubmit={handleSubmit} className="space-y-4">
|
|
64
|
+
<div className="space-y-2">
|
|
65
|
+
<Label htmlFor="create-email">Email</Label>
|
|
66
|
+
<Input
|
|
67
|
+
id="create-email"
|
|
68
|
+
name="email"
|
|
69
|
+
type="email"
|
|
70
|
+
placeholder="user@example.com"
|
|
71
|
+
required
|
|
72
|
+
disabled={isPending}
|
|
73
|
+
/>
|
|
74
|
+
</div>
|
|
75
|
+
<div className="space-y-2">
|
|
76
|
+
<Label htmlFor="create-name">Name</Label>
|
|
77
|
+
<Input
|
|
78
|
+
id="create-name"
|
|
79
|
+
name="name"
|
|
80
|
+
type="text"
|
|
81
|
+
placeholder="Full name"
|
|
82
|
+
required
|
|
83
|
+
disabled={isPending}
|
|
84
|
+
/>
|
|
85
|
+
</div>
|
|
86
|
+
<div className="space-y-2">
|
|
87
|
+
<Label htmlFor="create-password">Password</Label>
|
|
88
|
+
<Input
|
|
89
|
+
id="create-password"
|
|
90
|
+
name="password"
|
|
91
|
+
type="password"
|
|
92
|
+
placeholder="Min 8 characters"
|
|
93
|
+
minLength={8}
|
|
94
|
+
required
|
|
95
|
+
disabled={isPending}
|
|
96
|
+
/>
|
|
97
|
+
</div>
|
|
98
|
+
<div className="flex justify-end gap-2 pt-2">
|
|
99
|
+
<Button
|
|
100
|
+
type="button"
|
|
101
|
+
variant="outline"
|
|
102
|
+
onClick={() => setOpen(false)}
|
|
103
|
+
disabled={isPending}
|
|
104
|
+
>
|
|
105
|
+
Cancel
|
|
106
|
+
</Button>
|
|
107
|
+
<Button type="submit" disabled={isPending}>
|
|
108
|
+
{isPending && <Loader2 className="size-4 mr-1 animate-spin" />}
|
|
109
|
+
Create User
|
|
110
|
+
</Button>
|
|
111
|
+
</div>
|
|
112
|
+
</form>
|
|
113
|
+
</DialogContent>
|
|
114
|
+
</Dialog>
|
|
115
|
+
)
|
|
116
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { updateUserRole } from '@cms/actions/users'
|
|
4
|
+
import { Button } from '@cms/components/ui/button'
|
|
5
|
+
import {
|
|
6
|
+
Dialog,
|
|
7
|
+
DialogContent,
|
|
8
|
+
DialogDescription,
|
|
9
|
+
DialogFooter,
|
|
10
|
+
DialogHeader,
|
|
11
|
+
DialogTitle,
|
|
12
|
+
DialogTrigger,
|
|
13
|
+
} from '@cms/components/ui/dialog'
|
|
14
|
+
import { Label } from '@cms/components/ui/label'
|
|
15
|
+
import {
|
|
16
|
+
Select,
|
|
17
|
+
SelectContent,
|
|
18
|
+
SelectItem,
|
|
19
|
+
SelectTrigger,
|
|
20
|
+
SelectValue,
|
|
21
|
+
} from '@cms/components/ui/select'
|
|
22
|
+
import { UserRole } from '@cms/types/auth'
|
|
23
|
+
import { useQueryClient } from '@tanstack/react-query'
|
|
24
|
+
import { Loader2 } from 'lucide-react'
|
|
25
|
+
import * as React from 'react'
|
|
26
|
+
import { toast } from 'sonner'
|
|
27
|
+
|
|
28
|
+
interface EditRoleDialogProps {
|
|
29
|
+
userId: string
|
|
30
|
+
currentRole: string
|
|
31
|
+
userName: string
|
|
32
|
+
children: React.ReactNode
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function EditRoleDialog({ userId, currentRole, userName, children }: EditRoleDialogProps) {
|
|
36
|
+
const [open, setOpen] = React.useState(false)
|
|
37
|
+
const [role, setRole] = React.useState(currentRole)
|
|
38
|
+
const [isPending, startTransition] = React.useTransition()
|
|
39
|
+
const queryClient = useQueryClient()
|
|
40
|
+
|
|
41
|
+
const handleSave = () => {
|
|
42
|
+
startTransition(async () => {
|
|
43
|
+
try {
|
|
44
|
+
const result = await updateUserRole(userId, role as UserRole)
|
|
45
|
+
if (result.success) {
|
|
46
|
+
toast.success(`Role updated for ${userName}`)
|
|
47
|
+
queryClient.refetchQueries({ queryKey: ['users'] })
|
|
48
|
+
setOpen(false)
|
|
49
|
+
} else {
|
|
50
|
+
toast.error(result.error || 'Failed to update role')
|
|
51
|
+
}
|
|
52
|
+
} catch {
|
|
53
|
+
toast.error('An unexpected error occurred')
|
|
54
|
+
}
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<Dialog open={open} onOpenChange={setOpen}>
|
|
60
|
+
<DialogTrigger asChild>{children}</DialogTrigger>
|
|
61
|
+
<DialogContent className="sm:max-w-87.5">
|
|
62
|
+
<DialogHeader>
|
|
63
|
+
<DialogTitle>Edit Role</DialogTitle>
|
|
64
|
+
<DialogDescription>Change the role for {userName}</DialogDescription>
|
|
65
|
+
</DialogHeader>
|
|
66
|
+
|
|
67
|
+
<div className="p-2">
|
|
68
|
+
<Select value={role} onValueChange={setRole}>
|
|
69
|
+
<SelectTrigger className="w-full">
|
|
70
|
+
<SelectValue />
|
|
71
|
+
</SelectTrigger>
|
|
72
|
+
<SelectContent>
|
|
73
|
+
<SelectItem value={UserRole.ADMIN}>Admin</SelectItem>
|
|
74
|
+
<SelectItem value={UserRole.EDITOR}>Editor</SelectItem>
|
|
75
|
+
<SelectItem value={UserRole.MEMBER}>Member</SelectItem>
|
|
76
|
+
</SelectContent>
|
|
77
|
+
</Select>
|
|
78
|
+
</div>
|
|
79
|
+
|
|
80
|
+
<DialogFooter>
|
|
81
|
+
<Button variant="outline" onClick={() => setOpen(false)} disabled={isPending}>
|
|
82
|
+
Cancel
|
|
83
|
+
</Button>
|
|
84
|
+
<Button onClick={handleSave} disabled={isPending || role === currentRole}>
|
|
85
|
+
{isPending && <Loader2 className="size-4 animate-spin" />}
|
|
86
|
+
Save Role
|
|
87
|
+
</Button>
|
|
88
|
+
</DialogFooter>
|
|
89
|
+
</DialogContent>
|
|
90
|
+
</Dialog>
|
|
91
|
+
)
|
|
92
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import type { ColumnDef } from '@tanstack/react-table'
|
|
4
|
+
import { PageHeader } from '@cms/components/shared/page-header'
|
|
5
|
+
import type { UserData } from '@cms/types/auth'
|
|
6
|
+
import { CreateUserDialog } from './create-user-dialog'
|
|
7
|
+
import { UsersTable } from './users-table'
|
|
8
|
+
|
|
9
|
+
interface UsersPageContentProps<TValue> {
|
|
10
|
+
columns: ColumnDef<UserData, TValue>[]
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function UsersPageContent<TValue>({ columns }: UsersPageContentProps<TValue>) {
|
|
14
|
+
return (
|
|
15
|
+
<>
|
|
16
|
+
<PageHeader
|
|
17
|
+
title="Users"
|
|
18
|
+
actions={
|
|
19
|
+
<div className="flex items-center gap-2">
|
|
20
|
+
<CreateUserDialog />
|
|
21
|
+
</div>
|
|
22
|
+
}
|
|
23
|
+
/>
|
|
24
|
+
<main className="space-y-6 p-6">
|
|
25
|
+
<UsersTable columns={columns} />
|
|
26
|
+
</main>
|
|
27
|
+
</>
|
|
28
|
+
)
|
|
29
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
import { columns } from './columns'
|
|
3
|
+
import { UsersPageContent } from './users-page-content'
|
|
4
|
+
|
|
5
|
+
export default function UsersPage() {
|
|
6
|
+
return (
|
|
7
|
+
<React.Suspense
|
|
8
|
+
fallback={
|
|
9
|
+
<div className="flex items-center justify-center h-48">
|
|
10
|
+
<div className="text-muted-foreground">Loading Users...</div>
|
|
11
|
+
</div>
|
|
12
|
+
}
|
|
13
|
+
>
|
|
14
|
+
<div className="flex flex-col">
|
|
15
|
+
<UsersPageContent columns={columns} />
|
|
16
|
+
</div>
|
|
17
|
+
</React.Suspense>
|
|
18
|
+
)
|
|
19
|
+
}
|