api ulangan
This commit is contained in:
180
src/features/users/ui/AddUsers.tsx
Normal file
180
src/features/users/ui/AddUsers.tsx
Normal file
@@ -0,0 +1,180 @@
|
||||
import { user_api } from "@/features/users/lib/api";
|
||||
import type { UserCreateReq, UserData } from "@/features/users/lib/data";
|
||||
import { AddedUser } from "@/features/users/lib/form";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormMessage,
|
||||
} from "@/shared/ui/form";
|
||||
import { Input } from "@/shared/ui/input";
|
||||
import { Label } from "@/shared/ui/label";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/shared/ui/select";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import type { AxiosError } from "axios";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import type z from "zod";
|
||||
|
||||
interface UserFormProps {
|
||||
initialData: UserData | null;
|
||||
setDialogOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
}
|
||||
|
||||
const AddUsers = ({ initialData, setDialogOpen }: UserFormProps) => {
|
||||
const form = useForm<z.infer<typeof AddedUser>>({
|
||||
resolver: zodResolver(AddedUser),
|
||||
defaultValues: {
|
||||
is_superuser: initialData?.is_superuser ? "true" : "false",
|
||||
password: "",
|
||||
username: initialData?.username || "",
|
||||
},
|
||||
});
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { mutate: update } = useMutation({
|
||||
mutationFn: ({ body, id }: { id: string; body: UserCreateReq }) =>
|
||||
user_api.update({ body, id }),
|
||||
onSuccess: () => {
|
||||
queryClient.refetchQueries({ queryKey: ["user_list"] });
|
||||
toast.success(`Foydalanuvchi tahrirlandi`);
|
||||
setDialogOpen(false);
|
||||
},
|
||||
onError: (err: AxiosError) => {
|
||||
const errMessage = err.response?.data as { message: string };
|
||||
const messageText = errMessage.message;
|
||||
toast.error(messageText || "Xatolik yuz berdi", {
|
||||
richColors: true,
|
||||
position: "top-center",
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const { mutate: create, isPending: createPending } = useMutation({
|
||||
mutationFn: (body: UserCreateReq) => user_api.create(body),
|
||||
onSuccess: () => {
|
||||
queryClient.refetchQueries({ queryKey: ["user_list"] });
|
||||
toast.success(`Foydalanuvchi qo'shildi`);
|
||||
setDialogOpen(false);
|
||||
},
|
||||
onError: (err: AxiosError) => {
|
||||
const errMessage = err.response?.data as { message: string };
|
||||
const messageText = errMessage.message;
|
||||
toast.error(messageText || "Xatolik yuz berdi", {
|
||||
richColors: true,
|
||||
position: "top-center",
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
function onSubmit(values: z.infer<typeof AddedUser>) {
|
||||
if (initialData) {
|
||||
update({
|
||||
body: {
|
||||
is_superuser: values.is_superuser === "true" ? true : false,
|
||||
username: values.username,
|
||||
password: values.password,
|
||||
},
|
||||
id: initialData.id,
|
||||
});
|
||||
} else if (initialData === null) {
|
||||
create({
|
||||
is_superuser: values.is_superuser === "true" ? true : false,
|
||||
username: values.username,
|
||||
password: values.password,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="username"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<Label className="text-md">Username</Label>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="Username"
|
||||
{...field}
|
||||
className="!h-12 !text-md"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="password"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<Label className="text-md">Parol</Label>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="Parol"
|
||||
{...field}
|
||||
className="!h-12 !text-md"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="is_superuser"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<Label className="text-md mr-4">Foydalanuvchi roli</Label>
|
||||
<FormControl>
|
||||
<Select
|
||||
value={String(field.value)}
|
||||
onValueChange={field.onChange}
|
||||
>
|
||||
<SelectTrigger className="w-full !h-12">
|
||||
<SelectValue placeholder="Foydalanuvchi roli" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="true">Admin</SelectItem>
|
||||
<SelectItem value="false">Foydalanuvchi</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full h-12 text-lg rounded-lg bg-blue-600 hover:bg-blue-600 cursor-pointer"
|
||||
>
|
||||
{createPending ? (
|
||||
<Loader2 className="animate-spin" />
|
||||
) : initialData ? (
|
||||
"Saqlash"
|
||||
) : (
|
||||
"Qo'shish"
|
||||
)}
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddUsers;
|
||||
89
src/features/users/ui/DeleteUser.tsx
Normal file
89
src/features/users/ui/DeleteUser.tsx
Normal file
@@ -0,0 +1,89 @@
|
||||
import { user_api } from "@/features/users/lib/api";
|
||||
import type { UserData } from "@/features/users/lib/data";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/shared/ui/dialog";
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import type { AxiosError } from "axios";
|
||||
import { Loader2, Trash, X } from "lucide-react";
|
||||
import type { Dispatch, SetStateAction } from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
interface Props {
|
||||
opneDelete: boolean;
|
||||
setOpenDelete: Dispatch<SetStateAction<boolean>>;
|
||||
setUserDelete: Dispatch<SetStateAction<UserData | null>>;
|
||||
userDelete: UserData | null;
|
||||
}
|
||||
|
||||
const DeleteUser = ({
|
||||
opneDelete,
|
||||
setOpenDelete,
|
||||
userDelete,
|
||||
setUserDelete,
|
||||
}: Props) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { mutate: deleteUser, isPending } = useMutation({
|
||||
mutationFn: ({ id }: { id: string }) => user_api.delete({ id }),
|
||||
|
||||
onSuccess: () => {
|
||||
queryClient.refetchQueries({ queryKey: ["user_list"] });
|
||||
toast.success(`Foydalanuvchi o'chirildi`);
|
||||
setOpenDelete(false);
|
||||
setUserDelete(null);
|
||||
},
|
||||
onError: (err: AxiosError) => {
|
||||
const errMessage = err.response?.data as { message: string };
|
||||
const messageText = errMessage.message;
|
||||
toast.error(messageText || "Xatolik yuz berdi", {
|
||||
richColors: true,
|
||||
position: "top-center",
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<Dialog open={opneDelete} onOpenChange={setOpenDelete}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Foydalanuvchini o'chrish</DialogTitle>
|
||||
<DialogDescription className="text-md font-semibold">
|
||||
Siz rostan ham {userDelete?.username}
|
||||
nomli foydalanuvchini o'chimoqchimiszi
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
className="bg-blue-600 cursor-pointer hover:bg-blue-600"
|
||||
onClick={() => setOpenDelete(false)}
|
||||
>
|
||||
<X />
|
||||
Bekor qilish
|
||||
</Button>
|
||||
<Button
|
||||
variant={"destructive"}
|
||||
onClick={() => userDelete && deleteUser({ id: userDelete.id })}
|
||||
>
|
||||
{isPending ? (
|
||||
<Loader2 className="animate-spin" />
|
||||
) : (
|
||||
<>
|
||||
<Trash />
|
||||
O'chirish
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default DeleteUser;
|
||||
57
src/features/users/ui/Filter.tsx
Normal file
57
src/features/users/ui/Filter.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
import type { RegionListResData } from "@/features/region/lib/data";
|
||||
import type { UserData } from "@/features/users/lib/data";
|
||||
import AddUsers from "@/features/users/ui/AddUsers";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/shared/ui/dialog";
|
||||
import { Plus } from "lucide-react";
|
||||
import { type Dispatch, type SetStateAction } from "react";
|
||||
|
||||
interface Props {
|
||||
setRegionValue: Dispatch<SetStateAction<RegionListResData | null>>;
|
||||
dialogOpen: boolean;
|
||||
setDialogOpen: Dispatch<SetStateAction<boolean>>;
|
||||
editingUser: UserData | null;
|
||||
setEditingUser: Dispatch<SetStateAction<UserData | null>>;
|
||||
}
|
||||
|
||||
const Filter = ({
|
||||
dialogOpen,
|
||||
setDialogOpen,
|
||||
editingUser,
|
||||
setEditingUser,
|
||||
}: Props) => {
|
||||
return (
|
||||
<div className="flex flex-wrap gap-2 items-center">
|
||||
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
variant="default"
|
||||
className="bg-blue-500 cursor-pointer hover:bg-blue-500"
|
||||
onClick={() => setEditingUser(null)}
|
||||
>
|
||||
<Plus className="!h-5 !w-5" /> Qo'shish
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{editingUser
|
||||
? "Foydalanuvchini tahrirlash"
|
||||
: "Foydalanuvchi qo'shish"}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<AddUsers initialData={editingUser} setDialogOpen={setDialogOpen} />
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Filter;
|
||||
112
src/features/users/ui/UserCard.tsx
Normal file
112
src/features/users/ui/UserCard.tsx
Normal file
@@ -0,0 +1,112 @@
|
||||
"use client";
|
||||
|
||||
import type { UserData } from "@/features/users/lib/data";
|
||||
import { Avatar, AvatarFallback } from "@/shared/ui/avatar";
|
||||
import { Badge } from "@/shared/ui/badge";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import { Card, CardContent, CardHeader } from "@/shared/ui/card";
|
||||
import { Clock, Edit, Trash2 } from "lucide-react";
|
||||
import { type Dispatch, type SetStateAction } from "react";
|
||||
|
||||
export function UserCard({
|
||||
user,
|
||||
setEditingUser,
|
||||
setDialogOpen,
|
||||
setOpenDelete,
|
||||
setUserDelete,
|
||||
}: {
|
||||
user: UserData;
|
||||
setEditingUser: (user: UserData) => void;
|
||||
setDialogOpen: (open: boolean) => void;
|
||||
setOpenDelete: Dispatch<SetStateAction<boolean>>;
|
||||
setUserDelete: Dispatch<SetStateAction<UserData | null>>;
|
||||
}) {
|
||||
const getRoleColor = (role: UserData["role"]) => {
|
||||
switch (role) {
|
||||
case "admin":
|
||||
return "bg-red-100 text-red-800";
|
||||
case "moderator":
|
||||
return "bg-blue-100 text-blue-800";
|
||||
case "user":
|
||||
return "bg-green-100 text-green-800";
|
||||
default:
|
||||
return "bg-gray-100 text-gray-800";
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card className="group hover:shadow-md transition-shadow">
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<Avatar>
|
||||
<AvatarFallback className="bg-green-100 text-green-700">
|
||||
{user.username
|
||||
.split(" ")
|
||||
.map((n) => n[0])
|
||||
.join("")
|
||||
.toUpperCase()}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div>
|
||||
<h3 className="font-semibold">{user.username}</h3>
|
||||
</div>
|
||||
</div>
|
||||
<Badge className={getRoleColor(user.role)}>
|
||||
{user.role === "admin"
|
||||
? "Admin"
|
||||
: user.role === "user"
|
||||
? "Foydalanuvchi"
|
||||
: user.role}
|
||||
</Badge>
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="space-y-4">
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<Clock className="h-4 w-4" />
|
||||
<span>
|
||||
{"Kirgan vaqt"} {new Date(user.date_joined).toLocaleDateString()}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{user.last_login && (
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{"Ohirgi marta kirgan"}:{" "}
|
||||
{new Date(user.last_login).toLocaleDateString()}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setEditingUser(user);
|
||||
setDialogOpen(true);
|
||||
}}
|
||||
className="flex-1"
|
||||
>
|
||||
<Edit className="h-4 w-4 mr-2" />
|
||||
Tahrirlash
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setOpenDelete(true);
|
||||
if (setUserDelete) {
|
||||
setUserDelete(user);
|
||||
}
|
||||
}}
|
||||
className="text-red-600 hover:text-red-700"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</>
|
||||
);
|
||||
}
|
||||
61
src/features/users/ui/UserTable.tsx
Normal file
61
src/features/users/ui/UserTable.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
import type { UserData } from "@/features/users/lib/data";
|
||||
import { UserCard } from "@/features/users/ui/UserCard";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { type Dispatch, type SetStateAction } from "react";
|
||||
|
||||
interface Props {
|
||||
data: UserData[] | undefined;
|
||||
isLoading: boolean;
|
||||
isError: boolean;
|
||||
setDialogOpen: Dispatch<SetStateAction<boolean>>;
|
||||
setEditingUser: Dispatch<SetStateAction<UserData | null>>;
|
||||
setOpenDelete: Dispatch<SetStateAction<boolean>>;
|
||||
setUserDelete: Dispatch<SetStateAction<UserData | null>>;
|
||||
}
|
||||
|
||||
const UserTable = ({
|
||||
data,
|
||||
isLoading,
|
||||
isError,
|
||||
setDialogOpen,
|
||||
setEditingUser,
|
||||
setOpenDelete,
|
||||
setUserDelete,
|
||||
}: Props) => {
|
||||
return (
|
||||
<div className="flex-1 overflow-auto">
|
||||
{isLoading && (
|
||||
<div className="h-full flex items-center justify-center bg-white/70 z-10">
|
||||
<Loader2 className="animate-spin" />
|
||||
</div>
|
||||
)}
|
||||
{isError && (
|
||||
<div className="h-full flex items-center justify-center z-10">
|
||||
<span className="text-lg font-medium text-red-600">
|
||||
Ma'lumotlarni olishda xatolik yuz berdi.
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isLoading && !isError && (
|
||||
<>
|
||||
{/* Users Grid */}
|
||||
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||
{data?.map((user) => (
|
||||
<UserCard
|
||||
setOpenDelete={setOpenDelete}
|
||||
setUserDelete={setUserDelete}
|
||||
key={user.id}
|
||||
setDialogOpen={setDialogOpen}
|
||||
setEditingUser={setEditingUser}
|
||||
user={{ ...user, role: user.is_superuser ? "admin" : "user" }}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserTable;
|
||||
77
src/features/users/ui/UsersList.tsx
Normal file
77
src/features/users/ui/UsersList.tsx
Normal file
@@ -0,0 +1,77 @@
|
||||
import type { RegionListResData } from "@/features/region/lib/data";
|
||||
import { user_api } from "@/features/users/lib/api";
|
||||
import type { UserData } from "@/features/users/lib/data";
|
||||
import DeleteUser from "@/features/users/ui/DeleteUser";
|
||||
import Filter from "@/features/users/ui/Filter";
|
||||
import UserTable from "@/features/users/ui/UserTable";
|
||||
import Pagination from "@/shared/ui/pagination";
|
||||
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useState } from "react";
|
||||
|
||||
const UsersList = () => {
|
||||
const [editingUser, setEditingUser] = useState<UserData | null>(null);
|
||||
const [opneDelete, setOpenDelete] = useState(false);
|
||||
const [userDelete, setUserDelete] = useState<UserData | null>(null);
|
||||
|
||||
const [regionValue, setRegionValue] = useState<RegionListResData | null>(
|
||||
null,
|
||||
);
|
||||
const limit = 20;
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
|
||||
const { data, isLoading, isError } = useQuery({
|
||||
queryKey: ["user_list", currentPage, regionValue],
|
||||
queryFn: () => {
|
||||
return user_api.list({
|
||||
page: currentPage,
|
||||
page_size: limit,
|
||||
});
|
||||
},
|
||||
select(data) {
|
||||
return data.data;
|
||||
},
|
||||
});
|
||||
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full p-10 w-full">
|
||||
<div className="flex flex-col md:flex-row justify-between items-start md:items-center mb-4 gap-4">
|
||||
<h1 className="text-2xl font-bold">Foydalanuvchilar ro'yxati</h1>
|
||||
|
||||
<Filter
|
||||
dialogOpen={dialogOpen}
|
||||
setDialogOpen={setDialogOpen}
|
||||
editingUser={editingUser}
|
||||
setEditingUser={setEditingUser}
|
||||
setRegionValue={setRegionValue}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<UserTable
|
||||
data={data?.results}
|
||||
isLoading={isLoading}
|
||||
setEditingUser={setEditingUser}
|
||||
isError={isError}
|
||||
setDialogOpen={setDialogOpen}
|
||||
setOpenDelete={setOpenDelete}
|
||||
setUserDelete={setUserDelete}
|
||||
/>
|
||||
<Pagination
|
||||
currentPage={currentPage}
|
||||
setCurrentPage={setCurrentPage}
|
||||
totalPages={data?.total_pages || 1}
|
||||
/>
|
||||
|
||||
<DeleteUser
|
||||
opneDelete={opneDelete}
|
||||
setOpenDelete={setOpenDelete}
|
||||
userDelete={userDelete}
|
||||
setUserDelete={setUserDelete}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default UsersList;
|
||||
Reference in New Issue
Block a user