346 lines
12 KiB
TypeScript
346 lines
12 KiB
TypeScript
"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;
|