api ulandi
This commit is contained in:
583
src/pages/site-banner/ui/Banner.tsx
Normal file
583
src/pages/site-banner/ui/Banner.tsx
Normal file
@@ -0,0 +1,583 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
bannerDelete,
|
||||
createBanner,
|
||||
getBanner,
|
||||
getBannerDetail,
|
||||
updateBanner,
|
||||
} from "@/pages/site-banner/lib/api";
|
||||
import TicketsImagesModel from "@/pages/tours/ui/TicketsImagesModel";
|
||||
import { Badge } from "@/shared/ui/badge";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/shared/ui/dialog";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/shared/ui/form";
|
||||
import { Input } from "@/shared/ui/input";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/shared/ui/select";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/shared/ui/table";
|
||||
import { Textarea } from "@/shared/ui/textarea";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
AlertTriangle,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
Edit2,
|
||||
Loader2,
|
||||
Plus,
|
||||
Trash2,
|
||||
} from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
|
||||
const fileSchema = z.union([
|
||||
z.instanceof(File, { message: "Rasm faylini yuklang" }),
|
||||
z.string().min(1, { message: "Rasm faylini yuklang" }),
|
||||
]);
|
||||
|
||||
const bannerSchema = z.object({
|
||||
title: z
|
||||
.string()
|
||||
.min(3, "Sarlavha kamida 3 ta belgidan iborat bo‘lishi kerak"),
|
||||
title_ru: z
|
||||
.string()
|
||||
.min(3, "Sarlavha kamida 3 ta belgidan iborat bo‘lishi kerak"),
|
||||
description: z
|
||||
.string()
|
||||
.min(5, "Tavsif kamida 5 ta belgidan iborat bo‘lishi kerak"),
|
||||
description_ru: z
|
||||
.string()
|
||||
.min(5, "Tavsif kamida 5 ta belgidan iborat bo‘lishi kerak"),
|
||||
image: fileSchema,
|
||||
link: z.string().url("Yaroqli havola URL manzili kiriting"),
|
||||
position: z.string().min(1, "Pozitsiyani tanlang"),
|
||||
});
|
||||
|
||||
type BannerFormData = z.infer<typeof bannerSchema>;
|
||||
|
||||
const positions = [
|
||||
{ value: "banner1", label: "Asosiy" },
|
||||
{ value: "banner2", label: "Kun taklifi" },
|
||||
{ value: "banner3", label: "Mashhur yo‘nalishlar" },
|
||||
{ value: "banner4", label: "Reytingi baland turlar" },
|
||||
];
|
||||
|
||||
const SiteBannerAdmin = () => {
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const {
|
||||
data: banner,
|
||||
isLoading,
|
||||
isError,
|
||||
refetch,
|
||||
} = useQuery({
|
||||
queryKey: ["all_banner", currentPage],
|
||||
queryFn: () => getBanner({ page: currentPage, page_size: 10 }),
|
||||
select(data) {
|
||||
return data.data.data;
|
||||
},
|
||||
});
|
||||
const { t } = useTranslation();
|
||||
const queryClient = useQueryClient();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [editingBanner, setEditingBanner] = useState<number | null>(null);
|
||||
const [deleteId, setDeleteId] = useState<number | null>(null);
|
||||
|
||||
const form = useForm<BannerFormData>({
|
||||
resolver: zodResolver(bannerSchema),
|
||||
defaultValues: {
|
||||
title: "",
|
||||
description: "",
|
||||
image: "",
|
||||
link: "",
|
||||
position: "banner1",
|
||||
},
|
||||
});
|
||||
|
||||
const { data: bannerDetail } = useQuery({
|
||||
queryKey: ["detail_banner", editingBanner],
|
||||
queryFn: () => getBannerDetail(editingBanner!),
|
||||
select(data) {
|
||||
return data.data.data;
|
||||
},
|
||||
enabled: !!editingBanner,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (editingBanner && bannerDetail) {
|
||||
form.setValue("title", bannerDetail.title_uz);
|
||||
form.setValue("title_ru", bannerDetail.title_ru);
|
||||
form.setValue("description", bannerDetail.description_uz);
|
||||
form.setValue("description_ru", bannerDetail.description_ru);
|
||||
form.setValue("image", bannerDetail.image);
|
||||
form.setValue("link", bannerDetail.link);
|
||||
form.setValue("position", bannerDetail.position);
|
||||
}
|
||||
}, [bannerDetail, editingBanner]);
|
||||
|
||||
const { mutate: create, isPending } = useMutation({
|
||||
mutationFn: (body: FormData) => createBanner(body),
|
||||
onSuccess: () => {
|
||||
queryClient.refetchQueries({ queryKey: ["all_banner"] });
|
||||
queryClient.refetchQueries({ queryKey: ["detail_banner"] });
|
||||
setOpen(false);
|
||||
form.reset();
|
||||
},
|
||||
onError: () => {
|
||||
toast.error(t("Xatolik yuz berdi"), {
|
||||
richColors: true,
|
||||
position: "top-center",
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const { mutate: update, isPending: updatePending } = useMutation({
|
||||
mutationFn: ({ body, id }: { id: number; body: FormData }) =>
|
||||
updateBanner({ body, id }),
|
||||
onSuccess: () => {
|
||||
queryClient.refetchQueries({ queryKey: ["all_banner"] });
|
||||
queryClient.refetchQueries({ queryKey: ["detail_banner"] });
|
||||
setOpen(false);
|
||||
form.reset();
|
||||
},
|
||||
onError: () => {
|
||||
toast.error(t("Xatolik yuz berdi"), {
|
||||
richColors: true,
|
||||
position: "top-center",
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const { mutate: deletBanner, isPending: deletePending } = useMutation({
|
||||
mutationFn: ({ id }: { id: number }) => bannerDelete(id),
|
||||
onSuccess: () => {
|
||||
queryClient.refetchQueries({ queryKey: ["all_banner"] });
|
||||
queryClient.refetchQueries({ queryKey: ["detail_banner"] });
|
||||
},
|
||||
onError: () => {
|
||||
toast.error(t("Xatolik yuz berdi"), {
|
||||
richColors: true,
|
||||
position: "top-center",
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const handleOpen = (banner: number) => {
|
||||
setOpen(true);
|
||||
setEditingBanner(banner);
|
||||
};
|
||||
|
||||
const onSubmit = (values: BannerFormData) => {
|
||||
const formData = new FormData();
|
||||
formData.append("title", values.title);
|
||||
formData.append("title_ru", values.title_ru);
|
||||
formData.append("description", values.description);
|
||||
formData.append("description_ru", values.description_ru);
|
||||
if (values.image instanceof File) {
|
||||
formData.append("image", values.image);
|
||||
}
|
||||
formData.append("link", values.link);
|
||||
formData.append("position", values.position);
|
||||
|
||||
if (editingBanner) {
|
||||
update({
|
||||
body: formData,
|
||||
id: editingBanner,
|
||||
});
|
||||
} else {
|
||||
create(formData);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = (id: number) => {
|
||||
deletBanner({ id });
|
||||
setDeleteId(id);
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center min-h-screen bg-slate-900 text-white gap-4 w-full">
|
||||
<Loader2 className="w-10 h-10 animate-spin text-cyan-400" />
|
||||
<p className="text-slate-400">{t("Ma'lumotlar yuklanmoqda...")}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center min-h-screen bg-slate-900 w-full text-center text-white gap-4">
|
||||
<AlertTriangle className="w-10 h-10 text-red-500" />
|
||||
<p className="text-lg">
|
||||
{t("Ma'lumotlarni yuklashda xatolik yuz berdi.")}
|
||||
</p>
|
||||
<Button
|
||||
onClick={() => refetch()}
|
||||
className="bg-gradient-to-r from-blue-600 to-cyan-600 text-white rounded-lg px-5 py-2 hover:opacity-90"
|
||||
>
|
||||
{t("Qayta urinish")}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen w-full bg-gray-900 text-white p-8">
|
||||
<div className="max-w-full mx-auto">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-white">
|
||||
{t("Sayt Bannerlari")}
|
||||
</h1>
|
||||
<p className="text-gray-400 mt-2">{t("Bannerlarni boshqarish")}</p>
|
||||
</div>
|
||||
<Button
|
||||
className="gap-2 bg-blue-600 hover:bg-blue-700 text-white"
|
||||
onClick={() => {
|
||||
setOpen(true);
|
||||
setEditingBanner(null);
|
||||
form.reset();
|
||||
}}
|
||||
>
|
||||
<Plus size={18} /> {t("Qo'shish")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="bg-gray-800 rounded-lg shadow-md border border-gray-700">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="border-b border-gray-700 bg-gray-800/60">
|
||||
<TableHead className="text-gray-300 font-medium">ID</TableHead>
|
||||
<TableHead className="text-gray-300 font-medium">
|
||||
{t("Rasm")}
|
||||
</TableHead>
|
||||
<TableHead className="text-gray-300 font-medium">
|
||||
{t("Sarlavha")}
|
||||
</TableHead>
|
||||
<TableHead className="text-gray-300 font-medium">
|
||||
{t("Tavsif")}
|
||||
</TableHead>
|
||||
<TableHead className="text-gray-300 font-medium">
|
||||
{t("Joylashuvi")}
|
||||
</TableHead>
|
||||
<TableHead className="text-gray-300 font-medium text-right">
|
||||
{t("Amallar")}
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
|
||||
<TableBody>
|
||||
{banner && banner.results.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colSpan={6}
|
||||
className="py-10 text-center text-gray-400"
|
||||
>
|
||||
<div className="flex flex-col items-center justify-center gap-3">
|
||||
<AlertTriangle className="w-8 h-8 text-yellow-500" />
|
||||
<p className="text-lg">
|
||||
{t("Hozircha bannerlar mavjud emas")}
|
||||
</p>
|
||||
<Button
|
||||
onClick={() => setOpen(true)}
|
||||
className="bg-blue-600 hover:bg-blue-700 text-white mt-2"
|
||||
>
|
||||
<Plus size={16} className="mr-2" />
|
||||
{t("Qo'shish")}
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
banner &&
|
||||
banner.results.map((item) => (
|
||||
<TableRow
|
||||
key={item.id}
|
||||
className="border-b border-gray-700 hover:bg-gray-700/30 transition-colors"
|
||||
>
|
||||
<TableCell className="text-gray-300">{item.id}</TableCell>
|
||||
|
||||
<TableCell>
|
||||
<img
|
||||
src={item.image}
|
||||
alt={item.title}
|
||||
className="w-24 h-16 object-cover rounded-md border border-gray-600"
|
||||
/>
|
||||
</TableCell>
|
||||
|
||||
<TableCell className="font-medium text-white">
|
||||
{item.title}
|
||||
<div className="text-blue-400 text-sm truncate max-w-[180px]">
|
||||
<a href={item.link} target="_blank" rel="noreferrer">
|
||||
{item.link}
|
||||
</a>
|
||||
</div>
|
||||
</TableCell>
|
||||
|
||||
<TableCell className="text-gray-300 truncate max-w-xs">
|
||||
{item.description}
|
||||
</TableCell>
|
||||
|
||||
<TableCell>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="bg-blue-900 text-blue-300"
|
||||
>
|
||||
{t(
|
||||
positions.find((p) => p.value === item.position)
|
||||
?.label ?? "",
|
||||
)}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
|
||||
<TableCell className="text-right flex justify-end items-center gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="border-gray-600 text-blue-400 hover:text-blue-200"
|
||||
onClick={() => handleOpen(item.id)}
|
||||
>
|
||||
<Edit2 size={16} />
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="icon"
|
||||
onClick={() => handleDelete(item.id)}
|
||||
>
|
||||
{deleteId === item.id && deletePending ? (
|
||||
<Loader2 className="animate-spin" size={16} />
|
||||
) : (
|
||||
<Trash2 size={16} />
|
||||
)}
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-2 mt-5">
|
||||
<button
|
||||
disabled={currentPage === 1}
|
||||
onClick={() => setCurrentPage((p) => Math.max(p - 1, 1))}
|
||||
className="p-2 rounded-lg border border-slate-600 text-slate-300 hover:bg-slate-700/50 disabled:opacity-50 disabled:cursor-not-allowed transition-all hover:border-slate-500"
|
||||
>
|
||||
<ChevronLeft className="w-5 h-5" />
|
||||
</button>
|
||||
|
||||
{[...Array(banner?.total_pages)].map((_, i) => (
|
||||
<button
|
||||
key={i}
|
||||
onClick={() => setCurrentPage(i + 1)}
|
||||
className={`px-4 py-2 rounded-lg border transition-all font-medium ${
|
||||
currentPage === i + 1
|
||||
? "bg-gradient-to-r from-blue-600 to-cyan-600 border-blue-500 text-white shadow-lg shadow-cyan-500/50"
|
||||
: "border-slate-600 text-slate-300 hover:bg-slate-700/50 hover:border-slate-500"
|
||||
}`}
|
||||
>
|
||||
{i + 1}
|
||||
</button>
|
||||
))}
|
||||
|
||||
<button
|
||||
disabled={currentPage === banner?.total_pages}
|
||||
onClick={() =>
|
||||
setCurrentPage((p) =>
|
||||
Math.min(p + 1, banner ? banner?.total_pages : 0),
|
||||
)
|
||||
}
|
||||
className="p-2 rounded-lg border border-slate-600 text-slate-300 hover:bg-slate-700/50 disabled:opacity-50 disabled:cursor-not-allowed transition-all hover:border-slate-500"
|
||||
>
|
||||
<ChevronRight className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogContent className="sm:max-w-[600px] max-h-[80vh] overflow-y-scroll bg-gray-800 border border-gray-700 text-white">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{editingBanner
|
||||
? t("Bannerni tahrirlash")
|
||||
: t("Yangi banner qo'shish")}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="space-y-4"
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="title"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("Sarlavha")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
className="bg-gray-700 border-gray-600 text-white"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="title_ru"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("Sarlavha")} (ru)</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
className="bg-gray-700 border-gray-600 text-white"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="description"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("Tavsif")}</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
className="bg-gray-700 border-gray-600 text-white"
|
||||
rows={3}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="description_ru"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("Tavsif")} (ru)</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
className="bg-gray-700 border-gray-600 text-white"
|
||||
rows={3}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<TicketsImagesModel
|
||||
form={form}
|
||||
name="image"
|
||||
multiple={false}
|
||||
label={t("Banner")}
|
||||
imageUrl={bannerDetail?.image}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="link"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("Havola URL")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
className="bg-gray-700 border-gray-600 text-white"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="position"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("Joylashuvi")}</FormLabel>
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
value={field.value}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger className="w-full bg-gray-700 border-gray-600 text-white">
|
||||
<SelectValue placeholder="Pozitsiyani tanlang" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent className="bg-gray-800 text-white border-gray-700">
|
||||
{positions.map((pos) => (
|
||||
<SelectItem key={pos.value} value={pos.value}>
|
||||
{t(pos.label)}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full bg-blue-600 hover:bg-blue-700 text-white"
|
||||
>
|
||||
{isPending || updatePending ? (
|
||||
<Loader2 className="animate-spin" />
|
||||
) : (
|
||||
<>{editingBanner ? "Saqlash" : "Qo‘shish"}</>
|
||||
)}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SiteBannerAdmin;
|
||||
Reference in New Issue
Block a user