Files
simple-admin/src/pages/site-banner/ui/Banner.tsx
2025-11-17 13:09:24 +05:00

595 lines
19 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"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 { useSearchParams } from "react-router-dom";
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 bolishi kerak"),
title_ru: z
.string()
.min(3, "Sarlavha kamida 3 ta belgidan iborat bolishi kerak"),
description: z
.string()
.min(5, "Tavsif kamida 5 ta belgidan iborat bolishi kerak"),
description_ru: z
.string()
.min(5, "Tavsif kamida 5 ta belgidan iborat bolishi 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 yonalishlar" },
{ value: "banner4", label: "Reytingi baland turlar" },
];
const SiteBannerAdmin = () => {
const [searchParams, setSearchParams] = useSearchParams();
const initialPage = Number(searchParams.get("page")) || 1;
const [page, setPage] = useState(initialPage);
const updatePage = (newPage: number) => {
setPage(newPage);
setSearchParams({ page: newPage.toString() });
};
const {
data: banner,
isLoading,
isError,
refetch,
} = useQuery({
queryKey: ["all_banner", page],
queryFn: () => getBanner({ page, page_size: 20 }),
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 mt-10 gap-3">
<button
disabled={page === 1}
onClick={() => updatePage(Math.max(page - 1, 1))}
className="p-2 rounded-lg border border-slate-600 text-slate-300 hover:bg-slate-700/50 disabled:opacity-50"
>
<ChevronLeft className="w-5 h-5" />
</button>
{[...Array(banner?.total_pages)].map((_, i) => {
const pageNum = i + 1;
return (
<button
key={i}
onClick={() => updatePage(pageNum)}
className={`px-4 py-2 rounded-lg border font-medium transition-all ${
page === pageNum
? "bg-gradient-to-r from-blue-600 to-cyan-600 text-white border-blue-500"
: "border-slate-600 text-slate-300 hover:bg-slate-700/50"
}`}
>
{pageNum}
</button>
);
})}
<button
disabled={page === banner?.total_pages}
onClick={() =>
updatePage(Math.min(page + 1, banner?.total_pages ?? 1))
}
className="p-2 rounded-lg border border-slate-600 text-slate-300 hover:bg-slate-700/50 disabled:opacity-50"
>
<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" : "Qoshish"}</>
)}
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
</Dialog>
</div>
</div>
);
};
export default SiteBannerAdmin;