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 };
|
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);
|
||||||
if (status) params.set("status", status);
|
if (status) params.set("status", status);
|
||||||
|
|
||||||
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) {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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(() => {
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -247,9 +247,11 @@
|
|||||||
"adminPanel": {
|
"adminPanel": {
|
||||||
"campaignsNav": "Реклама (кампании)",
|
"campaignsNav": "Реклама (кампании)",
|
||||||
"postsNav": "Все объявления",
|
"postsNav": "Все объявления",
|
||||||
"tabsLabel": "Разделы админ-панели",
|
"postsRejectedNav": "Отклонённые",
|
||||||
|
"tabsLabel": "Вкладки админ-панели",
|
||||||
"tabCampaigns": "Рекламные кампании",
|
"tabCampaigns": "Рекламные кампании",
|
||||||
"tabPosts": "Все объявления",
|
"tabPosts": "Все объявления",
|
||||||
|
"tabPostsRejected": "Отклонённые",
|
||||||
"pageTitle": "Рекламные кампании",
|
"pageTitle": "Рекламные кампании",
|
||||||
"postsPageTitle": "Все объявления",
|
"postsPageTitle": "Все объявления",
|
||||||
"postsSearchPlaceholder": "Поиск по названию, описанию, автору",
|
"postsSearchPlaceholder": "Поиск по названию, описанию, автору",
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
Reference in New Issue
Block a user