feat: admin panel — SMTP, topic bank, maintenance mode UI

AdminTopicBank.js: банк тем блога по категориям
  Аккордеон: неиспользованные + использованные темы
  Прогресс-бар использования, кнопка +10 AI (фоновая генерация)
  Мультистрочное добавление (одна строка = одна тема)
AdminPanel: + Email/SMTP + Банк тем блога + Mail иконка
SmtpTestButton: тест отправки прямо в разделе SMTP
API routes: /api/admin/blog-topics, /[id], /generate, /api/admin/email/test
This commit is contained in:
Ник (Claude)
2026-06-13 11:46:10 +03:00
parent 2e9f099b95
commit 789cfe10db
6 changed files with 331 additions and 6 deletions
+49 -6
View File
@@ -1,6 +1,6 @@
'use client';
import { useState } from 'react';
import { Settings2, CreditCard, TrendingUp, Users, ChevronRight, Loader2, Eye, EyeOff, Save, RefreshCw, Check, AlertCircle, BarChart3, ArrowLeft, Zap, Tag, AlertTriangle, BookOpen, Sliders } from 'lucide-react';
import { Settings2, CreditCard, TrendingUp, Users, ChevronRight, Loader2, Eye, EyeOff, Save, RefreshCw, Check, AlertCircle, BarChart3, ArrowLeft, Zap, Tag, AlertTriangle, BookOpen, Sliders, Mail } from 'lucide-react';
import Link from 'next/link';
import AdminBilling from './admin/AdminBilling';
import AdminUsers from './admin/AdminUsers';
@@ -8,7 +8,8 @@ import AdminPromos from './admin/AdminPromos';
import AdminQueue from './admin/AdminQueue';
import AdminLogs from './admin/AdminLogs';
import AdminAutogen from './admin/AdminAutogen';
import AdminContent from './admin/AdminContent';
import AdminContent from './admin/AdminContent';
import AdminTopicBank from './admin/AdminTopicBank';
// ──────────────────────────────────────────────────────────────
// Sidebar navigation
@@ -22,8 +23,10 @@ const SECTIONS = [
{ id: 'queue', label: 'Очередь', icon: Zap, desc: 'Задачи генерации, ошибки' },
{ id: 'logs', label: 'Логи ошибок', icon: AlertTriangle, desc: 'Последние сбои и проблемы' },
{ id: 'autogen', label: 'Автогенерация', icon: BookOpen, desc: 'Расписание статей блога' },
{ id: 'content', label: 'Контент-дефолты', icon: Sliders, desc: 'Настройки для новых каналов' },
{ id: 'plans', label: 'Тарифы', icon: BarChart3, desc: 'Планы, кредиты, операции' },
{ id: 'content', label: 'Контент-дефолты', icon: Sliders, desc: 'Настройки для новых каналов' },
{ id: 'topicbank', label: 'Банк тем блога', icon: BookOpen, desc: 'Темы для zeropost.ru' },
{ id: 'smtp', label: 'Email / SMTP', icon: Mail, desc: 'Уведомления пользователям' },
{ id: 'plans', label: 'Тарифы', icon: BarChart3, desc: 'Планы, кредиты, операции' },
{ id: 'promos', label: 'Промокоды', icon: Tag, desc: 'Коды для кредитов и скидок' },
{ id: 'billing', label: 'Пользователи', icon: Users, desc: 'Балансы и кредиты' },
];
@@ -76,8 +79,10 @@ export default function AdminPanel({ initialSection = 'settings' }) {
{section === 'queue' && <AdminQueue />}
{section === 'logs' && <AdminLogs />}
{section === 'autogen' && <AdminAutogen />}
{section === 'content' && <AdminContent />}
{section === 'plans' && <PlansSection />}
{section === 'content' && <AdminContent />}
{section === 'topicbank' && <AdminTopicBank />}
{section === 'smtp' && <SettingsSection categories={['smtp']} extraActions={<SmtpTestButton />} />}
{section === 'plans' && <PlansSection />}
{section === 'promos' && <AdminPromos />}
{section === 'billing' && <AdminUsers />}
</div>
@@ -626,3 +631,41 @@ function DashboardSection() {
</div>
);
}
// ── SMTP Test Button ──────────────────────────────────────────
function SmtpTestButton() {
const [email, setEmail] = useState('');
const [busy, setBusy] = useState(false);
const [msg, setMsg] = useState('');
async function test() {
if (!email.trim()) return;
setBusy(true);
const res = await fetch('/api/admin/email/test', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ to: email }),
}).then(r => r.json());
setBusy(false);
setMsg(res.ok ? '✅ Письмо отправлено' : '❌ ' + (res.error || res.message));
setTimeout(() => setMsg(''), 5000);
}
return (
<div className="card p-4 border-accent/20 bg-accent/5">
<h3 className="font-medium text-sm mb-3">Тест отправки</h3>
<div className="flex gap-2">
<input value={email} onChange={e => setEmail(e.target.value)}
onKeyDown={e => e.key === 'Enter' && test()}
type="email" placeholder="test@example.com"
className="input flex-1 text-sm py-1.5" />
<button onClick={test} disabled={busy || !email.trim()}
className="btn-primary px-3 py-1.5 text-sm flex items-center gap-1.5">
{busy ? <Loader2 className="w-3.5 h-3.5 animate-spin" /> : <Mail className="w-3.5 h-3.5" />}
Отправить тест
</button>
</div>
{msg && <p className="text-xs mt-2">{msg}</p>}
</div>
);
}