first commit
This commit is contained in:
238
frontend/app/admin/posts/page.tsx
Normal file
238
frontend/app/admin/posts/page.tsx
Normal file
@@ -0,0 +1,238 @@
|
||||
"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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user