Files
simple-admin/src/pages/news/ui/News.tsx
Samandar Turgunboyev 2d96eab3d7 post bug fix
2025-11-04 18:10:37 +05:00

346 lines
12 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 { deleteNews, getAllNews, updateNews } from "@/pages/news/lib/api";
import { Badge } from "@/shared/ui/badge";
import { Button } from "@/shared/ui/button";
import { Card } from "@/shared/ui/card";
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/shared/ui/dialog";
import { Switch } from "@/shared/ui/switch";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import clsx from "clsx";
import {
ChevronLeft,
ChevronRight,
Edit,
Eye,
EyeOff,
FolderOpen,
Loader2,
PlusCircle,
Trash2,
} from "lucide-react";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
import { toast } from "sonner";
const News = () => {
const [currentPage, setCurrentPage] = useState(1);
const { t } = useTranslation();
const [deleteId, setDeleteId] = useState<number | null>(null);
const queryClient = useQueryClient();
const navigate = useNavigate();
const {
data: allNews,
isLoading,
isError,
} = useQuery({
queryKey: ["all_news", currentPage],
queryFn: () => getAllNews({ page: currentPage, page_size: 10 }),
});
const { mutate: deleteMutate, isPending } = useMutation({
mutationFn: (id: number) => deleteNews(id),
onSuccess: () => {
setDeleteId(null);
queryClient.refetchQueries({ queryKey: ["all_news"] });
toast.success(t("Yangilik o'chirildi"), {
richColors: true,
position: "top-center",
});
},
onError: () => {
toast.error(t("Xatolik yuz berdi"), {
richColors: true,
position: "top-center",
});
},
});
const { mutate: togglePublicMutate } = useMutation({
mutationFn: ({ id, body }: { id: number; body: FormData }) =>
updateNews({ body, id }),
onSuccess: () => {
queryClient.refetchQueries({ queryKey: ["all_news"] });
toast.success(t("Status o'zgartirildi"), {
richColors: true,
position: "top-center",
});
},
onError: () => {
toast.error(t("Xatolik yuz berdi"), {
richColors: true,
position: "top-center",
});
},
});
const confirmDelete = () => {
if (deleteId) {
deleteMutate(deleteId);
}
};
const handleTogglePublic = (id: number, currentStatus: boolean) => {
const formData = new FormData();
console.log(currentStatus);
formData.append("is_public", String(currentStatus));
togglePublicMutate({
id,
body: formData,
});
};
if (isLoading) {
return (
<div className="min-h-screen bg-gray-900 w-full text-white flex justify-center items-center">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-white mx-auto mb-4"></div>
<p className="text-lg">{t("Yuklanmoqda...")}</p>
</div>
</div>
);
}
if (isError) {
return (
<div className="min-h-screen bg-gray-900 w-full text-white flex justify-center items-center">
<div className="text-center">
<Button className="mt-4">{t("Qayta urinish")}</Button>
</div>
</div>
);
}
return (
<div className="min-h-screen bg-gray-900 w-full text-white p-6">
{/* Header */}
<div className="flex justify-between items-center mb-8 w-[90%] mx-auto">
<div>
<h1 className="text-4xl font-bold mb-2">{t("Yangiliklar")}</h1>
<p className="text-gray-400">
{t("Jami")} {allNews?.data.data.total_items}{" "}
{t("ta yangilik mavjud")}
</p>
</div>
<Button
onClick={() => navigate("/news/add")}
className="flex items-center gap-2 cursor-pointer bg-blue-600 hover:bg-blue-700 text-white"
>
<PlusCircle size={18} />
{t("Yangilik qo'shish")}
</Button>
</div>
{/* News Grid */}
<div
className={clsx(
"gap-6 w-[90%] mx-auto",
allNews?.data.data.total_items === 0
? "flex justify-center items-center min-h-[60vh]"
: "grid md:grid-cols-2 lg:grid-cols-2 xl:grid-cols-3",
)}
>
{allNews?.data.data.total_items === 0 ? (
<div className="text-center py-12">
<div className="mb-6">
<div className="w-24 h-24 bg-neutral-800 rounded-full flex items-center justify-center mx-auto mb-4">
<FolderOpen size={48} className="text-gray-600" />
</div>
</div>
<p className="text-2xl text-gray-400 mb-2 font-semibold">
{t("Hozircha yangilik yo'q")}
</p>
<p className="text-gray-500 mb-6">
{t("Birinchi yangilikni qo'shishni boshlang")}
</p>
<Button
onClick={() => navigate("/news/add")}
className="flex items-center gap-2 mx-auto bg-blue-600 hover:bg-blue-700 text-white"
>
<PlusCircle size={18} /> {t("Yangilik qo'shish")}
</Button>
</div>
) : (
allNews?.data.data.results.map((item) => (
<Card
key={item.id}
className="overflow-hidden p-0 bg-neutral-900 hover:bg-neutral-800 transition-all duration-300 border border-neutral-800 hover:border-neutral-700 group flex flex-col"
>
{/* Image */}
<div className="relative h-64 overflow-hidden">
<img
src={item.image}
alt={item.title}
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300"
onError={(e) => {
e.currentTarget.src =
"https://images.unsplash.com/photo-1507525428034-b723cf961d3e";
}}
/>
{/* Category Badge */}
{item.category && (
<Badge className="absolute top-3 left-3 bg-blue-600 hover:bg-blue-700 text-white border-0">
<FolderOpen size={12} className="mr-1" />
{item.category.name}
</Badge>
)}
</div>
{/* Content */}
<div className="p-4 space-y-3 flex-1 flex flex-col">
{/* Title */}
<h2 className="text-xl font-bold line-clamp-2 group-hover:text-blue-400 transition-colors">
{item.title}
</h2>
{/* Short Text */}
<p className="text-sm text-gray-400 line-clamp-3 leading-relaxed">
{item.text}
</p>
{/* Slug */}
{item.tag && item.tag.length > 0 && (
<div className="pt-2 border-t border-neutral-800 flex flex-wrap gap-2">
{item.tag.map((e, idx) => (
<code
key={idx}
className="text-xs text-gray-500 bg-neutral-800 px-2 py-1 rounded"
>
/{e.name}
</code>
))}
</div>
)}
{/* Spacer to push content to bottom */}
<div className="flex-1"></div>
{/* Public/Private Toggle */}
<div className="pt-3 border-t border-neutral-800">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
{item.is_public ? (
<Eye size={16} className="text-green-500" />
) : (
<EyeOff size={16} className="text-gray-500" />
)}
<span className="text-sm text-gray-400">
{t("Оmmaviy")}
</span>
</div>
<Switch
checked={item.is_public}
onCheckedChange={() =>
handleTogglePublic(item.id, !item.is_public)
}
className="data-[state=checked]:bg-green-600"
/>
</div>
</div>
{/* Actions - at the very bottom */}
<div className="flex justify-end gap-2 pt-3">
<Button
onClick={() => navigate(`/news/edit/${item.id}`)}
size="sm"
variant="outline"
className="hover:bg-neutral-700 hover:text-blue-400"
>
<Edit size={16} className="mr-1" />
{t("Tahrirlash")}
</Button>
<Button
size="sm"
variant="destructive"
onClick={() => setDeleteId(item.id)}
className="hover:bg-red-700"
>
<Trash2 size={16} className="mr-1" />
{t("O'chirish")}
</Button>
</div>
</div>
</Card>
))
)}
</div>
<Dialog open={deleteId !== null} onOpenChange={() => setDeleteId(null)}>
<DialogContent className="sm:max-w-[425px] bg-gray-900">
<DialogHeader>
<DialogTitle className="text-xl">
{t("Yangilikni o'chirishni tasdiqlang")}
</DialogTitle>
</DialogHeader>
<div className="py-4">
<p className="text-muted-foreground">
{t(
"Haqiqatan ham bu turni o'chirib tashlamoqchimisiz? Bu amalni ortga qaytarib bo'lmaydi.",
)}
</p>
</div>
<DialogFooter className="gap-4 flex">
<Button variant="outline" onClick={() => setDeleteId(null)}>
{t("Bekor qilish")}
</Button>
<Button variant="destructive" onClick={confirmDelete}>
<Trash2 className="w-4 h-4 mr-2" />
{isPending ? (
<Loader2 className="animate-spin" />
) : (
t("O'chirish")
)}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* Pagination */}
<div className="flex justify-end gap-2 w-[90%] mx-auto mt-8">
<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(allNews?.data.data.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 === allNews?.data.data.total_pages}
onClick={() =>
setCurrentPage((p) =>
Math.min(p + 1, allNews ? allNews?.data.data.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>
</div>
);
};
export default News;