diff --git a/components/AdminPanel.js b/components/AdminPanel.js
index f62782c..bdc4d90 100644
--- a/components/AdminPanel.js
+++ b/components/AdminPanel.js
@@ -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 } from 'lucide-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 Link from 'next/link';
import AdminBilling from './admin/AdminBilling';
import AdminUsers from './admin/AdminUsers';
@@ -8,6 +8,7 @@ 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';
// ──────────────────────────────────────────────────────────────
// Sidebar navigation
@@ -21,6 +22,7 @@ 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: 'promos', label: 'Промокоды', icon: Tag, desc: 'Коды для кредитов и скидок' },
{ id: 'billing', label: 'Пользователи', icon: Users, desc: 'Балансы и кредиты' },
@@ -74,6 +76,7 @@ export default function AdminPanel({ initialSection = 'settings' }) {
{section === 'queue' && }
{section === 'logs' && }
{section === 'autogen' && }
+ {section === 'content' && }
{section === 'plans' && }
{section === 'promos' && }
{section === 'billing' && }
diff --git a/components/admin/AdminContent.js b/components/admin/AdminContent.js
new file mode 100644
index 0000000..ae813ce
--- /dev/null
+++ b/components/admin/AdminContent.js
@@ -0,0 +1,302 @@
+'use client';
+import { useState, useEffect } from 'react';
+import { Save, Loader2, Check, RefreshCw, Info } from 'lucide-react';
+
+// Метаданные полей для красивого UI
+const FIELD_META = {
+ DEFAULT_POST_LANGUAGE: {
+ label: 'Язык постов',
+ desc: 'Применяется к новым каналам',
+ type: 'select',
+ options: [
+ { v: 'ru', l: '🇷🇺 Русский' },
+ { v: 'en', l: '🇬🇧 English' },
+ { v: 'auto', l: '🌐 Авто (по нише)' },
+ ],
+ },
+ DEFAULT_POST_LENGTH: {
+ label: 'Длина поста',
+ desc: 'short ≈ 300-500 зн, medium ≈ 600-1000 зн, long ≈ 1200-2000 зн',
+ type: 'select',
+ options: [
+ { v: 'short', l: '📏 Короткий (300-500 зн)' },
+ { v: 'medium', l: '📄 Средний (600-1000 зн)' },
+ { v: 'long', l: '📃 Длинный (1200-2000 зн)' },
+ ],
+ },
+ DEFAULT_POST_STYLE: {
+ label: 'Стиль написания',
+ desc: 'Тон голоса по умолчанию',
+ type: 'select',
+ options: [
+ { v: 'informative', l: '📚 Информативный' },
+ { v: 'casual', l: '😊 Разговорный' },
+ { v: 'professional', l: '👔 Профессиональный' },
+ { v: 'storytelling', l: '📖 Сторителлинг' },
+ { v: 'provocative', l: '🔥 Провокационный' },
+ ],
+ },
+ DEFAULT_POST_GOAL: {
+ label: 'Цель поста',
+ desc: 'На что ориентирован контент',
+ type: 'select',
+ options: [
+ { v: 'educational', l: '🎓 Образовательный' },
+ { v: 'entertainment',l: '🎭 Развлекательный' },
+ { v: 'sales', l: '💰 Продажи' },
+ { v: 'engagement', l: '❤️ Вовлечение' },
+ { v: 'news', l: '📰 Новости' },
+ ],
+ },
+ DEFAULT_IMAGE_ENABLED: {
+ label: 'Генерация изображений',
+ desc: 'Создавать картинки для постов (расходует кредиты)',
+ type: 'toggle',
+ },
+ DEFAULT_EMOJI_ENABLED: {
+ label: 'Эмодзи в постах',
+ desc: 'Добавлять эмодзи по умолчанию',
+ type: 'toggle',
+ },
+ DEFAULT_HASHTAGS_IN_POST: {
+ label: 'Хештеги в постах',
+ desc: 'Автоматически добавлять хештеги в конец поста',
+ type: 'toggle',
+ },
+ DEFAULT_AUTO_DRAFT_COUNT: {
+ label: 'Авто-черновиков в день',
+ desc: 'Для новых каналов с включённой авто-генерацией',
+ type: 'number',
+ min: 1, max: 10,
+ },
+ DEFAULT_AUTO_DRAFT_TIME: {
+ label: 'Время генерации черновиков',
+ desc: 'HH:MM (московское время UTC+3)',
+ type: 'time',
+ },
+ DEFAULT_AI_STYLE_PROMPT: {
+ label: 'Базовые инструкции стиля',
+ desc: 'Применяются ко всем каналам поверх индивидуальных настроек',
+ type: 'textarea',
+ placeholder: 'Например: Всегда пиши от первого лица. Используй активный залог...',
+ },
+};
+
+const GROUP_ORDER = [
+ {
+ title: 'Контент',
+ keys: ['DEFAULT_POST_LANGUAGE', 'DEFAULT_POST_STYLE', 'DEFAULT_POST_GOAL', 'DEFAULT_POST_LENGTH'],
+ },
+ {
+ title: 'Форматирование',
+ keys: ['DEFAULT_IMAGE_ENABLED', 'DEFAULT_EMOJI_ENABLED', 'DEFAULT_HASHTAGS_IN_POST'],
+ },
+ {
+ title: 'Авто-черновики',
+ keys: ['DEFAULT_AUTO_DRAFT_COUNT', 'DEFAULT_AUTO_DRAFT_TIME'],
+ },
+ {
+ title: 'AI-инструкции',
+ keys: ['DEFAULT_AI_STYLE_PROMPT'],
+ },
+];
+
+export default function AdminContent() {
+ const [rows, setRows] = useState([]);
+ const [vals, setVals] = useState({});
+ const [dirty, setDirty] = useState({});
+ const [saving, setSaving] = useState({});
+ const [saved, setSaved] = useState({});
+ const [loading, setLoading] = useState(true);
+
+ async function load() {
+ setLoading(true);
+ try {
+ const res = await fetch('/api/admin/settings?category=content').then(r => r.json());
+ const arr = Array.isArray(res) ? res : [];
+ setRows(arr);
+ const v = Object.fromEntries(arr.map(r => [r.key, r.value ?? '']));
+ setVals(v);
+ setDirty({});
+ } catch {}
+ setLoading(false);
+ }
+
+ useEffect(() => { load(); }, []);
+
+ function change(key, val) {
+ setVals(v => ({ ...v, [key]: val }));
+ setDirty(d => ({ ...d, [key]: true }));
+ setSaved(s => ({ ...s, [key]: false }));
+ }
+
+ async function save(key) {
+ setSaving(s => ({ ...s, [key]: true }));
+ try {
+ const res = await fetch(`/api/admin/settings/${encodeURIComponent(key)}`, {
+ method: 'PUT',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ value: vals[key] }),
+ }).then(r => r.json());
+ if (!res.error) {
+ setDirty(d => ({ ...d, [key]: false }));
+ setSaved(s => ({ ...s, [key]: true }));
+ setTimeout(() => setSaved(s => ({ ...s, [key]: false })), 2000);
+ }
+ } catch {}
+ setSaving(s => ({ ...s, [key]: false }));
+ }
+
+ function renderField(key) {
+ const meta = FIELD_META[key];
+ if (!meta) return null;
+ const val = vals[key] ?? '';
+ const isDirty = dirty[key];
+ const isSaving = saving[key];
+ const isSaved = saved[key];
+
+ if (meta.type === 'toggle') {
+ const isOn = val === 'true';
+ return (
+
+
+
{meta.label}
+
{meta.desc}
+
+
{ change(key, isOn ? 'false' : 'true'); setTimeout(() => save(key), 50); }}
+ className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${isOn ? 'bg-accent' : 'bg-gray-600'}`}>
+
+
+
+ );
+ }
+
+ if (meta.type === 'select') {
+ return (
+
+
+
+
{meta.label}
+
{meta.desc}
+
change(key, e.target.value)}
+ className="input mt-2 text-sm py-1.5 w-full max-w-xs">
+ {meta.options?.map(o => {o.l} )}
+
+
+ {isDirty && (
+
save(key)} disabled={isSaving}
+ className="btn-primary mt-6 px-3 py-1.5 text-sm flex items-center gap-1.5 shrink-0">
+ {isSaving ? : }
+ Сохранить
+
+ )}
+ {isSaved &&
}
+
+
+ );
+ }
+
+ if (meta.type === 'number') {
+ return (
+
+
+
+
{meta.label}
+
{meta.desc}
+
change(key, e.target.value)}
+ className="input mt-2 text-sm py-1.5 w-24" />
+
+ {isDirty && (
+
save(key)} disabled={isSaving}
+ className="btn-primary mt-6 px-3 py-1.5 text-sm flex items-center gap-1.5 shrink-0">
+ {isSaving ? : }
+ Сохранить
+
+ )}
+ {isSaved &&
}
+
+
+ );
+ }
+
+ if (meta.type === 'time') {
+ return (
+
+
+
+
{meta.label}
+
{meta.desc}
+
change(key, e.target.value)}
+ className="input mt-2 text-sm py-1.5 w-32" />
+
+ {isDirty && (
+
save(key)} disabled={isSaving}
+ className="btn-primary mt-6 px-3 py-1.5 text-sm flex items-center gap-1.5 shrink-0">
+ {isSaving ? : }
+ Сохранить
+
+ )}
+ {isSaved &&
}
+
+
+ );
+ }
+
+ if (meta.type === 'textarea') {
+ return (
+
+
{meta.label}
+
{meta.desc}
+
+ );
+ }
+
+ return null;
+ }
+
+ return (
+
+
+
+
Настройки контента
+
Дефолты для новых каналов. Существующие каналы не затрагиваются.
+
+
+
+
+
+
+ {/* Подсказка */}
+
+
+
+ Эти настройки применяются при создании нового канала как стартовые значения.
+ Каждый канал можно затем настроить индивидуально через вкладку «AI-стиль».
+
+
+
+ {loading &&
}
+
+ {!loading && GROUP_ORDER.map(group => (
+
+
{group.title}
+ {group.keys.map(key => renderField(key))}
+
+ ))}
+
+ );
+}