From 9b4ee36fca5e0c528cfb88803ca369d4b1b0817c Mon Sep 17 00:00:00 2001 From: Harun CAN Date: Sat, 28 Mar 2026 17:16:46 +0300 Subject: [PATCH] main --- .../(dashboard)/source-accounts/page.tsx | 17 +- src/components/content/ContentTable.tsx | 342 +++++++++++++++--- 2 files changed, 306 insertions(+), 53 deletions(-) diff --git a/src/app/[locale]/(dashboard)/source-accounts/page.tsx b/src/app/[locale]/(dashboard)/source-accounts/page.tsx index ee9cfa8..c3e13d4 100644 --- a/src/app/[locale]/(dashboard)/source-accounts/page.tsx +++ b/src/app/[locale]/(dashboard)/source-accounts/page.tsx @@ -56,8 +56,10 @@ export default function SourceAccountsPage() { try { const res = await fetch('/api/backend/source-accounts', { headers: getHeaders() }); if (res.ok) { - const data = await res.json(); - setAccounts(Array.isArray(data) ? data : []); + const responseData = await res.json(); + // Handle wrapped response from global interceptor: { success, data } + const items = responseData?.data || responseData; + setAccounts(Array.isArray(items) ? items : []); } } catch (error) { console.error('Fetch error:', error); @@ -80,16 +82,19 @@ export default function SourceAccountsPage() { body: JSON.stringify({ url: newAccountUrl }), }); - if (res.ok) { - toaster.create({ title: "Source account added", type: "success" }); + const responseData = await res.json(); + + if (res.ok && responseData?.success !== false) { + toaster.create({ title: "Kaynak hesap eklendi", type: "success" }); setNewAccountUrl(""); setShowAddModal(false); fetchAccounts(); } else { - throw new Error('Failed to add account'); + const errorMsg = responseData?.message || 'Hesap eklenemedi'; + toaster.create({ title: errorMsg, type: "error" }); } } catch (error) { - toaster.create({ title: "Failed to add account", type: "error" }); + toaster.create({ title: "Hesap eklenemedi", type: "error" }); } finally { setIsAdding(false); } diff --git a/src/components/content/ContentTable.tsx b/src/components/content/ContentTable.tsx index 56f4489..dd634c0 100644 --- a/src/components/content/ContentTable.tsx +++ b/src/components/content/ContentTable.tsx @@ -1,14 +1,12 @@ "use client"; -import { Box, Table, Badge, HStack, IconButton, Button } from "@chakra-ui/react"; +import { Box, Table, Badge, HStack, IconButton, Button, Text, Flex, Checkbox } from "@chakra-ui/react"; import { useState, useEffect, useCallback } from "react"; import { useSession } from "next-auth/react"; -import { LuEye, LuPencil, LuTrash2, LuRefreshCw } from "react-icons/lu"; +import { LuEye, LuPencil, LuTrash2, LuRefreshCw, LuArrowUp, LuArrowDown, LuChevronLeft, LuChevronRight } from "react-icons/lu"; import { toaster } from "@/components/ui/feedback/toaster"; import { ContentPreviewDialog } from "./ContentPreviewDialog"; - - const getStatusColor = (status: string) => { switch (status?.toLowerCase()) { case "published": return "green"; @@ -19,6 +17,8 @@ const getStatusColor = (status: string) => { } }; +const PAGE_SIZE_OPTIONS = [25, 50, 100]; + export function ContentTable() { const { data: session } = useSession(); const [selectedItem, setSelectedItem] = useState(null); @@ -26,6 +26,19 @@ export function ContentTable() { const [contentList, setContentList] = useState([]); const [isLoading, setIsLoading] = useState(false); + // Pagination + const [pageSize, setPageSize] = useState(100); + const [currentPage, setCurrentPage] = useState(0); + const [totalItems, setTotalItems] = useState(0); + + // Sorting + const [sortBy, setSortBy] = useState("createdAt"); + const [sortOrder, setSortOrder] = useState<"asc" | "desc">("desc"); + + // Multi-select + const [selectedIds, setSelectedIds] = useState>(new Set()); + const [isDeleting, setIsDeleting] = useState(false); + const fetchContent = useCallback(async () => { setIsLoading(true); try { @@ -34,26 +47,81 @@ export function ContentTable() { headers['Authorization'] = `Bearer ${session.accessToken}`; } - const res = await fetch('/api/backend/content', { + const params = new URLSearchParams({ + limit: String(pageSize), + offset: String(currentPage * pageSize), + sortBy, + sortOrder, + }); + + const res = await fetch(`/api/backend/content?${params.toString()}`, { headers, }); if (res.ok) { const responseData = await res.json(); - // Handle wrapped response from global interceptor: { success, data, message } - const items = responseData?.data || responseData; - setContentList(Array.isArray(items) ? items : []); + const data = responseData?.data || responseData; + // Handle new { items, total } format + if (data?.items && typeof data.total === 'number') { + setContentList(Array.isArray(data.items) ? data.items : []); + setTotalItems(data.total); + } else { + // Fallback for old array format + const items = Array.isArray(data) ? data : []; + setContentList(items); + setTotalItems(items.length); + } } } catch (error) { console.error("Failed to fetch content:", error); } finally { setIsLoading(false); } - }, [session]); + }, [session, pageSize, currentPage, sortBy, sortOrder]); useEffect(() => { fetchContent(); }, [fetchContent]); + // Reset page when pageSize changes + useEffect(() => { + setCurrentPage(0); + }, [pageSize]); + + // Clear selection when data changes + useEffect(() => { + setSelectedIds(new Set()); + }, [contentList]); + + const handleSort = (field: string) => { + if (sortBy === field) { + setSortOrder(prev => prev === "asc" ? "desc" : "asc"); + } else { + setSortBy(field); + setSortOrder("desc"); + } + setCurrentPage(0); + }; + + const handleSelectAll = (checked: boolean) => { + if (checked) { + setSelectedIds(new Set(contentList.map(item => item.id))); + } else { + setSelectedIds(new Set()); + } + }; + + const handleSelectItem = (id: string, checked: boolean) => { + setSelectedIds(prev => { + const next = new Set(prev); + if (checked) { + next.add(id); + } else { + next.delete(id); + } + return next; + }); + }; + const handleDelete = async (item: any) => { if (!session?.accessToken) return; try { @@ -64,24 +132,54 @@ export function ContentTable() { } }); if (res.ok) { - toaster.create({ title: "Deleted", description: `"${item.title || 'Content'}" removed.`, type: "success" }); + toaster.create({ title: "Silindi", description: `İçerik kaldırıldı.`, type: "success" }); fetchContent(); } else { - toaster.create({ title: "Delete failed", type: "error" }); + toaster.create({ title: "Silme başarısız", type: "error" }); } } catch { - toaster.create({ title: "Delete failed", type: "error" }); + toaster.create({ title: "Silme başarısız", type: "error" }); + } + }; + + const handleBulkDelete = async () => { + if (selectedIds.size === 0) return; + setIsDeleting(true); + try { + const headers: HeadersInit = { + 'Content-Type': 'application/json', + }; + if (session?.accessToken) { + headers['Authorization'] = `Bearer ${session.accessToken}`; + } + + const res = await fetch('/api/backend/content/bulk-delete', { + method: 'POST', + headers, + body: JSON.stringify({ ids: Array.from(selectedIds) }), + }); + if (res.ok) { + const result = await res.json(); + const deletedCount = result?.data?.deleted || result?.deleted || selectedIds.size; + toaster.create({ + title: "Toplu silme başarılı", + description: `${deletedCount} içerik silindi.`, + type: "success", + }); + setSelectedIds(new Set()); + fetchContent(); + } else { + toaster.create({ title: "Toplu silme başarısız", type: "error" }); + } + } catch { + toaster.create({ title: "Toplu silme başarısız", type: "error" }); + } finally { + setIsDeleting(false); } }; const handleAction = (action: string, item: any) => { - if (action === 'View') { - setSelectedItem(item); - setIsPreviewOpen(true); - return; - } - if (action === 'Edit') { - // Open the preview in edit mode + if (action === 'View' || action === 'Edit') { setSelectedItem(item); setIsPreviewOpen(true); return; @@ -92,63 +190,162 @@ export function ContentTable() { } }; + const totalPages = Math.ceil(totalItems / pageSize); + const isAllSelected = contentList.length > 0 && selectedIds.size === contentList.length; + + const SortIcon = ({ field }: { field: string }) => { + if (sortBy !== field) return null; + return sortOrder === "asc" + ? + : ; + }; + return ( <> - - - + {/* Top toolbar */} + + + {selectedIds.size > 0 && ( + + )} + + Toplam {totalItems} içerik + + + + + {/* Page size selector */} + + Göster: + {PAGE_SIZE_OPTIONS.map(size => ( + + ))} + + + + + + - Title - Platform - Status - Date - Actions + + handleSelectAll(!!e.checked)} + > + + + + + handleSort("createdAt")} + _hover={{ color: "fg" }} + > + Başlık + + handleSort("type")} + _hover={{ color: "fg" }} + > + Platform + + handleSort("status")} + _hover={{ color: "fg" }} + > + Durum + + handleSort("createdAt")} + _hover={{ color: "fg" }} + > + Tarih + + İşlemler {isLoading ? ( - + - Loading content... + İçerikler yükleniyor... ) : contentList.length === 0 ? ( - - No content found. Start by generating some! + + İçerik bulunamadı. Önce içerik üretin! ) : contentList.map((item) => ( - - + + + handleSelectItem(item.id, !!e.checked)} + > + + + + + {item.title || item.body?.substring(0, 60) + '...' || 'Untitled'} - {item.type || item.platform} - + + {item.type || item.platform} + + + + {item.status} - {new Date(item.createdAt || item.date).toLocaleDateString()} + + {new Date(item.createdAt || item.date).toLocaleDateString('tr-TR', { + day: '2-digit', + month: '2-digit', + year: 'numeric', + hour: '2-digit', + minute: '2-digit', + })} + - + handleAction('View', item)} > @@ -156,7 +353,7 @@ export function ContentTable() { handleAction('Edit', item)} > @@ -165,7 +362,7 @@ export function ContentTable() { variant="ghost" size="sm" colorPalette="red" - aria-label="Delete" + aria-label="Sil" onClick={() => handleAction('Delete', item)} > @@ -178,6 +375,57 @@ export function ContentTable() { + {/* Pagination */} + {totalPages > 1 && ( + + + Sayfa {currentPage + 1} / {totalPages} ({totalItems} içerik) + + + setCurrentPage(prev => Math.max(0, prev - 1))} + > + + + {/* Show page numbers */} + {Array.from({ length: Math.min(totalPages, 5) }, (_, i) => { + let pageNum: number; + if (totalPages <= 5) { + pageNum = i; + } else if (currentPage < 3) { + pageNum = i; + } else if (currentPage > totalPages - 4) { + pageNum = totalPages - 5 + i; + } else { + pageNum = currentPage - 2 + i; + } + return ( + + ); + })} + = totalPages - 1} + onClick={() => setCurrentPage(prev => Math.min(totalPages - 1, prev + 1))} + > + + + + + )}