added rejected posts for admin

This commit is contained in:
SlimusMinus
2026-10-06 02:06:44 +03:00
parent 9ccad319cd
commit 0074e51829
6 changed files with 76 additions and 29 deletions

View File

@@ -62,13 +62,13 @@ function extractPage(body, size) {
return { items, last, total: hasTotal ? body.totalElements : undefined }; return { items, last, total: hasTotal ? body.totalElements : undefined };
} }
/** /**
* GET /api/v1/admin/posts?page&size&search&type&status * GET /api/v1/admin/posts?page&size&search&type&status
* *
* Постранично отдаёт ВСЕ объявления площадки вместе с данными автора. * Постранично отдаёт ВСЕ объявления площадки вместе с данными автора.
* Возвращает { items, last, total } — items уже нормализованы. * Возвращает { items, last, total } — items уже нормализованы.
*/ */
export async function getAdminPosts({ page = 0, size = 20, search = "", type = "", status = "" } = {}) { export async function getAdminPosts({ page = 0, size = 20, search = "", type = "", status = "" } = {}) {
const params = new URLSearchParams({ page, size }); const params = new URLSearchParams({ page, size });
if (search) params.set("search", search); if (search) params.set("search", search);
if (type) params.set("type", type); if (type) params.set("type", type);
@@ -77,7 +77,25 @@ export async function getAdminPosts({ page = 0, size = 20, search = "", type = "
const res = await apiFetch(`${ADMIN_BASE}/posts?${params.toString()}`); const res = await apiFetch(`${ADMIN_BASE}/posts?${params.toString()}`);
if (!res.ok) throw new AdminContentError(res.status, await readErrorMessage(res, "Failed to load posts")); if (!res.ok) throw new AdminContentError(res.status, await readErrorMessage(res, "Failed to load posts"));
return extractPage(await res.json(), size); 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 одинаково допустимы. */ /** DELETE /api/v1/admin/posts/{postId} — 204 и 200 одинаково допустимы. */
export async function deleteAdminPost(postId) { export async function deleteAdminPost(postId) {

View File

@@ -141,6 +141,16 @@ export default function Sidebar({
> >
{NavIcons.rules()} {t('adminPanel.postsNav')} {NavIcons.rules()} {t('adminPanel.postsNav')}
</button> </button>
<button
type="button"
className={`f-sb-item${activeNav === "admin-posts-rejected" ? " active" : ""}`}
onClick={() => {
navigate("/admin/ads?tab=rejected");
onClose();
}}
>
{NavIcons.rules()} {t('adminPanel.postsRejectedNav')}
</button>
</> </>
)} )}
</nav> </nav>

View File

@@ -7,6 +7,7 @@ import ImageViewer from "../ImageViewer";
import Toast from "../Toast"; import Toast from "../Toast";
import { import {
getAdminPosts, getAdminPosts,
getAdminRejectedPosts,
deleteAdminPost, deleteAdminPost,
updateAdminPostStatus, updateAdminPostStatus,
} from "../../api/adminContentApi"; } from "../../api/adminContentApi";
@@ -48,8 +49,9 @@ function authorLabelOf(post) {
* Разметка рассчитана на то, что страница уже нарисовала .admp > .admp-inner, * Разметка рассчитана на то, что страница уже нарисовала .admp > .admp-inner,
* поэтому собственного скролл-контейнера у вкладки нет. * поэтому собственного скролл-контейнера у вкладки нет.
*/ */
export default function PostsTab({ isAdmin }) { export default function PostsTab({ isAdmin, mode = "all" }) {
const { t } = useTranslation(); const { t } = useTranslation();
const isRejectedMode = mode === "rejected";
const [posts, setPosts] = useState([]); const [posts, setPosts] = useState([]);
const [page, setPage] = useState(0); const [page, setPage] = useState(0);
@@ -86,7 +88,8 @@ export default function PostsTab({ isAdmin }) {
setError(""); setError("");
try { 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)); setPosts((prev) => (append ? [...prev, ...result.items] : result.items));
setHasMore(!result.last); setHasMore(!result.last);
setPage(pageNum); setPage(pageNum);
@@ -98,7 +101,7 @@ export default function PostsTab({ isAdmin }) {
setLoading(false); setLoading(false);
setLoadingMore(false); setLoadingMore(false);
} }
}, [isAdmin, search, typeFilter, statusFilter, t]); }, [isAdmin, search, typeFilter, statusFilter, t, isRejectedMode]);
// Первичная загрузка + перезагрузка при смене фильтров // Первичная загрузка + перезагрузка при смене фильтров
useEffect(() => { useEffect(() => {

View File

@@ -247,9 +247,11 @@
"adminPanel": { "adminPanel": {
"campaignsNav": "Ads (campaigns)", "campaignsNav": "Ads (campaigns)",
"postsNav": "All listings", "postsNav": "All listings",
"tabsLabel": "Admin panel sections", "postsRejectedNav": "Rejected",
"tabCampaigns": "Advertising campaigns", "tabsLabel": "Admin panel tabs",
"tabCampaigns": "Ad campaigns",
"tabPosts": "All listings", "tabPosts": "All listings",
"tabPostsRejected": "Rejected",
"pageTitle": "Advertising campaigns", "pageTitle": "Advertising campaigns",
"postsPageTitle": "All listings", "postsPageTitle": "All listings",
"postsSearchPlaceholder": "Search by title, description, author", "postsSearchPlaceholder": "Search by title, description, author",

View File

@@ -247,9 +247,11 @@
"adminPanel": { "adminPanel": {
"campaignsNav": "Реклама (кампании)", "campaignsNav": "Реклама (кампании)",
"postsNav": "Все объявления", "postsNav": "Все объявления",
"tabsLabel": "Разделы админ-панели", "postsRejectedNav": "Отклонённые",
"tabsLabel": "Вкладки админ-панели",
"tabCampaigns": "Рекламные кампании", "tabCampaigns": "Рекламные кампании",
"tabPosts": "Все объявления", "tabPosts": "Все объявления",
"tabPostsRejected": "Отклонённые",
"pageTitle": "Рекламные кампании", "pageTitle": "Рекламные кампании",
"postsPageTitle": "Все объявления", "postsPageTitle": "Все объявления",
"postsSearchPlaceholder": "Поиск по названию, описанию, автору", "postsSearchPlaceholder": "Поиск по названию, описанию, автору",

View File

@@ -25,12 +25,14 @@ import "../styles/admin.css";
const POLL_INTERVAL_MS = 3000; const POLL_INTERVAL_MS = 3000;
const MAX_POLL_ATTEMPTS = 100; // ≈ 5 минут const MAX_POLL_ATTEMPTS = 100; // ≈ 5 минут
/** Активная вкладка задаётся query-параметром ?tab=posts, иначе — реклама. */ /** Активная вкладка задаётся query-параметром ?tab=posts или ?tab=rejected, иначе — реклама. */
const TAB_CAMPAIGNS = "campaigns"; const TAB_CAMPAIGNS = "campaigns";
const TAB_POSTS = "posts"; const TAB_POSTS = "posts";
const TAB_POSTS_REJECTED = "rejected";
const TABS = [ const TABS = [
{ key: TAB_CAMPAIGNS, labelKey: "adminPanel.tabCampaigns" }, { key: TAB_CAMPAIGNS, labelKey: "adminPanel.tabCampaigns" },
{ key: TAB_POSTS, labelKey: "adminPanel.tabPosts" }, { key: TAB_POSTS, labelKey: "adminPanel.tabPosts" },
{ key: TAB_POSTS_REJECTED, labelKey: "adminPanel.tabPostsRejected" },
]; ];
/** "2026-09-20T14:30:00" → "20.09.2026 14:30" */ /** "2026-09-20T14:30:00" → "20.09.2026 14:30" */
@@ -60,12 +62,18 @@ export default function AdminAdsPage() {
const isDesktop = useIsDesktop(); const isDesktop = useIsDesktop();
const modals = useModals(); 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 isPostsTab = tab === TAB_POSTS;
const isPostsRejectedTab = tab === TAB_POSTS_REJECTED;
const selectTab = useCallback((key) => { const selectTab = useCallback((key) => {
// Для рекламы параметр не пишем — /admin/ads без query открывает именно её. // Для рекламы параметр не пишем — /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]); }, [setSearchParams]);
const [campaigns, setCampaigns] = useState([]); // { id, name, status, startDate, endDate, dailyImpressionCap, ads: [], adsLoaded, adsLoading, adsLoadError, adsAttempted } const [campaigns, setCampaigns] = useState([]); // { id, name, status, startDate, endDate, dailyImpressionCap, ads: [], adsLoaded, adsLoading, adsLoadError, adsAttempted }
@@ -298,7 +306,7 @@ export default function AdminAdsPage() {
<div className={`f-app${sidebarOpen ? " f-sb-open" : ""}`}> <div className={`f-app${sidebarOpen ? " f-sb-open" : ""}`}>
<Sidebar <Sidebar
open={sidebarOpen} open={sidebarOpen}
activeNav={isPostsTab ? "admin-posts" : "admin-campaigns"} activeNav={isPostsTab ? "admin-posts" : isPostsRejectedTab ? "admin-posts-rejected" : "admin-campaigns"}
onClose={() => setSidebarOpen(false)} onClose={() => setSidebarOpen(false)}
onNavClick={() => navigate("/feed")} onNavClick={() => navigate("/feed")}
onLogoClick={() => navigate("/feed")} onLogoClick={() => navigate("/feed")}
@@ -320,7 +328,11 @@ export default function AdminAdsPage() {
hideWordmark hideWordmark
centerContent={ centerContent={
<h1 className="admp-title"> <h1 className="admp-title">
{isPostsTab ? t('adminPanel.postsPageTitle') : t('adminPanel.pageTitle')} {isPostsTab
? t('adminPanel.postsPageTitle')
: isPostsRejectedTab
? t('adminPanel.tabPostsRejected')
: t('adminPanel.pageTitle')}
</h1> </h1>
} }
/> />
@@ -342,8 +354,8 @@ export default function AdminAdsPage() {
))} ))}
</div> </div>
{isPostsTab ? ( {isPostsTab || isPostsRejectedTab ? (
<PostsTab isAdmin={isAdmin} /> <PostsTab isAdmin={isAdmin} mode={isPostsRejectedTab ? "rejected" : "all"} />
) : ( ) : (
<> <>
<div className="admp-toolbar"> <div className="admp-toolbar">