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}
+
+ +
+ ); + } + + if (meta.type === 'select') { + return ( +
+
+
+ +
{meta.desc}
+ +
+ {isDirty && ( + + )} + {isSaved && } +
+
+ ); + } + + if (meta.type === 'number') { + return ( +
+
+
+ +
{meta.desc}
+ change(key, e.target.value)} + className="input mt-2 text-sm py-1.5 w-24" /> +
+ {isDirty && ( + + )} + {isSaved && } +
+
+ ); + } + + if (meta.type === 'time') { + return ( +
+
+
+ +
{meta.desc}
+ change(key, e.target.value)} + className="input mt-2 text-sm py-1.5 w-32" /> +
+ {isDirty && ( + + )} + {isSaved && } +
+
+ ); + } + + if (meta.type === 'textarea') { + return ( +
+ +
{meta.desc}
+