diff --git a/src/api/adminContentApi.js b/src/api/adminContentApi.js index 8ebdd10..76661ca 100644 --- a/src/api/adminContentApi.js +++ b/src/api/adminContentApi.js @@ -62,22 +62,40 @@ function extractPage(body, size) { return { items, last, total: hasTotal ? body.totalElements : undefined }; } -/** - * GET /api/v1/admin/posts?page&size&search&type&status - * - * Постранично отдаёт ВСЕ объявления площадки вместе с данными автора. - * Возвращает { items, last, total } — items уже нормализованы. - */ -export async function getAdminPosts({ page = 0, size = 20, search = "", type = "", status = "" } = {}) { - const params = new URLSearchParams({ page, size }); - if (search) params.set("search", search); - if (type) params.set("type", type); - if (status) params.set("status", status); - - const res = await apiFetch(`${ADMIN_BASE}/posts?${params.toString()}`); - if (!res.ok) throw new AdminContentError(res.status, await readErrorMessage(res, "Failed to load posts")); - return extractPage(await res.json(), size); -} + /** + * GET /api/v1/admin/posts?page&size&search&type&status + * + * Постранично отдаёт ВСЕ объявления площадки вместе с данными автора. + * Возвращает { items, last, total } — items уже нормализованы. + */ + export async function getAdminPosts({ page = 0, size = 20, search = "", type = "", status = "" } = {}) { + const params = new URLSearchParams({ page, size }); + if (search) params.set("search", search); + if (type) params.set("type", type); + if (status) params.set("status", status); + + const res = await apiFetch(`${ADMIN_BASE}/posts?${params.toString()}`); + if (!res.ok) throw new AdminContentError(res.status, await readErrorMessage(res, "Failed to load posts")); + return extractPage(await res.json(), size); + } + + /** + * GET /api/v1/admin/posts/rejected?page&size&search&type&status + * + * Постранично отдаёт объявления, не прошедшие модерацию (REJECTED), + * вместе с данными автора. + * Возвращает { items, last, total } — items уже нормализованы. + */ + export async function getAdminRejectedPosts({ page = 0, size = 20, search = "", type = "", status = "" } = {}) { + const params = new URLSearchParams({ page, size }); + if (search) params.set("search", search); + if (type) params.set("type", type); + if (status) params.set("status", status); + + const res = await apiFetch(`${ADMIN_BASE}/posts/rejected?${params.toString()}`); + if (!res.ok) throw new AdminContentError(res.status, await readErrorMessage(res, "Failed to load rejected posts")); + return extractPage(await res.json(), size); + } /** DELETE /api/v1/admin/posts/{postId} — 204 и 200 одинаково допустимы. */ export async function deleteAdminPost(postId) { diff --git a/src/components/Sidebar.jsx b/src/components/Sidebar.jsx index d9cf083..3c11328 100644 --- a/src/components/Sidebar.jsx +++ b/src/components/Sidebar.jsx @@ -141,6 +141,16 @@ export default function Sidebar({ > {NavIcons.rules()} {t('adminPanel.postsNav')} + )} diff --git a/src/components/admin/PostsTab.jsx b/src/components/admin/PostsTab.jsx index c459654..2a1c022 100644 --- a/src/components/admin/PostsTab.jsx +++ b/src/components/admin/PostsTab.jsx @@ -7,6 +7,7 @@ import ImageViewer from "../ImageViewer"; import Toast from "../Toast"; import { getAdminPosts, + getAdminRejectedPosts, deleteAdminPost, updateAdminPostStatus, } from "../../api/adminContentApi"; @@ -48,8 +49,9 @@ function authorLabelOf(post) { * Разметка рассчитана на то, что страница уже нарисовала .admp > .admp-inner, * поэтому собственного скролл-контейнера у вкладки нет. */ -export default function PostsTab({ isAdmin }) { +export default function PostsTab({ isAdmin, mode = "all" }) { const { t } = useTranslation(); + const isRejectedMode = mode === "rejected"; const [posts, setPosts] = useState([]); const [page, setPage] = useState(0); @@ -86,7 +88,8 @@ export default function PostsTab({ isAdmin }) { setError(""); try { - const result = await getAdminPosts({ page: pageNum, size: PAGE_SIZE, search, type: typeFilter, status: statusFilter }); + const fetcher = isRejectedMode ? getAdminRejectedPosts : getAdminPosts; + const result = await fetcher({ page: pageNum, size: PAGE_SIZE, search, type: typeFilter, status: statusFilter }); setPosts((prev) => (append ? [...prev, ...result.items] : result.items)); setHasMore(!result.last); setPage(pageNum); @@ -98,7 +101,7 @@ export default function PostsTab({ isAdmin }) { setLoading(false); setLoadingMore(false); } - }, [isAdmin, search, typeFilter, statusFilter, t]); + }, [isAdmin, search, typeFilter, statusFilter, t, isRejectedMode]); // Первичная загрузка + перезагрузка при смене фильтров useEffect(() => { diff --git a/src/locales/en.json b/src/locales/en.json index 14d8291..263cf6b 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -247,9 +247,11 @@ "adminPanel": { "campaignsNav": "Ads (campaigns)", "postsNav": "All listings", - "tabsLabel": "Admin panel sections", - "tabCampaigns": "Advertising campaigns", + "postsRejectedNav": "Rejected", + "tabsLabel": "Admin panel tabs", + "tabCampaigns": "Ad campaigns", "tabPosts": "All listings", + "tabPostsRejected": "Rejected", "pageTitle": "Advertising campaigns", "postsPageTitle": "All listings", "postsSearchPlaceholder": "Search by title, description, author", diff --git a/src/locales/ru.json b/src/locales/ru.json index 55405bb..ac47f2e 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -247,9 +247,11 @@ "adminPanel": { "campaignsNav": "Реклама (кампании)", "postsNav": "Все объявления", - "tabsLabel": "Разделы админ-панели", + "postsRejectedNav": "Отклонённые", + "tabsLabel": "Вкладки админ-панели", "tabCampaigns": "Рекламные кампании", "tabPosts": "Все объявления", + "tabPostsRejected": "Отклонённые", "pageTitle": "Рекламные кампании", "postsPageTitle": "Все объявления", "postsSearchPlaceholder": "Поиск по названию, описанию, автору", diff --git a/src/pages/AdminAdsPage.jsx b/src/pages/AdminAdsPage.jsx index a25581d..3593d01 100644 --- a/src/pages/AdminAdsPage.jsx +++ b/src/pages/AdminAdsPage.jsx @@ -25,12 +25,14 @@ import "../styles/admin.css"; const POLL_INTERVAL_MS = 3000; const MAX_POLL_ATTEMPTS = 100; // ≈ 5 минут -/** Активная вкладка задаётся query-параметром ?tab=posts, иначе — реклама. */ +/** Активная вкладка задаётся query-параметром ?tab=posts или ?tab=rejected, иначе — реклама. */ const TAB_CAMPAIGNS = "campaigns"; const TAB_POSTS = "posts"; +const TAB_POSTS_REJECTED = "rejected"; const TABS = [ { key: TAB_CAMPAIGNS, labelKey: "adminPanel.tabCampaigns" }, { key: TAB_POSTS, labelKey: "adminPanel.tabPosts" }, + { key: TAB_POSTS_REJECTED, labelKey: "adminPanel.tabPostsRejected" }, ]; /** "2026-09-20T14:30:00" → "20.09.2026 14:30" */ @@ -60,12 +62,18 @@ export default function AdminAdsPage() { const isDesktop = useIsDesktop(); const modals = useModals(); - const tab = searchParams.get("tab") === TAB_POSTS ? TAB_POSTS : TAB_CAMPAIGNS; + const tabParam = searchParams.get("tab"); + const tab = tabParam === TAB_POSTS ? TAB_POSTS : tabParam === TAB_POSTS_REJECTED ? TAB_POSTS_REJECTED : TAB_CAMPAIGNS; const isPostsTab = tab === TAB_POSTS; + const isPostsRejectedTab = tab === TAB_POSTS_REJECTED; const selectTab = useCallback((key) => { // Для рекламы параметр не пишем — /admin/ads без query открывает именно её. - setSearchParams(key === TAB_CAMPAIGNS ? {} : { tab: key }, { replace: true }); + if (key === TAB_CAMPAIGNS) { + setSearchParams({}, { replace: true }); + } else { + setSearchParams({ tab: key }, { replace: true }); + } }, [setSearchParams]); const [campaigns, setCampaigns] = useState([]); // { id, name, status, startDate, endDate, dailyImpressionCap, ads: [], adsLoaded, adsLoading, adsLoadError, adsAttempted } @@ -298,7 +306,7 @@ export default function AdminAdsPage() {
setSidebarOpen(false)} onNavClick={() => navigate("/feed")} onLogoClick={() => navigate("/feed")} @@ -320,7 +328,11 @@ export default function AdminAdsPage() { hideWordmark centerContent={

- {isPostsTab ? t('adminPanel.postsPageTitle') : t('adminPanel.pageTitle')} + {isPostsTab + ? t('adminPanel.postsPageTitle') + : isPostsRejectedTab + ? t('adminPanel.tabPostsRejected') + : t('adminPanel.pageTitle')}

} /> @@ -342,8 +354,8 @@ export default function AdminAdsPage() { ))}
- {isPostsTab ? ( - + {isPostsTab || isPostsRejectedTab ? ( + ) : ( <>