api ulangan

This commit is contained in:
Samandar Turgunboyev
2025-12-22 19:03:57 +05:00
parent fd397b670b
commit 7f2fe3868b
121 changed files with 12636 additions and 5528 deletions

View File

@@ -0,0 +1,29 @@
import type { Faq, FaqBody } from "@/features/faq/lib/type";
import httpClient from "@/shared/config/api/httpClient";
import { API_URLS } from "@/shared/config/api/URLs";
import type { AxiosResponse } from "axios";
export const faq_api = {
async getFaqs(params: {
page?: number;
page_size?: number;
}): Promise<AxiosResponse<Faq>> {
const res = await httpClient.get(API_URLS.FaqList, { params });
return res;
},
async createFaq(body: FaqBody) {
const res = httpClient.post(API_URLS.FaqCreate, body);
return res;
},
async updateFaq({ body, id }: { id: string; body: FaqBody }) {
const res = httpClient.patch(API_URLS.FaqUpdate(id), body);
return res;
},
async deleteFaq(id: string) {
const res = await httpClient.delete(API_URLS.FaqDelete(id));
return res;
},
};

View File

@@ -0,0 +1,8 @@
import z from "zod";
export const faqForm = z.object({
question_uz: z.string().min(1, { message: "Majburiy maydon" }),
question_ru: z.string().min(1, { message: "Majburiy maydon" }),
answer_uz: z.string().min(1, { message: "Majburiy maydon" }),
answer_ru: z.string().min(1, { message: "Majburiy maydon" }),
});

View File

@@ -0,0 +1,24 @@
export interface Faq {
has_next: boolean;
has_previous: boolean;
page: number;
page_size: number;
total: number;
total_pages: number;
results: FaqItem[];
}
export interface FaqItem {
answer_ru: string;
answer_uz: string;
id: string;
question_ru: string;
question_uz: string;
}
export interface FaqBody {
question_uz: string;
question_ru: string;
answer_uz: string;
answer_ru: string;
}

View File

@@ -0,0 +1,190 @@
import { faq_api } from "@/features/faq/lib/api";
import { faqForm } from "@/features/faq/lib/form";
import type { FaqBody, FaqItem } from "@/features/faq/lib/type";
import { Button } from "@/shared/ui/button";
import {
Form,
FormControl,
FormField,
FormItem,
FormMessage,
} from "@/shared/ui/form";
import { Label } from "@/shared/ui/label";
import { Textarea } from "@/shared/ui/textarea";
import { zodResolver } from "@hookform/resolvers/zod";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import type { AxiosError } from "axios";
import { Loader2 } from "lucide-react";
import { type Dispatch, type SetStateAction } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import type z from "zod";
interface Props {
initialValues: FaqItem | null;
setDialogOpen: Dispatch<SetStateAction<boolean>>;
}
const CreateFaq = ({ initialValues, setDialogOpen }: Props) => {
const queryClient = useQueryClient();
const { mutate, isPending } = useMutation({
mutationFn: (body: FaqBody) => faq_api.createFaq(body),
onSuccess: () => {
setDialogOpen(false);
toast.success("Faq qo'shildi", {
richColors: true,
position: "top-center",
});
queryClient.refetchQueries({ queryKey: ["faqs"] });
},
onError: (err: AxiosError) => {
toast.error((err.response?.data as string) || "Xatolik yuz berdi", {
richColors: true,
position: "top-center",
});
},
});
const { mutate: update, isPending: updateLoad } = useMutation({
mutationFn: ({ id, body }: { id: string; body: FaqBody }) =>
faq_api.updateFaq({ body, id }),
onSuccess: () => {
setDialogOpen(false);
toast.success("Faq yangilandi", {
richColors: true,
position: "top-center",
});
queryClient.refetchQueries({ queryKey: ["faqs"] });
},
onError: (err: AxiosError) => {
toast.error((err.response?.data as string) || "Xatolik yuz berdi", {
richColors: true,
position: "top-center",
});
},
});
const form = useForm<z.infer<typeof faqForm>>({
resolver: zodResolver(faqForm),
defaultValues: {
answer_ru: initialValues?.answer_ru || "",
answer_uz: initialValues?.answer_uz || "",
question_ru: initialValues?.question_ru || "",
question_uz: initialValues?.question_uz || "",
},
});
function onSubmit(values: z.infer<typeof faqForm>) {
if (initialValues) {
update({
body: {
answer_ru: values.answer_ru,
answer_uz: values.answer_uz,
question_ru: values.question_ru,
question_uz: values.question_uz,
},
id: initialValues.id,
});
} else if (!initialValues) {
mutate({
answer_ru: values.answer_ru,
answer_uz: values.answer_uz,
question_ru: values.question_ru,
question_uz: values.question_uz,
});
}
}
return (
<Form {...form}>
<form className="space-y-4" onSubmit={form.handleSubmit(onSubmit)}>
<FormField
control={form.control}
name="question_uz"
render={({ field }) => (
<FormItem>
<Label>Savol (uz)</Label>
<FormControl>
<Textarea
placeholder="Savol (uz)"
{...field}
className="min-h-32 max-h-44"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="question_ru"
render={({ field }) => (
<FormItem>
<Label>Savol (ru)</Label>
<FormControl>
<Textarea
placeholder="Savol (ru)"
{...field}
className="min-h-32 max-h-44"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="answer_uz"
render={({ field }) => (
<FormItem>
<Label>Javob (uz)</Label>
<FormControl>
<Textarea
placeholder="Javob (uz)"
{...field}
className="min-h-32 max-h-44"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="answer_ru"
render={({ field }) => (
<FormItem>
<Label>Javob (ru)</Label>
<FormControl>
<Textarea
placeholder="Javob (ru)"
{...field}
className="min-h-32 max-h-44"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button
className="w-full h-12 bg-blue-500 hover:bg-blue-500 cursor-pointer"
type="submit"
>
{isPending || updateLoad ? (
<Loader2 className="animate-spin" />
) : initialValues ? (
"Tahrirlash"
) : (
"Qo'shish"
)}
</Button>
</form>
</Form>
);
};
export default CreateFaq;

View File

@@ -0,0 +1,143 @@
import { faq_api } from "@/features/faq/lib/api";
import type { FaqItem } from "@/features/faq/lib/type";
import CreateFaq from "@/features/faq/ui/CreateFaq";
import FaqTable from "@/features/faq/ui/FaqTable";
import { Button } from "@/shared/ui/button";
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/shared/ui/dialog";
import { DialogDescription } from "@radix-ui/react-dialog";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import type { AxiosError } from "axios";
import { Loader2, Plus, Trash, X } from "lucide-react";
import { useState } from "react";
import { toast } from "sonner";
const FaqList = () => {
const [faqDelete, setFaqDelete] = useState<FaqItem | null>(null);
const [openDelete, setOpenDelete] = useState<boolean>(false);
const [dialogOpen, setDialogOpen] = useState<boolean>(false);
const [editingFaq, setEditingFaq] = useState<FaqItem | null>(null);
const queryClient = useQueryClient();
const {
data: faq,
isError,
isLoading,
isFetching,
} = useQuery({
queryKey: ["faqs"],
queryFn: async () => faq_api.getFaqs({ page: 1, page_size: 20 }),
select(data) {
return data.data;
},
});
const handleDelete = (user: FaqItem) => {
setFaqDelete(user);
setOpenDelete(true);
};
const { mutate, isPending } = useMutation({
mutationFn: (id: string) => faq_api.deleteFaq(id),
onSuccess: () => {
toast.success("Faq o'chirildi", {
position: "top-center",
richColors: true,
});
queryClient.refetchQueries({ queryKey: ["faqs"] });
setOpenDelete(false);
},
onError: (err: AxiosError) => {
toast.success((err.response?.data as string) || "Xatolik yu berdi", {
position: "top-center",
richColors: true,
});
},
});
return (
<div className="flex flex-col h-full p-10 w-full">
{/* Header */}
<div className="flex flex-col md:flex-row justify-between items-start md:items-center mb-4 gap-4">
<div className="flex flex-col gap-4 w-full">
<h1 className="text-2xl font-bold">FAQ</h1>
<div className="flex justify-end gap-2 w-full">
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
<DialogTrigger asChild>
<Button
variant="default"
className="bg-blue-500 cursor-pointer hover:bg-blue-500"
onClick={() => setEditingFaq(null)}
>
<Plus className="!h-5 !w-5" /> Qo'shish
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-lg max-h-[80vh] overflow-x-hidden">
<DialogHeader>
<DialogTitle className="text-xl">
{editingFaq ? "Tahrirlash" : "Faq qo'shish"}
</DialogTitle>
</DialogHeader>
<CreateFaq
initialValues={editingFaq}
setDialogOpen={setDialogOpen}
/>
</DialogContent>
</Dialog>
</div>
</div>
</div>
<FaqTable
isError={isError}
isLoading={isLoading}
faq={faq ? faq.results : []}
handleDelete={handleDelete}
isFetching={isFetching}
setDialogOpen={setDialogOpen}
setEditingFaq={setEditingFaq}
/>
<Dialog open={openDelete} onOpenChange={setOpenDelete}>
<DialogContent>
<DialogHeader>
<DialogTitle>Faqni o'chirish</DialogTitle>
<DialogDescription className="text-md font-semibold">
Siz rostan ham faqni o'chirmoqchimisiz
</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={() => faqDelete && mutate(faqDelete.id)}
>
{isPending ? (
<Loader2 className="animate-spin" />
) : (
<>
<Trash />
O'chirish
</>
)}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
};
export default FaqList;

View File

@@ -0,0 +1,117 @@
import type { FaqItem } from "@/features/faq/lib/type";
import { Button } from "@/shared/ui/button";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/shared/ui/table";
import { Loader2, Pencil, Trash2 } from "lucide-react";
import type { Dispatch, SetStateAction } from "react";
interface Props {
setEditingFaq: Dispatch<SetStateAction<FaqItem | null>>;
setDialogOpen: Dispatch<SetStateAction<boolean>>;
faq: FaqItem[] | [];
isLoading: boolean;
isError: boolean;
isFetching: boolean;
handleDelete: (user: FaqItem) => void;
}
const FaqTable = ({
faq,
handleDelete,
isError,
isFetching,
isLoading,
setDialogOpen,
setEditingFaq,
}: Props) => {
return (
<div className="flex-1 overflow-auto">
{(isLoading || isFetching) && (
<div className="h-full flex items-center justify-center bg-white/70 z-10">
<span className="text-lg font-medium">
<Loader2 className="animate-spin" />
</span>
</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 && (
<Table>
<TableHeader>
<TableRow>
<TableHead>#</TableHead>
<TableHead>Savol (uz)</TableHead>
<TableHead>Savol (ru)</TableHead>
<TableHead>Javob (uz)</TableHead>
<TableHead>Javob (ru)</TableHead>
<TableHead className="text-right">Amallar</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{faq.length > 0 ? (
faq.map((item, index) => (
<TableRow key={item.id}>
<TableCell>{index + 1}</TableCell>
<TableCell className="font-medium">
{item.question_uz}
</TableCell>
<TableCell className="font-medium">
{item.question_ru}
</TableCell>
<TableCell className="font-medium">
{item.answer_uz}
</TableCell>
<TableCell className="font-medium">
{item.answer_uz}
</TableCell>
<TableCell className="text-right flex gap-2 justify-end">
<Button
variant="outline"
size="icon"
className="bg-blue-600 text-white cursor-pointer hover:bg-blue-600 hover:text-white"
onClick={() => {
setEditingFaq(item);
setDialogOpen(true);
}}
>
<Pencil size={18} />
</Button>
<Button
variant="destructive"
size="icon"
className="cursor-pointer"
onClick={() => handleDelete(item)}
>
<Trash2 size={18} />
</Button>
</TableCell>
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={9} className="text-center py-4 text-lg">
Birlik topilmadi.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
)}
</div>
);
};
export default FaqTable;