feat(admin/articles): delete button + 'scheduled' badge for future drip slots

ArticleRowActions (client component): edit / view / delete actions.
Delete: confirm dialog → /admin/api/articles/:id DELETE → reload page.
StatusBadge: shows blue 'выйдет ДД.ММ ЧЧ:ММ' badge when published_at is
in the future (article scheduled for a drip slot), green 'Опубликована'
when actually live, amber 'Черновик' otherwise.

Proxy: app/admin/api/articles/[id]/route.js → engine /api/articles/:id.
This commit is contained in:
Aleksei Pavlov
2026-06-19 20:55:34 +03:00
parent 4151894935
commit c7e9196370
3 changed files with 130 additions and 37 deletions
+4 -25
View File
@@ -1,6 +1,7 @@
import Link from 'next/link';
import { adminListArticles } from '@/lib/engine';
import { Plus, Pencil, Eye } from 'lucide-react';
import { Plus } from 'lucide-react';
import ArticleRowActions, { StatusBadge } from '@/components/admin/ArticleRowActions';
export const dynamic = 'force-dynamic';
export const metadata = { title: 'Статьи' };
@@ -55,35 +56,13 @@ export default async function AdminArticlesPage() {
</div>
</td>
<td className="px-3 py-3.5">
<span className={`text-xs px-2 py-0.5 rounded-full font-medium ${
a.status === 'published'
? 'bg-emerald-50 dark:bg-emerald-950 text-emerald-600 dark:text-emerald-400'
: 'bg-amber-50 dark:bg-amber-950 text-amber-600 dark:text-amber-400'
}`}>
{a.status === 'published' ? 'Опубликована' : 'Черновик'}
</span>
<StatusBadge status={a.status} published_at={a.published_at} />
</td>
<td className="px-3 py-3.5 text-xs text-neutral-400 hidden lg:table-cell">
{a.published_at ? new Date(a.published_at).toLocaleDateString('ru-RU') : '—'}
</td>
<td className="px-3 py-3.5">
<div className="flex items-center gap-1">
<Link
href={`/admin/articles/${a.id}`}
className="p-1.5 rounded-lg hover:bg-neutral-100 dark:hover:bg-neutral-700 text-neutral-400 hover:text-neutral-700 dark:hover:text-neutral-200 transition-colors"
title="Редактировать"
>
<Pencil className="w-3.5 h-3.5" />
</Link>
<Link
href={`/blog/${a.slug}`}
target="_blank"
className="p-1.5 rounded-lg hover:bg-neutral-100 dark:hover:bg-neutral-700 text-neutral-400 hover:text-neutral-700 dark:hover:text-neutral-200 transition-colors"
title="Открыть на сайте"
>
<Eye className="w-3.5 h-3.5" />
</Link>
</div>
<ArticleRowActions article={a} />
</td>
</tr>
))}
+31 -12
View File
@@ -1,18 +1,37 @@
/**
* Proxy для /admin/api/articles/:id → engine /api/articles/:id
* Используется для DELETE из admin UI.
*/
import { NextResponse } from 'next/server';
import { checkAdminAuth } from '@/lib/adminAuth';
import { adminUpdateArticle, adminDeleteArticle } from '@/lib/engine';
export async function PATCH(req, { params }) {
if (!(await checkAdminAuth())) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
const E = process.env.ENGINE_URL || 'http://127.0.0.1:3030';
const S = process.env.ENGINE_SECRET || 'zeropost_internal_2026';
async function proxy(req, { params }) {
if (!(await checkAdminAuth())) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const { id } = await params;
const body = await req.json();
const result = await adminUpdateArticle(id, body);
return NextResponse.json(result);
const url = `${E}/api/articles/${id}`;
const headers = { 'x-internal-secret': S, 'x-user-id': '1' };
let body;
if (req.method !== 'GET' && req.method !== 'HEAD') {
headers['Content-Type'] = 'application/json';
body = (await req.text()) || undefined;
}
try {
const res = await fetch(url, { method: req.method, headers, body, cache: 'no-store' });
const data = await res.json().catch(() => ({}));
return NextResponse.json(data, { status: res.status });
} catch (err) {
return NextResponse.json({ error: err.message }, { status: 502 });
}
}
export async function DELETE(req, { params }) {
if (!(await checkAdminAuth())) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
const { id } = await params;
const result = await adminDeleteArticle(id);
return NextResponse.json(result);
}
export const GET = proxy;
export const PATCH = proxy;
export const PUT = proxy;
export const DELETE = proxy;