239 lines
8.3 KiB
TypeScript
239 lines
8.3 KiB
TypeScript
"use client"
|
||
|
||
import { useEffect, useState, useCallback } from "react"
|
||
import { useSession } from "next-auth/react"
|
||
import { DataTable } from "@/components/ui/data-table"
|
||
import { Button } from "@/components/ui/button"
|
||
import { Input } from "@/components/ui/input"
|
||
import { Plus } from "lucide-react"
|
||
import { postService } from "@/services/postService"
|
||
import { Post } from "@/types/post"
|
||
import { PostDialog } from "./_components/post-dialog"
|
||
import { getPostColumns } from "./_components/columns"
|
||
import { toast } from "sonner"
|
||
import Swal from "sweetalert2"
|
||
import withReactContent from "sweetalert2-react-content"
|
||
import {
|
||
Select,
|
||
SelectContent,
|
||
SelectItem,
|
||
SelectTrigger,
|
||
SelectValue,
|
||
} from "@/components/ui/select"
|
||
|
||
const MySwal = withReactContent(Swal)
|
||
|
||
export default function PostsPage() {
|
||
const { data: session } = useSession()
|
||
const [posts, setPosts] = useState<Post[]>([])
|
||
const [total, setTotal] = useState(0)
|
||
const [page, setPage] = useState(1)
|
||
const [perPage] = useState(10)
|
||
const [search, setSearch] = useState("")
|
||
const [statusFilter, setStatusFilter] = useState("with") // "with" | "only" (backend defaults to active if not with/only)
|
||
const [dialogOpen, setDialogOpen] = useState(false)
|
||
const [deletedIds, setDeletedIds] = useState<number[]>([])
|
||
const [selectedPost, setSelectedPost] = useState<Post | null>(null)
|
||
|
||
const fetchPosts = useCallback(async () => {
|
||
try {
|
||
const res = await postService.getPosts(page, perPage, search, statusFilter)
|
||
|
||
// Liste verisini al
|
||
const baseItems = res.items || []
|
||
|
||
// images alanı boş olanlar için, detay endpoint'inden gerçek images değerini çek
|
||
const itemsWithImages = await Promise.all(
|
||
baseItems.map(async (p) => {
|
||
if (p.images && p.images.trim() !== "") {
|
||
return p
|
||
}
|
||
const id = p.id || p.ID
|
||
if (!id) {
|
||
return p
|
||
}
|
||
try {
|
||
const detail = await postService.getPost(id)
|
||
return {
|
||
...p,
|
||
images: detail.data.images,
|
||
}
|
||
} catch {
|
||
return p
|
||
}
|
||
})
|
||
)
|
||
|
||
setPosts(itemsWithImages)
|
||
setTotal(res.total)
|
||
|
||
// Silinmiş post ID'lerini ayrıca takip et:
|
||
if (statusFilter === "only") {
|
||
const ids = itemsWithImages
|
||
.map(p => p.id || p.ID)
|
||
.filter((id): id is number => typeof id === "number")
|
||
setDeletedIds(ids)
|
||
} else if (statusFilter === "with") {
|
||
// 'with' görünümünde, silinmişleri ayrı bir çağrı ile çekelim
|
||
try {
|
||
const deletedRes = await postService.getPosts(1, 200, search, "only")
|
||
const ids = (deletedRes.items || [])
|
||
.map(p => p.id || p.ID)
|
||
.filter((id): id is number => typeof id === "number")
|
||
setDeletedIds(ids)
|
||
} catch (e) {
|
||
console.error("Silinmiş yazılar alınamadı:", e)
|
||
setDeletedIds([])
|
||
}
|
||
} else {
|
||
// Sadece aktif filtresinde silinmiş saymayalım
|
||
setDeletedIds([])
|
||
}
|
||
} catch (error) {
|
||
toast.error("Yazılar yüklenirken hata oluştu")
|
||
console.error(error)
|
||
}
|
||
}, [page, perPage, search, statusFilter])
|
||
|
||
useEffect(() => {
|
||
if (session) {
|
||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||
fetchPosts()
|
||
}
|
||
}, [session, fetchPosts])
|
||
|
||
const handleDelete = async (id: number) => {
|
||
const result = await MySwal.fire({
|
||
title: "Emin misiniz?",
|
||
text: "Bu yazıyı silmek istediğinize emin misiniz?",
|
||
icon: "warning",
|
||
showCancelButton: true,
|
||
confirmButtonText: "Evet, Sil",
|
||
cancelButtonText: "İptal",
|
||
customClass: {
|
||
popup: "dark:bg-gray-800 dark:text-white",
|
||
title: "dark:text-white",
|
||
},
|
||
})
|
||
|
||
if (result.isConfirmed) {
|
||
try {
|
||
await postService.deletePost(id)
|
||
toast.success("Yazı başarıyla silindi")
|
||
fetchPosts()
|
||
} catch (error) {
|
||
toast.error("Silme işlemi başarısız oldu")
|
||
console.error(error)
|
||
}
|
||
}
|
||
}
|
||
|
||
const handleRestore = async (id: number) => {
|
||
const result = await MySwal.fire({
|
||
title: "Geri Yükle?",
|
||
text: "Bu yazıyı geri yüklemek istediğinize emin misiniz?",
|
||
icon: "question",
|
||
showCancelButton: true,
|
||
confirmButtonText: "Evet, Geri Yükle",
|
||
cancelButtonText: "İptal",
|
||
customClass: {
|
||
popup: "dark:bg-gray-800 dark:text-white",
|
||
title: "dark:text-white",
|
||
},
|
||
})
|
||
|
||
if (result.isConfirmed) {
|
||
try {
|
||
await postService.restorePost(id)
|
||
toast.success("Yazı başarıyla geri yüklendi")
|
||
fetchPosts()
|
||
} catch (error) {
|
||
toast.error("Geri yükleme başarısız oldu")
|
||
console.error(error)
|
||
}
|
||
}
|
||
}
|
||
|
||
const handleEdit = async (post: Post) => {
|
||
try {
|
||
const id = post.id || post.ID
|
||
if (!id) {
|
||
toast.error("Yazı ID'si bulunamadı")
|
||
return
|
||
}
|
||
|
||
// Detay endpoint'inden güncel veriyi çek
|
||
const res = await postService.getPost(id)
|
||
setSelectedPost(res.data)
|
||
setDialogOpen(true)
|
||
} catch (error) {
|
||
console.error("Yazı detayı alınamadı:", error)
|
||
toast.error("Yazı detayı alınamadı")
|
||
}
|
||
}
|
||
|
||
const handleCreate = () => {
|
||
setSelectedPost(null)
|
||
setDialogOpen(true)
|
||
}
|
||
|
||
const columns = getPostColumns({
|
||
onEdit: handleEdit,
|
||
onDelete: handleDelete,
|
||
onRestore: handleRestore,
|
||
statusFilter,
|
||
deletedIds,
|
||
})
|
||
|
||
return (
|
||
<div className="p-8">
|
||
<div className="flex items-center justify-between mb-6">
|
||
<div>
|
||
<h1 className="text-3xl font-bold tracking-tight">Blog Yazıları</h1>
|
||
<p className="text-muted-foreground">
|
||
Blog içeriğini, kategorileri ve etiketleri yönetin.
|
||
</p>
|
||
</div>
|
||
<Button onClick={handleCreate}>
|
||
<Plus className="mr-2 h-4 w-4" /> Yeni Yazı
|
||
</Button>
|
||
</div>
|
||
|
||
<div className="flex items-center py-4 gap-4">
|
||
<Input
|
||
placeholder="Başlık ara..."
|
||
value={search}
|
||
onChange={(e) => setSearch(e.target.value)}
|
||
className="max-w-sm"
|
||
/>
|
||
<Select value={statusFilter} onValueChange={setStatusFilter}>
|
||
<SelectTrigger className="w-[180px]">
|
||
<SelectValue placeholder="Durum" />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
<SelectItem value="with">Tümü (Dahil)</SelectItem>
|
||
{/* Backend logic: empty 'soft' param usually means active only, 'only' means deleted only */}
|
||
<SelectItem value="active">Sadece Aktif</SelectItem>
|
||
<SelectItem value="only">Sadece Silinenler</SelectItem>
|
||
</SelectContent>
|
||
</Select>
|
||
</div>
|
||
|
||
<DataTable
|
||
columns={columns}
|
||
data={posts}
|
||
pageCount={Math.ceil(total / perPage)}
|
||
page={page}
|
||
onPageChange={setPage}
|
||
/>
|
||
|
||
<PostDialog
|
||
open={dialogOpen}
|
||
onOpenChange={setDialogOpen}
|
||
post={selectedPost}
|
||
onSuccess={fetchPosts}
|
||
/>
|
||
</div>
|
||
)
|
||
}
|