Files
zeropost-tool/components/TopicBank.js
T
Ник (Claude) ab4e340db9 feat: onboarding + topic bank UI + channel limit handling
/onboarding: 3-шаговый вайзард (платформа → название/ниша → готово)
login/page.js: новый пользователь → /onboarding, существующий → /
TopicBank.js: просмотр/пополнение/добавление/удаление тем
ChannelEdit AI-стиль: TopicBank компонент внизу вкладки
channels/new: при 402 CHANNEL_LIMIT_REACHED → ошибка + redirect /plans
lib/engine.js: ENGINE_URL дефолт 3040 → 3030
API routes: /api/topics-bank/[channelId]/{refill,add}, /item/[id]
2026-06-12 11:50:22 +03:00

127 lines
4.7 KiB
JavaScript

'use client';
import { useState, useEffect } from 'react';
import { RefreshCw, Plus, Trash2, Loader2, Lightbulb, X } from 'lucide-react';
export default function TopicBank({ channelId }) {
const [topics, setTopics] = useState([]);
const [total, setTotal] = useState(0);
const [loading, setLoading] = useState(true);
const [refilling,setRefilling]= useState(false);
const [newTopic, setNewTopic] = useState('');
const [adding, setAdding] = useState(false);
const [showAdd, setShowAdd] = useState(false);
async function load() {
setLoading(true);
try {
const res = await fetch(`/api/topics-bank/${channelId}`).then(r => r.json());
setTopics(res.topics || []);
setTotal(res.total_unused || 0);
} catch {}
setLoading(false);
}
useEffect(() => { load(); }, [channelId]);
async function refill() {
setRefilling(true);
await fetch(`/api/topics-bank/${channelId}/refill`, { method: 'POST' });
await load();
setRefilling(false);
}
async function addManual() {
if (!newTopic.trim()) return;
setAdding(true);
await fetch(`/api/topics-bank/${channelId}/add`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ topics: [newTopic.trim()] }),
});
setNewTopic('');
setShowAdd(false);
await load();
setAdding(false);
}
async function deleteTopic(id) {
await fetch(`/api/topics-bank/item/${id}`, { method: 'DELETE' });
setTopics(t => t.filter(x => x.id !== id));
setTotal(n => Math.max(0, n - 1));
}
const unusedTopics = topics.filter(t => !t.is_used);
const stockColor = total >= 5 ? 'text-green-400' : total > 0 ? 'text-yellow-400' : 'text-red-400';
return (
<div className="card p-5 space-y-4">
<div className="flex items-center justify-between">
<div>
<h3 className="font-semibold text-sm flex items-center gap-2">
<Lightbulb className="w-4 h-4 text-accent" /> Банк тем
</h3>
<p className={`text-xs mt-0.5 ${stockColor}`}>
{total === 0 ? 'Темы закончились — нужно пополнить' : `${total} тем в запасе`}
</p>
</div>
<div className="flex gap-1.5">
<button onClick={() => setShowAdd(v => !v)} className="btn-ghost p-2" title="Добавить тему">
<Plus className="w-4 h-4" />
</button>
<button onClick={refill} disabled={refilling} className="btn-ghost p-2" title="Сгенерировать темы AI">
<RefreshCw className={`w-4 h-4 ${refilling ? 'animate-spin text-accent' : ''}`} />
</button>
</div>
</div>
{/* Добавить вручную */}
{showAdd && (
<div className="flex gap-2">
<input
value={newTopic}
onChange={e => setNewTopic(e.target.value)}
onKeyDown={e => e.key === 'Enter' && addManual()}
placeholder="Введите тему поста..."
className="input flex-1 text-sm py-1.5"
autoFocus
/>
<button onClick={addManual} disabled={adding || !newTopic.trim()} className="btn-primary px-3 py-1.5 text-sm">
{adding ? <Loader2 className="w-3.5 h-3.5 animate-spin" /> : 'Добавить'}
</button>
<button onClick={() => { setShowAdd(false); setNewTopic(''); }} className="btn-ghost p-2">
<X className="w-4 h-4" />
</button>
</div>
)}
{loading && <div className="py-4 text-center"><Loader2 className="w-4 h-4 animate-spin mx-auto text-accent" /></div>}
{!loading && unusedTopics.length === 0 && (
<div className="py-4 text-center text-sm text-gray-500">
Тем нет. Нажмите чтобы сгенерировать AI
</div>
)}
{!loading && unusedTopics.length > 0 && (
<ul className="space-y-1.5 max-h-64 overflow-y-auto">
{unusedTopics.map(t => (
<li key={t.id} className="flex items-start gap-2 text-sm group">
<span className="flex-1 text-gray-300 leading-snug">{t.topic}</span>
<button
onClick={() => deleteTopic(t.id)}
className="opacity-0 group-hover:opacity-100 btn-ghost p-1 shrink-0 text-gray-500 hover:text-red-400 transition-opacity"
>
<Trash2 className="w-3 h-3" />
</button>
</li>
))}
</ul>
)}
<p className="text-xs text-gray-500">
Темы используются при автоматической генерации постов. При запасе &lt;5 AI пополняет автоматически.
</p>
</div>
);
}