feat: admin channels — list, editor, publish panel, TG/VK/Max support
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
import { requireAdminAuth } from '@/lib/adminAuth';
|
||||
import { adminListChannels, adminListArticles } from '@/lib/engine';
|
||||
import ChannelEditor from '@/components/admin/ChannelEditor';
|
||||
import { notFound } from 'next/navigation';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export async function generateMetadata({ params }) {
|
||||
const { id } = await params;
|
||||
if (id === 'new') return { title: 'Новый канал' };
|
||||
return { title: 'Настройки канала' };
|
||||
}
|
||||
|
||||
export default async function AdminChannelPage({ params }) {
|
||||
await requireAdminAuth();
|
||||
const { id } = await params;
|
||||
|
||||
let channel = null;
|
||||
let articles = [];
|
||||
|
||||
if (id !== 'new') {
|
||||
const channels = await adminListChannels();
|
||||
channel = channels.find(c => String(c.id) === id);
|
||||
if (!channel) notFound();
|
||||
}
|
||||
|
||||
try {
|
||||
const raw = await adminListArticles({ limit: 50 });
|
||||
articles = (Array.isArray(raw) ? raw : raw?.articles || []).filter(a => a.status === 'published');
|
||||
} catch {}
|
||||
|
||||
return <ChannelEditor channel={channel} articles={articles} />;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { requireAdminAuth } from '@/lib/adminAuth';
|
||||
import ChannelEditor from '@/components/admin/ChannelEditor';
|
||||
|
||||
export const metadata = { title: 'Новый канал' };
|
||||
|
||||
export default async function NewChannelPage() {
|
||||
await requireAdminAuth();
|
||||
return <ChannelEditor channel={null} articles={[]} />;
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
import { requireAdminAuth } from '@/lib/adminAuth';
|
||||
import { adminListChannels } from '@/lib/engine';
|
||||
import Link from 'next/link';
|
||||
import { Plus, Send, MessageSquare, Users } from 'lucide-react';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
export const metadata = { title: 'Каналы' };
|
||||
|
||||
const PLATFORM_LABELS = {
|
||||
telegram: { label: 'Telegram', color: 'bg-blue-50 dark:bg-blue-950 text-blue-600 dark:text-blue-400', icon: '✈️' },
|
||||
vk: { label: 'ВКонтакте', color: 'bg-indigo-50 dark:bg-indigo-950 text-indigo-600 dark:text-indigo-400', icon: '🔵' },
|
||||
max: { label: 'Max', color: 'bg-purple-50 dark:bg-purple-950 text-purple-600 dark:text-purple-400', icon: '🟣' },
|
||||
};
|
||||
|
||||
export default async function AdminChannelsPage() {
|
||||
await requireAdminAuth();
|
||||
let channels = [];
|
||||
try { channels = await adminListChannels(); } catch {}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-neutral-900 dark:text-neutral-100">Каналы</h1>
|
||||
<p className="text-sm text-neutral-500 mt-0.5">Публикация контента zeropost.ru в соцсети</p>
|
||||
</div>
|
||||
<Link
|
||||
href="/admin/channels/new"
|
||||
className="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-emerald-500 hover:bg-emerald-600 text-white text-sm font-medium transition-colors"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
Добавить канал
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{channels.length === 0 ? (
|
||||
<div className="bg-white dark:bg-neutral-900 rounded-xl border border-neutral-200 dark:border-neutral-800 p-12 text-center">
|
||||
<div className="text-4xl mb-4">📢</div>
|
||||
<h2 className="text-lg font-semibold text-neutral-900 dark:text-neutral-100 mb-2">Каналов пока нет</h2>
|
||||
<p className="text-sm text-neutral-500 mb-6">Добавьте Telegram-канал, страницу ВКонтакте или канал Max<br/>чтобы публиковать статьи zeropost.ru в один клик</p>
|
||||
<Link
|
||||
href="/admin/channels/new"
|
||||
className="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-emerald-500 hover:bg-emerald-600 text-white text-sm font-medium transition-colors"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
Добавить первый канал
|
||||
</Link>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{channels.map(ch => {
|
||||
const pl = PLATFORM_LABELS[ch.platform] || PLATFORM_LABELS.telegram;
|
||||
return (
|
||||
<Link
|
||||
key={ch.id}
|
||||
href={`/admin/channels/${ch.id}`}
|
||||
className="bg-white dark:bg-neutral-900 rounded-xl border border-neutral-200 dark:border-neutral-800 p-5 hover:border-emerald-300 dark:hover:border-emerald-700 hover:shadow-sm transition-all group"
|
||||
>
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<div className={`text-2xl w-10 h-10 flex items-center justify-center rounded-xl ${pl.color}`}>
|
||||
{pl.icon}
|
||||
</div>
|
||||
<span className={`text-xs px-2 py-0.5 rounded-full font-medium ${
|
||||
ch.is_active
|
||||
? 'bg-emerald-50 dark:bg-emerald-950 text-emerald-600 dark:text-emerald-400'
|
||||
: 'bg-neutral-100 dark:bg-neutral-800 text-neutral-400'
|
||||
}`}>
|
||||
{ch.is_active ? 'Активен' : 'Неактивен'}
|
||||
</span>
|
||||
</div>
|
||||
<h3 className="font-semibold text-neutral-900 dark:text-neutral-100 group-hover:text-emerald-600 dark:group-hover:text-emerald-400 transition-colors mb-1">
|
||||
{ch.name}
|
||||
</h3>
|
||||
<p className="text-xs text-neutral-400 mb-3">
|
||||
{pl.label}
|
||||
{ch.tg_username && ` · @${ch.tg_username}`}
|
||||
{ch.vk_group_id && ` · id${ch.vk_group_id}`}
|
||||
</p>
|
||||
<div className="flex items-center gap-3 text-xs text-neutral-400">
|
||||
<span className="flex items-center gap-1"><Send className="w-3 h-3" /> Опубликовать</span>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { checkAdminAuth } from '@/lib/adminAuth';
|
||||
import { adminPublishToChannel } from '@/lib/engine';
|
||||
|
||||
export async function POST(req, { params }) {
|
||||
if (!(await checkAdminAuth())) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
const { id } = await params;
|
||||
const body = await req.json();
|
||||
try {
|
||||
const data = await adminPublishToChannel(id, body);
|
||||
return NextResponse.json(data);
|
||||
} catch (e) {
|
||||
return NextResponse.json({ error: e.message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { checkAdminAuth } from '@/lib/adminAuth';
|
||||
import { adminUpdateChannel, adminDeleteChannel } from '@/lib/engine';
|
||||
|
||||
export async function PATCH(req, { params }) {
|
||||
if (!(await checkAdminAuth())) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
const { id } = await params;
|
||||
const body = await req.json();
|
||||
const data = await adminUpdateChannel(id, body);
|
||||
return NextResponse.json(data);
|
||||
}
|
||||
|
||||
export async function DELETE(req, { params }) {
|
||||
if (!(await checkAdminAuth())) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
const { id } = await params;
|
||||
const data = await adminDeleteChannel(id);
|
||||
return NextResponse.json(data);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { checkAdminAuth } from '@/lib/adminAuth';
|
||||
import { adminListChannels, adminCreateChannel } from '@/lib/engine';
|
||||
|
||||
export async function GET() {
|
||||
if (!(await checkAdminAuth())) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
const data = await adminListChannels();
|
||||
return NextResponse.json(data);
|
||||
}
|
||||
|
||||
export async function POST(req) {
|
||||
if (!(await checkAdminAuth())) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
const body = await req.json();
|
||||
const data = await adminCreateChannel(body);
|
||||
return NextResponse.json(data);
|
||||
}
|
||||
Reference in New Issue
Block a user