Skip to content

Commit aaabc65

Browse files
committed
feat: add customer page for admin, change user role
1 parent 05a4468 commit aaabc65

File tree

7 files changed

+287
-13
lines changed

7 files changed

+287
-13
lines changed

src/appdata/list.ts

+1
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
export const STATUSES = ['PENDING', 'SHIPPING', 'SUCCESS', 'CANCELED']
2+
export const ROLES = ['ADMIN', 'USER', 'SUPERADMIN']

src/pages/api/hello.ts

-13
This file was deleted.

src/pages/dashboard/customer/[id].tsx

+103
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import { zodResolver } from '@hookform/resolvers/zod'
2+
import { useRouter } from 'next/router'
3+
import { useForm } from 'react-hook-form'
4+
import { toast } from 'react-hot-toast'
5+
6+
import { ROLES } from '@/appdata/list'
7+
import { AdminLayout } from '@/components/common/layout/AdminLayout'
8+
import { SelectInput } from '@/components/common/Select'
9+
import type { GetUser } from '@/types/user'
10+
import { getUser } from '@/types/user'
11+
import { trpc } from '@/utils/trpc'
12+
13+
export default function Example() {
14+
const router = useRouter()
15+
const utils = trpc.useContext()
16+
const userDetails = trpc.user.getById.useQuery({
17+
id: router.query.id as string,
18+
})
19+
const updateUserRole = trpc.user.updateUserRole.useMutation()
20+
const {
21+
register,
22+
handleSubmit,
23+
formState: { errors, isSubmitting },
24+
} = useForm<GetUser>({
25+
resolver: zodResolver(getUser),
26+
defaultValues: {
27+
role: userDetails.data?.role,
28+
},
29+
})
30+
async function submit(val: GetUser) {
31+
if (val.role === userDetails.data?.role) {
32+
toast.error('User role is not changed')
33+
return
34+
}
35+
await updateUserRole.mutate(
36+
{ id: router.query.id as string, ...val },
37+
{
38+
onSuccess() {
39+
toast.success('Changed user role')
40+
utils.user.getById.invalidate({
41+
id: router.query.id as string,
42+
})
43+
},
44+
}
45+
)
46+
}
47+
return (
48+
<AdminLayout>
49+
<section className="section" aria-labelledby="page-title">
50+
<h1 id="page-title" className="heading1">
51+
User: {router.query.id}
52+
</h1>
53+
{userDetails.isLoading && <p>Loading . . .</p>}
54+
{userDetails.error && (
55+
<p className="text-red-500">{userDetails.error.message}</p>
56+
)}
57+
{userDetails.data && (
58+
<div className="mx-auto my-4 flow-root max-w-7xl rounded border border-gray-300 p-4 dark:border-gray-700">
59+
<img
60+
src={userDetails.data.image ?? '/unknown.webp'}
61+
alt={userDetails.data.name || 'random'}
62+
width="50"
63+
height="50"
64+
className="float-left rounded-full"
65+
/>
66+
<dl className="grid grid-cols-[max-content,max-content] gap-x-4 space-y-0.5">
67+
{userDetails.data.name && (
68+
<>
69+
<dt>Name</dt>
70+
<dd>{userDetails.data.name}</dd>
71+
</>
72+
)}
73+
<dt>Email</dt>
74+
<dd className="truncate">{userDetails.data.email}</dd>
75+
<dt>Created on</dt>
76+
<dd>
77+
{userDetails.data.createdAt?.toISOString().substring(0, 10)}
78+
</dd>
79+
</dl>
80+
<form
81+
onSubmit={handleSubmit((v) => submit(v))}
82+
className="gap-4 border-t border-gray-300 p-2 font-medium tracking-wider dark:border-gray-700"
83+
>
84+
<SelectInput
85+
label="Roles"
86+
options={ROLES}
87+
error={errors.role}
88+
{...register('role')}
89+
/>
90+
<button
91+
disabled={isSubmitting}
92+
type="submit"
93+
className="mt-2 max-w-fit self-center rounded-2xl bg-indigo-700 px-4 py-1.5 text-sm text-white transition hover:shadow-lg dark:bg-indigo-500"
94+
>
95+
Update
96+
</button>
97+
</form>
98+
</div>
99+
)}
100+
</section>
101+
</AdminLayout>
102+
)
103+
}
+105
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import type { ROLES as PROLES } from '@prisma/client'
2+
import type { ColumnDef } from '@tanstack/react-table'
3+
import Link from 'next/link'
4+
import { useMemo } from 'react'
5+
import { AiFillEdit } from 'react-icons/ai'
6+
7+
import { AdminLayout } from '@/components/common/layout/AdminLayout'
8+
import { Table } from '@/components/dashboard/common/Table'
9+
import { trpc } from '@/utils/trpc'
10+
11+
type User = {
12+
id: string
13+
image: string | null
14+
name: string | null
15+
email: string | null
16+
role: PROLES
17+
createdAt: Date | null
18+
}
19+
export default function Example() {
20+
const userList = trpc.user.getAll.useQuery()
21+
22+
const columns = useMemo<ColumnDef<User>[]>(
23+
() => [
24+
{
25+
header: 'Name',
26+
accessorKey: 'name',
27+
enableSorting: false,
28+
enableGlobalFilter: false,
29+
accessorFn: (row) => row.name,
30+
cell: (props) => (
31+
<div className="flex items-center space-x-1">
32+
<img
33+
src={props.row.original.image ?? '/unknown.webp'}
34+
alt={props.row.original.name || 'random'}
35+
width="50"
36+
height="50"
37+
className="rounded-full"
38+
/>
39+
<span className="whitespace-nowrap">{props.row.original.name}</span>
40+
</div>
41+
),
42+
},
43+
{
44+
header: 'Email',
45+
accessorKey: 'email',
46+
accessorFn: (row) => row.email,
47+
cell: (props) => (
48+
<span className="whitespace-nowrap">{props.row.original.email}</span>
49+
),
50+
},
51+
{
52+
header: 'Roles',
53+
accessorKey: 'registered',
54+
cell: (props) => (
55+
<span className="font-medium tracking-wider">
56+
{props.row.original.role}
57+
</span>
58+
),
59+
},
60+
{
61+
header: 'Created at',
62+
accessorKey: 'createdAt',
63+
enableSorting: false,
64+
enableGlobalFilter: false,
65+
cell: (props) => (
66+
<span>
67+
{props.row.original.createdAt?.toISOString().substring(0, 10)}
68+
</span>
69+
),
70+
},
71+
{
72+
header: 'Details',
73+
accessorKey: 'details',
74+
enableSorting: false,
75+
enableGlobalFilter: false,
76+
cell: (props) => (
77+
<Link
78+
href={`/dashboard/customer/${props.row.original.id}`}
79+
className="flex max-w-max items-center justify-center rounded-full bg-indigo-600 p-1 transition hover:scale-105 dark:bg-indigo-500"
80+
>
81+
<AiFillEdit aria-hidden="true" className="h-5 w-5 text-white" />
82+
<p className="sr-only">Order Details {props.row.original.id}</p>
83+
</Link>
84+
),
85+
},
86+
],
87+
[]
88+
)
89+
return (
90+
<AdminLayout>
91+
<section className="section" aria-labelledby="page-title">
92+
<h1 id="page-title" className="heading1">
93+
User list
94+
</h1>
95+
<div className="w-full p-4">
96+
{userList.isLoading && <p>Loading . . .</p>}
97+
{userList.error && <p>{userList.error.message}</p>}
98+
{!userList.isError && !userList.isLoading && (
99+
<Table columns={columns} data={userList.data || []} />
100+
)}
101+
</div>
102+
</section>
103+
</AdminLayout>
104+
)
105+
}

src/server/router/_app.ts

+2
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@ import { orderRouter } from './order'
44
import { productRouter } from './product'
55
import { reviewRouter } from './review'
66
import { stripeRouter } from './stripe'
7+
import { userRouter } from './user'
78

89
export const appRouter = router({
910
auth: authRouter,
1011
product: productRouter,
1112
stripe: stripeRouter,
1213
order: orderRouter,
1314
review: reviewRouter,
15+
user: userRouter,
1416
})
1517
export type AppRouter = typeof appRouter

src/server/router/user.ts

+64
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { z } from 'zod'
2+
3+
import { ROLES } from '@/appdata/list'
4+
5+
import {
6+
protectedAdminProcedure,
7+
protectedSuperAdminProcedure,
8+
router,
9+
} from '../trpc'
10+
11+
export const userRouter = router({
12+
updateUserRole: protectedSuperAdminProcedure
13+
.input(
14+
z.object({
15+
id: z.string(),
16+
role: z
17+
.string({ invalid_type_error: 'Please select valid status' })
18+
.refine((val) => ROLES.map((c) => c).includes(val as any), {
19+
message: 'Please select valid role',
20+
}),
21+
})
22+
)
23+
.mutation(async ({ input, ctx }) =>
24+
ctx.prisma.user.update({
25+
where: {
26+
id: input.id,
27+
},
28+
data: {
29+
role: input.role as any,
30+
},
31+
})
32+
),
33+
getById: protectedAdminProcedure
34+
.input(
35+
z.object({
36+
id: z.string(),
37+
})
38+
)
39+
.query(async ({ input, ctx }) =>
40+
ctx.prisma.user.findUnique({
41+
where: { id: input.id },
42+
select: {
43+
id: true,
44+
name: true,
45+
createdAt: true,
46+
image: true,
47+
email: true,
48+
role: true,
49+
},
50+
})
51+
),
52+
getAll: protectedAdminProcedure.query(({ ctx }) =>
53+
ctx.prisma.user.findMany({
54+
select: {
55+
id: true,
56+
name: true,
57+
createdAt: true,
58+
image: true,
59+
email: true,
60+
role: true,
61+
},
62+
})
63+
),
64+
})

src/types/user.ts

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { z } from 'zod'
2+
3+
import { ROLES } from '@/appdata/list'
4+
5+
export const getUser = z.object({
6+
role: z
7+
.string({ invalid_type_error: 'Please select valid status' })
8+
.refine((val) => ROLES.map((c) => c).includes(val as any), {
9+
message: 'Please select valid role',
10+
}),
11+
})
12+
export type GetUser = z.infer<typeof getUser>

0 commit comments

Comments
 (0)