refactor: implement custom hooks for user management and ban/unban user
This commit is contained in:
parent
c00223c79a
commit
d153ca655e
@ -6,7 +6,6 @@ import {
|
||||
Drawer,
|
||||
DrawerClose,
|
||||
DrawerContent,
|
||||
DrawerDescription,
|
||||
DrawerFooter,
|
||||
DrawerHeader,
|
||||
DrawerTitle,
|
||||
@ -21,13 +20,12 @@ import {
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { useIsMobile } from '@/hooks/use-mobile';
|
||||
import { authClient } from '@/lib/auth-client';
|
||||
import { useBanUser, useUnbanUser } from '@/hooks/use-users';
|
||||
import type { User } from '@/lib/auth-types';
|
||||
import { isDemoWebsite } from '@/lib/demo';
|
||||
import { formatDate } from '@/lib/formatter';
|
||||
import { getStripeDashboardCustomerUrl } from '@/lib/urls/urls';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useUsersStore } from '@/stores/users-store';
|
||||
import {
|
||||
CalendarIcon,
|
||||
Loader2Icon,
|
||||
@ -47,11 +45,13 @@ interface UserDetailViewerProps {
|
||||
export function UserDetailViewer({ user }: UserDetailViewerProps) {
|
||||
const t = useTranslations('Dashboard.admin.users');
|
||||
const isMobile = useIsMobile();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | undefined>();
|
||||
const [banReason, setBanReason] = useState(t('ban.defaultReason'));
|
||||
const [banExpiresAt, setBanExpiresAt] = useState<Date | undefined>();
|
||||
const triggerRefresh = useUsersStore((state) => state.triggerRefresh);
|
||||
|
||||
// TanStack Query mutations
|
||||
const banUserMutation = useBanUser();
|
||||
const unbanUserMutation = useUnbanUser();
|
||||
|
||||
// show fake data in demo website
|
||||
const isDemo = isDemoWebsite();
|
||||
@ -67,11 +67,10 @@ export function UserDetailViewer({ user }: UserDetailViewerProps) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
setError('');
|
||||
|
||||
try {
|
||||
await authClient.admin.banUser({
|
||||
await banUserMutation.mutateAsync({
|
||||
userId: user.id,
|
||||
banReason,
|
||||
banExpiresIn: banExpiresAt
|
||||
@ -83,15 +82,11 @@ export function UserDetailViewer({ user }: UserDetailViewerProps) {
|
||||
// Reset form
|
||||
setBanReason('');
|
||||
setBanExpiresAt(undefined);
|
||||
// Trigger refresh
|
||||
triggerRefresh();
|
||||
} catch (err) {
|
||||
const error = err as Error;
|
||||
console.error('Failed to ban user:', error);
|
||||
setError(error.message || t('ban.error'));
|
||||
toast.error(error.message || t('ban.error'));
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
@ -101,24 +96,19 @@ export function UserDetailViewer({ user }: UserDetailViewerProps) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
setError('');
|
||||
|
||||
try {
|
||||
await authClient.admin.unbanUser({
|
||||
await unbanUserMutation.mutateAsync({
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
toast.success(t('unban.success'));
|
||||
// Trigger refresh
|
||||
triggerRefresh();
|
||||
} catch (err) {
|
||||
const error = err as Error;
|
||||
console.error('Failed to unban user:', error);
|
||||
setError(error.message || t('unban.error'));
|
||||
toast.error(error.message || t('unban.error'));
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
@ -166,7 +156,7 @@ export function UserDetailViewer({ user }: UserDetailViewerProps) {
|
||||
{user.role === 'admin' ? t('admin') : t('user')}
|
||||
</Badge>
|
||||
{/* email verified */}
|
||||
<Badge variant="outline" className="px-1.5 hover:bg-accent">
|
||||
{/* <Badge variant="outline" className="px-1.5 hover:bg-accent">
|
||||
{user.emailVerified ? (
|
||||
<MailCheckIcon className="stroke-green-500 dark:stroke-green-400" />
|
||||
) : (
|
||||
@ -175,7 +165,7 @@ export function UserDetailViewer({ user }: UserDetailViewerProps) {
|
||||
{user.emailVerified
|
||||
? t('email.verified')
|
||||
: t('email.unverified')}
|
||||
</Badge>
|
||||
</Badge> */}
|
||||
|
||||
{/* user banned */}
|
||||
<div className="flex items-center gap-2">
|
||||
@ -196,15 +186,23 @@ export function UserDetailViewer({ user }: UserDetailViewerProps) {
|
||||
<span className="text-muted-foreground text-xs">
|
||||
{t('columns.email')}:
|
||||
</span>
|
||||
<span
|
||||
className="break-words cursor-pointer hover:bg-accent px-2 py-1 rounded border"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(user.email!);
|
||||
toast.success(t('emailCopied'));
|
||||
}}
|
||||
>
|
||||
{user.email}
|
||||
</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="text-sm px-1.5 cursor-pointer hover:bg-accent"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(user.email);
|
||||
toast.success(t('emailCopied'));
|
||||
}}
|
||||
>
|
||||
{user.emailVerified ? (
|
||||
<MailCheckIcon className="stroke-green-500 dark:stroke-green-400" />
|
||||
) : (
|
||||
<MailQuestionIcon className="stroke-red-500 dark:stroke-red-400" />
|
||||
)}
|
||||
{user.email}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -256,10 +254,10 @@ export function UserDetailViewer({ user }: UserDetailViewerProps) {
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={handleUnban}
|
||||
disabled={isLoading || isDemo}
|
||||
disabled={unbanUserMutation.isPending || isDemo}
|
||||
className="mt-4 cursor-pointer"
|
||||
>
|
||||
{isLoading && (
|
||||
{unbanUserMutation.isPending && (
|
||||
<Loader2Icon className="mr-2 size-4 animate-spin" />
|
||||
)}
|
||||
{t('unban.button')}
|
||||
@ -315,10 +313,10 @@ export function UserDetailViewer({ user }: UserDetailViewerProps) {
|
||||
<Button
|
||||
type="submit"
|
||||
variant="destructive"
|
||||
disabled={isLoading || !banReason || isDemo}
|
||||
disabled={banUserMutation.isPending || !banReason || isDemo}
|
||||
className="mt-4 cursor-pointer"
|
||||
>
|
||||
{isLoading && (
|
||||
{banUserMutation.isPending && (
|
||||
<Loader2Icon className="mr-2 size-4 animate-spin" />
|
||||
)}
|
||||
{t('ban.button')}
|
||||
|
@ -1,74 +1,34 @@
|
||||
'use client';
|
||||
|
||||
import { getUsersAction } from '@/actions/get-users';
|
||||
import { UsersTable } from '@/components/admin/users-table';
|
||||
import type { User } from '@/lib/auth-types';
|
||||
import { useUsersStore } from '@/stores/users-store';
|
||||
import { useUsers } from '@/hooks/use-users';
|
||||
import type { SortingState } from '@tanstack/react-table';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import { useState } from 'react';
|
||||
|
||||
export function UsersPageClient() {
|
||||
const t = useTranslations('Dashboard.admin.users');
|
||||
const [pageIndex, setPageIndex] = useState(0);
|
||||
const [pageSize, setPageSize] = useState(10);
|
||||
const [search, setSearch] = useState('');
|
||||
const [data, setData] = useState<User[]>([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [sorting, setSorting] = useState<SortingState>([
|
||||
{ id: 'createdAt', desc: true },
|
||||
]);
|
||||
const refreshTrigger = useUsersStore((state) => state.refreshTrigger);
|
||||
|
||||
const fetchUsers = useCallback(async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const result = await getUsersAction({
|
||||
pageIndex,
|
||||
pageSize,
|
||||
search,
|
||||
sorting,
|
||||
});
|
||||
|
||||
if (result?.data?.success) {
|
||||
setData(result.data.data?.items || []);
|
||||
setTotal(result.data.data?.total || 0);
|
||||
} else {
|
||||
const errorMessage = result?.data?.error || t('error');
|
||||
toast.error(errorMessage);
|
||||
setData([]);
|
||||
setTotal(0);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch users:', error);
|
||||
toast.error(t('error'));
|
||||
setData([]);
|
||||
setTotal(0);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [pageIndex, pageSize, search, sorting, refreshTrigger]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchUsers();
|
||||
}, [fetchUsers]);
|
||||
const { data, isLoading } = useUsers(pageIndex, pageSize, search, sorting);
|
||||
|
||||
return (
|
||||
<>
|
||||
<UsersTable
|
||||
data={data}
|
||||
total={total}
|
||||
pageIndex={pageIndex}
|
||||
pageSize={pageSize}
|
||||
search={search}
|
||||
loading={loading}
|
||||
onSearch={setSearch}
|
||||
onPageChange={setPageIndex}
|
||||
onPageSizeChange={setPageSize}
|
||||
onSortingChange={setSorting}
|
||||
/>
|
||||
</>
|
||||
<UsersTable
|
||||
data={data?.items || []}
|
||||
total={data?.total || 0}
|
||||
pageIndex={pageIndex}
|
||||
pageSize={pageSize}
|
||||
search={search}
|
||||
loading={isLoading}
|
||||
onSearch={setSearch}
|
||||
onPageChange={setPageIndex}
|
||||
onPageSizeChange={setPageSize}
|
||||
onSortingChange={setSorting}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
93
src/hooks/use-users.ts
Normal file
93
src/hooks/use-users.ts
Normal file
@ -0,0 +1,93 @@
|
||||
import { getUsersAction } from '@/actions/get-users';
|
||||
import { authClient } from '@/lib/auth-client';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import type { SortingState } from '@tanstack/react-table';
|
||||
|
||||
// Query keys
|
||||
export const usersKeys = {
|
||||
all: ['users'] as const,
|
||||
lists: () => [...usersKeys.all, 'list'] as const,
|
||||
list: (filters: {
|
||||
pageIndex: number;
|
||||
pageSize: number;
|
||||
search: string;
|
||||
sorting: SortingState;
|
||||
}) => [...usersKeys.lists(), filters] as const,
|
||||
};
|
||||
|
||||
// Hook to fetch users with pagination, search, and sorting
|
||||
export function useUsers(
|
||||
pageIndex: number,
|
||||
pageSize: number,
|
||||
search: string,
|
||||
sorting: SortingState
|
||||
) {
|
||||
return useQuery({
|
||||
queryKey: usersKeys.list({ pageIndex, pageSize, search, sorting }),
|
||||
queryFn: async () => {
|
||||
const result = await getUsersAction({
|
||||
pageIndex,
|
||||
pageSize,
|
||||
search,
|
||||
sorting,
|
||||
});
|
||||
|
||||
if (!result?.data?.success) {
|
||||
throw new Error(result?.data?.error || 'Failed to fetch users');
|
||||
}
|
||||
|
||||
return {
|
||||
items: result.data.data?.items || [],
|
||||
total: result.data.data?.total || 0,
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Hook to ban user
|
||||
export function useBanUser() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({
|
||||
userId,
|
||||
banReason,
|
||||
banExpiresIn,
|
||||
}: {
|
||||
userId: string;
|
||||
banReason: string;
|
||||
banExpiresIn?: number;
|
||||
}) => {
|
||||
return authClient.admin.banUser({
|
||||
userId,
|
||||
banReason,
|
||||
banExpiresIn,
|
||||
});
|
||||
},
|
||||
onSuccess: () => {
|
||||
// Invalidate all users queries to refresh the data
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: usersKeys.all,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Hook to unban user
|
||||
export function useUnbanUser() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({ userId }: { userId: string }) => {
|
||||
return authClient.admin.unbanUser({
|
||||
userId,
|
||||
});
|
||||
},
|
||||
onSuccess: () => {
|
||||
// Invalidate all users queries to refresh the data
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: usersKeys.all,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
Loading…
Reference in New Issue
Block a user