'use client'; import { useState, useEffect } from 'react'; import { Loader2, Save, Eye, EyeOff, RefreshCw, Check, AlertCircle } from 'lucide-react'; // Категории, которые управляются здесь (в админке tool, а не в админке блога). // Категория `engine` (TELEGRAM_API_BASE и т.п.) намеренно живёт в zeropost.ru/admin. const CATEGORIES = [ { slug: 'photo_search', title: 'Поиск фото', hint: 'Yandex Search API: provider, ключ, folder, лимиты.' }, // Сюда позже: { slug: 'billing', ... }, { slug: 'serpapi', ... } ]; export default function SystemSettings() { const [byCategory, setByCategory] = useState({}); const [loading, setLoading] = useState(true); const [error, setError] = useState(''); async function load() { setLoading(true); setError(''); try { const result = {}; for (const cat of CATEGORIES) { const res = await fetch(`/api/admin/settings?category=${cat.slug}`); if (!res.ok) throw new Error((await res.json()).error || `HTTP ${res.status}`); result[cat.slug] = await res.json(); } setByCategory(result); } catch (err) { setError(err.message); } finally { setLoading(false); } } useEffect(() => { load(); }, []); if (loading) { return (
); } if (error) { return (
{error}
); } return (
{CATEGORIES.map(cat => ( load()} /> ))}
); } function CategoryBlock({ category, rows, onSaved }) { return (

{category.title}

{category.hint &&

{category.hint}

}
{rows.length === 0 ? (

Нет настроек в этой категории.

) : (
{rows.map(r => ( ))}
)}
); } function SettingRow({ row, onSaved }) { const [value, setValue] = useState(row.value ?? ''); const [saving, setSaving] = useState(false); const [saved, setSaved] = useState(false); const [reveal, setReveal] = useState(false); const [err, setErr] = useState(''); // если row.value меняется снаружи — синхронизируем useEffect(() => { setValue(row.value ?? ''); }, [row.value]); const isSecret = row.is_secret; const dirty = value !== (row.value ?? ''); async function save() { setSaving(true); setErr(''); try { const res = await fetch(`/api/admin/settings/${encodeURIComponent(row.key)}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ value: value === '' ? null : value }), }); if (!res.ok) throw new Error((await res.json()).error || `HTTP ${res.status}`); setSaved(true); setTimeout(() => setSaved(false), 1500); onSaved?.(); } catch (e) { setErr(e.message); } finally { setSaving(false); } } // Маскировка отображения секретов: показываем хвост ***last4 пока не reveal=true const masked = isSecret && value && !reveal ? '•'.repeat(Math.max(value.length - 4, 4)) + value.slice(-4) : value; return (
{row.key} {isSecret && ( secret )}
{row.description && (

{row.description}

)}
{ // При маскированном просмотре редактирование запрещаем — пусть сначала откроют if (isSecret && !reveal) return; setValue(e.target.value); }} placeholder={isSecret ? '(скрыто)' : '(пусто)'} spellCheck={false} /> {isSecret && ( )}
{err && (
{err}
)}
Категория: {row.category} · обновлено{' '} {row.updated_at ? new Date(row.updated_at).toLocaleString('ru-RU') : '—'}
); }