added rejected posts for admin
This commit is contained in:
@@ -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);
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
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) {
|
||||
|
||||
@@ -141,6 +141,16 @@ export default function Sidebar({
|
||||
>
|
||||
{NavIcons.rules()} {t('adminPanel.postsNav')}
|
||||
</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>
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -247,9 +247,11 @@
|
||||
"adminPanel": {
|
||||
"campaignsNav": "Реклама (кампании)",
|
||||
"postsNav": "Все объявления",
|
||||
"tabsLabel": "Разделы админ-панели",
|
||||
"postsRejectedNav": "Отклонённые",
|
||||
"tabsLabel": "Вкладки админ-панели",
|
||||
"tabCampaigns": "Рекламные кампании",
|
||||
"tabPosts": "Все объявления",
|
||||
"tabPostsRejected": "Отклонённые",
|
||||
"pageTitle": "Рекламные кампании",
|
||||
"postsPageTitle": "Все объявления",
|
||||
"postsSearchPlaceholder": "Поиск по названию, описанию, автору",
|
||||
|
||||
@@ -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() {
|
||||
<div className={`f-app${sidebarOpen ? " f-sb-open" : ""}`}>
|
||||
<Sidebar
|
||||
open={sidebarOpen}
|
||||
activeNav={isPostsTab ? "admin-posts" : "admin-campaigns"}
|
||||
activeNav={isPostsTab ? "admin-posts" : isPostsRejectedTab ? "admin-posts-rejected" : "admin-campaigns"}
|
||||
onClose={() => setSidebarOpen(false)}
|
||||
onNavClick={() => navigate("/feed")}
|
||||
onLogoClick={() => navigate("/feed")}
|
||||
@@ -320,7 +328,11 @@ export default function AdminAdsPage() {
|
||||
hideWordmark
|
||||
centerContent={
|
||||
<h1 className="admp-title">
|
||||
{isPostsTab ? t('adminPanel.postsPageTitle') : t('adminPanel.pageTitle')}
|
||||
{isPostsTab
|
||||
? t('adminPanel.postsPageTitle')
|
||||
: isPostsRejectedTab
|
||||
? t('adminPanel.tabPostsRejected')
|
||||
: t('adminPanel.pageTitle')}
|
||||
</h1>
|
||||
}
|
||||
/>
|
||||
@@ -342,8 +354,8 @@ export default function AdminAdsPage() {
|
||||
))}
|
||||
</div>
|
||||
|
||||
{isPostsTab ? (
|
||||
<PostsTab isAdmin={isAdmin} />
|
||||
{isPostsTab || isPostsRejectedTab ? (
|
||||
<PostsTab isAdmin={isAdmin} mode={isPostsRejectedTab ? "rejected" : "all"} />
|
||||
) : (
|
||||
<>
|
||||
<div className="admp-toolbar">
|
||||
|
||||
Reference in New Issue
Block a user