api ulandi

This commit is contained in:
Samandar Turgunboyev
2025-10-29 18:41:59 +05:00
parent a9e99f9755
commit 2d0285dafc
64 changed files with 6319 additions and 2352 deletions

View 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 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 [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" : "Qoshish"}</>
)}
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
</Dialog>
</div>
</div>
);
};
export default SiteBannerAdmin;

View File

@@ -0,0 +1,112 @@
"use client";
import { getBanner } from "@/pages/site-banner/lib/api";
import { Card } from "@/shared/ui/card";
import {
Carousel,
CarouselContent,
CarouselItem,
CarouselNext,
CarouselPrevious,
} from "@/shared/ui/carousel";
import { useQuery } from "@tanstack/react-query";
import { AlertTriangle, Loader2, MoveRightIcon } from "lucide-react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
const BannerCarousel = () => {
const { t } = useTranslation();
// 🧠 Bannerlarni backenddan olish
const { data, isLoading, isError, refetch } = useQuery({
queryKey: ["all_banner"],
queryFn: () => getBanner(),
select: (res) =>
res.data.data.results.filter((b) => b.position === "banner1"),
});
const colors = ["#EDF5C7", "#F5DCC7"];
if (isLoading)
return (
<div className="flex items-center justify-center min-h-[400px]">
<Loader2 className="animate-spin text-blue-500 w-8 h-8" />
</div>
);
if (isError)
return (
<div className="flex flex-col items-center justify-center min-h-[400px] text-white">
<AlertTriangle className="text-red-500 w-8 h-8 mb-2" />
<p>{t("Ma'lumotlarni yuklashda xatolik yuz berdi.")}</p>
<button
onClick={() => refetch()}
className="mt-3 bg-blue-600 hover:bg-blue-700 px-4 py-2 rounded-lg"
>
{t("Qayta urinish")}
</button>
</div>
);
if (!data || data.length === 0)
return (
<div className="flex items-center justify-center text-gray-400 min-h-[400px]">
{t("Hozircha bannerlar mavjud emas")}
</div>
);
return (
<div className="mt-10 max-lg:hidden custom-container">
<Carousel opts={{ loop: true, align: "start" }}>
<CarouselContent>
{data.map((banner, index) => (
<CarouselItem
key={banner.id}
className="basis-full md:basis-[80%] shrink-0"
>
<div className="h-[500px]">
<Card className="h-full !rounded-[50px] flex border-none items-center justify-start relative overflow-hidden">
{/* <BannerCircle
color={colors[index % colors.length]}
className="w-[60%] h-full absolute z-10"
/> */}
{/* Matn qismi */}
<div className="flex flex-col gap-6 w-96 z-20 absolute left-14 top-1/2 -translate-y-1/2">
<p className="text-4xl font-semibold text-[#232325]">
{banner.title}
</p>
<p className="text-[#212122] font-medium">
{banner.description}
</p>
<Link
to={banner.link || "#"}
className="bg-white text-[#212122] font-semibold flex gap-4 px-8 py-4 shadow-sm !rounded-4xl w-fit"
>
<p>{t("Batafsil")}</p>
<MoveRightIcon />
</Link>
</div>
{/* Rasm qismi */}
<div className="absolute right-0 w-[50%] h-full">
<img
src={banner.image}
alt={banner.title}
className="object-cover w-full h-full"
/>
</div>
</Card>
</div>
</CarouselItem>
))}
</CarouselContent>
<CarouselPrevious className="absolute left-2 top-1/2 -translate-y-1/2 bg-white/80 hover:bg-white p-2 size-10 rounded-full shadow z-10" />
<CarouselNext className="absolute right-2 top-1/2 -translate-y-1/2 bg-white/80 hover:bg-white p-2 rounded-full size-10 shadow z-10" />
</Carousel>
</div>
);
};
export default BannerCarousel;