|
|
|
@@ -2,14 +2,13 @@
|
|
|
|
|
import { useState, useEffect, useCallback } from 'react';
|
|
|
|
|
import { CheckCircle, RefreshCw, Pencil, Clock, ImageIcon, ChevronDown, ChevronUp, X, Save } from 'lucide-react';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const COVER_STYLES = [
|
|
|
|
|
{ id: 'tech-photo', name: 'Tech Photo — реальное железо' },
|
|
|
|
|
{ id: '3d-device', name: '3D Device — рендер устройства' },
|
|
|
|
|
{ id: 'code-screen', name: 'Code Screen — экран с кодом' },
|
|
|
|
|
{ id: 'data-flow', name: 'Data Flow — абстракция данных' },
|
|
|
|
|
{ id: 'ai-neural', name: 'AI Neural — нейросетевая эстетика' },
|
|
|
|
|
{ id: 'cinematic-tech', name: 'Cinematic — кинематографичный' },
|
|
|
|
|
{ id: 'robot-workspace', name: '🤖 Робот за работой' },
|
|
|
|
|
{ id: 'tech-hardware', name: '🖥 Железо и серверы' },
|
|
|
|
|
{ id: 'code-terminal', name: '💻 Код и терминал' },
|
|
|
|
|
{ id: 'cyber-security', name: '🔒 Кибербезопасность' },
|
|
|
|
|
{ id: 'data-dashboard', name: '📊 Данные и аналитика' },
|
|
|
|
|
{ id: 'future-tech', name: '🌆 Технологии будущего' },
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
const CATEGORY_LABELS = {
|
|
|
|
@@ -31,16 +30,10 @@ function DraftCard({ draft, onApproved, onCoverRegenerated }) {
|
|
|
|
|
const [coverUrl, setCoverUrl] = useState(draft.cover_url);
|
|
|
|
|
const [msg, setMsg] = useState('');
|
|
|
|
|
|
|
|
|
|
const engineUrl = process.env.NEXT_PUBLIC_ENGINE_URL || '';
|
|
|
|
|
|
|
|
|
|
async function approve() {
|
|
|
|
|
setLoading(true);
|
|
|
|
|
setMsg('');
|
|
|
|
|
setLoading(true); setMsg('');
|
|
|
|
|
try {
|
|
|
|
|
const r = await fetch(`${engineUrl}/api/drafts/${draft.id}/approve`, {
|
|
|
|
|
method: 'PATCH',
|
|
|
|
|
headers: { 'x-internal-secret': process.env.NEXT_PUBLIC_ENGINE_SECRET || '' },
|
|
|
|
|
});
|
|
|
|
|
const r = await fetch(`/api/admin/drafts/${draft.id}/approve`, { method: 'PATCH' });
|
|
|
|
|
const d = await r.json();
|
|
|
|
|
if (d.ok) {
|
|
|
|
|
const slot = d.scheduled_at
|
|
|
|
@@ -48,23 +41,17 @@ function DraftCard({ draft, onApproved, onCoverRegenerated }) {
|
|
|
|
|
: null;
|
|
|
|
|
setMsg(slot ? `✅ Одобрено, выйдет в ${slot}` : '✅ Одобрено');
|
|
|
|
|
setTimeout(() => onApproved(draft.id), 1200);
|
|
|
|
|
} else {
|
|
|
|
|
setMsg('❌ ' + (d.error || 'Ошибка'));
|
|
|
|
|
}
|
|
|
|
|
} else setMsg('❌ ' + (d.error || 'Ошибка'));
|
|
|
|
|
} catch { setMsg('❌ Ошибка сети'); }
|
|
|
|
|
setLoading(false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function regenCover() {
|
|
|
|
|
setRegenLoading(true);
|
|
|
|
|
setMsg('');
|
|
|
|
|
setRegenLoading(true); setMsg('');
|
|
|
|
|
try {
|
|
|
|
|
const r = await fetch(`${engineUrl}/api/drafts/${draft.id}/regenerate-cover`, {
|
|
|
|
|
const r = await fetch(`/api/admin/drafts/${draft.id}/regenerate-cover`, {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: {
|
|
|
|
|
'Content-Type': 'application/json',
|
|
|
|
|
'x-internal-secret': process.env.NEXT_PUBLIC_ENGINE_SECRET || '',
|
|
|
|
|
},
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
body: JSON.stringify({ style: selectedStyle || undefined }),
|
|
|
|
|
});
|
|
|
|
|
const d = await r.json();
|
|
|
|
@@ -72,9 +59,7 @@ function DraftCard({ draft, onApproved, onCoverRegenerated }) {
|
|
|
|
|
setCoverUrl(d.cover_url);
|
|
|
|
|
setMsg('🎨 Обложка обновлена');
|
|
|
|
|
onCoverRegenerated(draft.id, d.cover_url);
|
|
|
|
|
} else {
|
|
|
|
|
setMsg('❌ ' + (d.error || 'Ошибка генерации'));
|
|
|
|
|
}
|
|
|
|
|
} else setMsg('❌ ' + (d.error || 'Ошибка'));
|
|
|
|
|
} catch { setMsg('❌ Ошибка сети'); }
|
|
|
|
|
setRegenLoading(false);
|
|
|
|
|
}
|
|
|
|
@@ -82,12 +67,9 @@ function DraftCard({ draft, onApproved, onCoverRegenerated }) {
|
|
|
|
|
async function saveEdit() {
|
|
|
|
|
setSaveLoading(true);
|
|
|
|
|
try {
|
|
|
|
|
const r = await fetch(`${engineUrl}/api/drafts/${draft.id}`, {
|
|
|
|
|
const r = await fetch(`/api/admin/drafts/${draft.id}`, {
|
|
|
|
|
method: 'PATCH',
|
|
|
|
|
headers: {
|
|
|
|
|
'Content-Type': 'application/json',
|
|
|
|
|
'x-internal-secret': process.env.NEXT_PUBLIC_ENGINE_SECRET || '',
|
|
|
|
|
},
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
body: JSON.stringify({ title: editTitle, excerpt: editExcerpt }),
|
|
|
|
|
});
|
|
|
|
|
const d = await r.json();
|
|
|
|
@@ -98,14 +80,13 @@ function DraftCard({ draft, onApproved, onCoverRegenerated }) {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const coverSrc = coverUrl
|
|
|
|
|
? (coverUrl.startsWith('http') ? coverUrl : `${engineUrl}${coverUrl}`)
|
|
|
|
|
? (coverUrl.startsWith('http') ? coverUrl : `https://zeropost.ru${coverUrl}`)
|
|
|
|
|
: null;
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="bg-white dark:bg-neutral-900 rounded-xl border border-neutral-200 dark:border-neutral-800 overflow-hidden">
|
|
|
|
|
{/* Обложка + основная инфо */}
|
|
|
|
|
<div className="flex gap-4 p-4">
|
|
|
|
|
<div className="w-32 h-20 rounded-lg overflow-hidden bg-neutral-100 dark:bg-neutral-800 shrink-0 relative">
|
|
|
|
|
<div className="w-32 h-20 rounded-lg overflow-hidden bg-neutral-100 dark:bg-neutral-800 shrink-0">
|
|
|
|
|
{coverSrc
|
|
|
|
|
? <img src={coverSrc} alt="" className="w-full h-full object-cover" />
|
|
|
|
|
: <div className="w-full h-full flex items-center justify-center text-neutral-400"><ImageIcon className="w-6 h-6" /></div>
|
|
|
|
@@ -121,65 +102,40 @@ function DraftCard({ draft, onApproved, onCoverRegenerated }) {
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
{editing ? (
|
|
|
|
|
<input
|
|
|
|
|
value={editTitle}
|
|
|
|
|
onChange={e => setEditTitle(e.target.value)}
|
|
|
|
|
className="w-full font-semibold text-sm border border-neutral-300 dark:border-neutral-700 rounded px-2 py-1 bg-transparent mb-1"
|
|
|
|
|
/>
|
|
|
|
|
<input value={editTitle} onChange={e => setEditTitle(e.target.value)}
|
|
|
|
|
className="w-full font-semibold text-sm border border-neutral-300 dark:border-neutral-700 rounded px-2 py-1 bg-transparent mb-1" />
|
|
|
|
|
) : (
|
|
|
|
|
<div className="font-semibold text-sm text-neutral-900 dark:text-neutral-100 line-clamp-2 leading-snug">
|
|
|
|
|
{editTitle}
|
|
|
|
|
</div>
|
|
|
|
|
<div className="font-semibold text-sm text-neutral-900 dark:text-neutral-100 line-clamp-2 leading-snug">{editTitle}</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Анонс */}
|
|
|
|
|
<div className="px-4 pb-3">
|
|
|
|
|
{editing ? (
|
|
|
|
|
<textarea
|
|
|
|
|
value={editExcerpt}
|
|
|
|
|
onChange={e => setEditExcerpt(e.target.value)}
|
|
|
|
|
rows={3}
|
|
|
|
|
className="w-full text-xs border border-neutral-300 dark:border-neutral-700 rounded px-2 py-1.5 bg-transparent resize-none"
|
|
|
|
|
/>
|
|
|
|
|
<textarea value={editExcerpt} onChange={e => setEditExcerpt(e.target.value)} rows={3}
|
|
|
|
|
className="w-full text-xs border border-neutral-300 dark:border-neutral-700 rounded px-2 py-1.5 bg-transparent resize-none" />
|
|
|
|
|
) : (
|
|
|
|
|
<p className="text-xs text-neutral-500 line-clamp-3 leading-relaxed">{draft.excerpt}</p>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Сообщение */}
|
|
|
|
|
{msg && (
|
|
|
|
|
<div className="px-4 pb-2 text-xs font-medium text-neutral-700 dark:text-neutral-300">{msg}</div>
|
|
|
|
|
)}
|
|
|
|
|
{msg && <div className="px-4 pb-2 text-xs font-medium text-neutral-700 dark:text-neutral-300">{msg}</div>}
|
|
|
|
|
|
|
|
|
|
{/* Перегенерация обложки — разворачиваемый блок */}
|
|
|
|
|
<div className="border-t border-neutral-100 dark:border-neutral-800">
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => setExpanded(v => !v)}
|
|
|
|
|
className="w-full flex items-center justify-between px-4 py-2.5 text-xs text-neutral-500 hover:bg-neutral-50 dark:hover:bg-neutral-800 transition-colors"
|
|
|
|
|
>
|
|
|
|
|
<button onClick={() => setExpanded(v => !v)}
|
|
|
|
|
className="w-full flex items-center justify-between px-4 py-2.5 text-xs text-neutral-500 hover:bg-neutral-50 dark:hover:bg-neutral-800 transition-colors">
|
|
|
|
|
<span className="flex items-center gap-1.5"><ImageIcon className="w-3.5 h-3.5" /> Перегенерировать обложку</span>
|
|
|
|
|
{expanded ? <ChevronUp className="w-3.5 h-3.5" /> : <ChevronDown className="w-3.5 h-3.5" />}
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
{expanded && (
|
|
|
|
|
<div className="px-4 pb-3 space-y-2">
|
|
|
|
|
<select
|
|
|
|
|
value={selectedStyle}
|
|
|
|
|
onChange={e => setSelectedStyle(e.target.value)}
|
|
|
|
|
className="w-full text-xs border border-neutral-200 dark:border-neutral-700 rounded-lg px-2 py-1.5 bg-white dark:bg-neutral-800"
|
|
|
|
|
>
|
|
|
|
|
<option value="">— Авто (по рубрике AI)</option>
|
|
|
|
|
{COVER_STYLES.map(s => (
|
|
|
|
|
<option key={s.id} value={s.id}>{s.name}</option>
|
|
|
|
|
))}
|
|
|
|
|
<select value={selectedStyle} onChange={e => setSelectedStyle(e.target.value)}
|
|
|
|
|
className="w-full text-xs border border-neutral-200 dark:border-neutral-700 rounded-lg px-2 py-1.5 bg-white dark:bg-neutral-800">
|
|
|
|
|
<option value="">— Авто (AI выберет стиль)</option>
|
|
|
|
|
{COVER_STYLES.map(s => <option key={s.id} value={s.id}>{s.name}</option>)}
|
|
|
|
|
</select>
|
|
|
|
|
<button
|
|
|
|
|
onClick={regenCover}
|
|
|
|
|
disabled={regenLoading}
|
|
|
|
|
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-neutral-900 dark:bg-neutral-100 text-white dark:text-neutral-900 text-xs font-medium hover:opacity-80 disabled:opacity-50 transition"
|
|
|
|
|
>
|
|
|
|
|
<button onClick={regenCover} disabled={regenLoading}
|
|
|
|
|
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-neutral-900 dark:bg-neutral-100 text-white dark:text-neutral-900 text-xs font-medium hover:opacity-80 disabled:opacity-50 transition">
|
|
|
|
|
<RefreshCw className={`w-3.5 h-3.5 ${regenLoading ? 'animate-spin' : ''}`} />
|
|
|
|
|
{regenLoading ? 'Генерирую...' : 'Перегенерировать'}
|
|
|
|
|
</button>
|
|
|
|
@@ -187,39 +143,26 @@ function DraftCard({ draft, onApproved, onCoverRegenerated }) {
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Кнопки действий */}
|
|
|
|
|
<div className="border-t border-neutral-100 dark:border-neutral-800 px-4 py-3 flex items-center gap-2">
|
|
|
|
|
{editing ? (
|
|
|
|
|
<>
|
|
|
|
|
<button
|
|
|
|
|
onClick={saveEdit}
|
|
|
|
|
disabled={saveLoading}
|
|
|
|
|
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-blue-600 text-white text-xs font-medium hover:bg-blue-700 disabled:opacity-50 transition"
|
|
|
|
|
>
|
|
|
|
|
<Save className="w-3.5 h-3.5" />
|
|
|
|
|
{saveLoading ? 'Сохраняю...' : 'Сохранить'}
|
|
|
|
|
<button onClick={saveEdit} disabled={saveLoading}
|
|
|
|
|
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-blue-600 text-white text-xs font-medium hover:bg-blue-700 disabled:opacity-50 transition">
|
|
|
|
|
<Save className="w-3.5 h-3.5" />{saveLoading ? 'Сохраняю...' : 'Сохранить'}
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => { setEditing(false); setEditTitle(draft.title); setEditExcerpt(draft.excerpt || ''); }}
|
|
|
|
|
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-neutral-100 dark:bg-neutral-800 text-xs text-neutral-600 dark:text-neutral-300 hover:opacity-80 transition"
|
|
|
|
|
>
|
|
|
|
|
<button onClick={() => { setEditing(false); setEditTitle(draft.title); setEditExcerpt(draft.excerpt || ''); }}
|
|
|
|
|
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-neutral-100 dark:bg-neutral-800 text-xs text-neutral-600 dark:text-neutral-300 hover:opacity-80 transition">
|
|
|
|
|
<X className="w-3.5 h-3.5" /> Отмена
|
|
|
|
|
</button>
|
|
|
|
|
</>
|
|
|
|
|
) : (
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => setEditing(true)}
|
|
|
|
|
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-neutral-100 dark:bg-neutral-800 text-xs text-neutral-600 dark:text-neutral-300 hover:opacity-80 transition"
|
|
|
|
|
>
|
|
|
|
|
<button onClick={() => setEditing(true)}
|
|
|
|
|
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-neutral-100 dark:bg-neutral-800 text-xs text-neutral-600 dark:text-neutral-300 hover:opacity-80 transition">
|
|
|
|
|
<Pencil className="w-3.5 h-3.5" /> Редактировать
|
|
|
|
|
</button>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
<button
|
|
|
|
|
onClick={approve}
|
|
|
|
|
disabled={loading}
|
|
|
|
|
className="ml-auto flex items-center gap-1.5 px-4 py-1.5 rounded-lg bg-emerald-500 hover:bg-emerald-600 text-white text-xs font-semibold disabled:opacity-50 transition"
|
|
|
|
|
>
|
|
|
|
|
<button onClick={approve} disabled={loading}
|
|
|
|
|
className="ml-auto flex items-center gap-1.5 px-4 py-1.5 rounded-lg bg-emerald-500 hover:bg-emerald-600 text-white text-xs font-semibold disabled:opacity-50 transition">
|
|
|
|
|
<CheckCircle className="w-3.5 h-3.5" />
|
|
|
|
|
{loading ? 'Одобряю...' : 'Одобрить и запланировать'}
|
|
|
|
|
</button>
|
|
|
|
@@ -229,46 +172,27 @@ function DraftCard({ draft, onApproved, onCoverRegenerated }) {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default function AdminDraftsPage() {
|
|
|
|
|
const [drafts, setDrafts] = useState([]);
|
|
|
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
|
const [drafts, setDrafts] = useState([]);
|
|
|
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
|
const [approveAllLoading, setApproveAllLoading] = useState(false);
|
|
|
|
|
const [msg, setMsg] = useState('');
|
|
|
|
|
|
|
|
|
|
const engineUrl = typeof window !== 'undefined'
|
|
|
|
|
? (process.env.NEXT_PUBLIC_ENGINE_URL || '')
|
|
|
|
|
: '';
|
|
|
|
|
const [msg, setMsg] = useState('');
|
|
|
|
|
|
|
|
|
|
const load = useCallback(async () => {
|
|
|
|
|
setLoading(true);
|
|
|
|
|
try {
|
|
|
|
|
const r = await fetch(`${engineUrl}/api/drafts`, {
|
|
|
|
|
headers: { 'x-internal-secret': process.env.NEXT_PUBLIC_ENGINE_SECRET || '' },
|
|
|
|
|
cache: 'no-store',
|
|
|
|
|
});
|
|
|
|
|
const r = await fetch('/api/admin/drafts');
|
|
|
|
|
const d = await r.json();
|
|
|
|
|
setDrafts(Array.isArray(d) ? d : []);
|
|
|
|
|
} catch { setDrafts([]); }
|
|
|
|
|
setLoading(false);
|
|
|
|
|
}, [engineUrl]);
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
useEffect(() => { load(); }, [load]);
|
|
|
|
|
|
|
|
|
|
function handleApproved(id) {
|
|
|
|
|
setDrafts(prev => prev.filter(d => d.id !== id));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handleCoverRegenerated(id, coverUrl) {
|
|
|
|
|
setDrafts(prev => prev.map(d => d.id === id ? { ...d, cover_url: coverUrl } : d));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function approveAll() {
|
|
|
|
|
setApproveAllLoading(true);
|
|
|
|
|
setMsg('');
|
|
|
|
|
setApproveAllLoading(true); setMsg('');
|
|
|
|
|
try {
|
|
|
|
|
const r = await fetch(`${engineUrl}/api/drafts/approve-all`, {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: { 'x-internal-secret': process.env.NEXT_PUBLIC_ENGINE_SECRET || '' },
|
|
|
|
|
});
|
|
|
|
|
const r = await fetch('/api/admin/drafts/approve-all', { method: 'POST' });
|
|
|
|
|
const d = await r.json();
|
|
|
|
|
if (d.ok) { setMsg('✅ Все черновики одобрены'); load(); }
|
|
|
|
|
else setMsg('❌ ' + (d.error || 'Ошибка'));
|
|
|
|
@@ -286,18 +210,12 @@ export default function AdminDraftsPage() {
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
<button
|
|
|
|
|
onClick={load}
|
|
|
|
|
className="p-2 rounded-lg hover:bg-neutral-100 dark:hover:bg-neutral-800 transition"
|
|
|
|
|
>
|
|
|
|
|
<button onClick={load} className="p-2 rounded-lg hover:bg-neutral-100 dark:hover:bg-neutral-800 transition">
|
|
|
|
|
<RefreshCw className="w-4 h-4 text-neutral-500" />
|
|
|
|
|
</button>
|
|
|
|
|
{drafts.length > 0 && (
|
|
|
|
|
<button
|
|
|
|
|
onClick={approveAll}
|
|
|
|
|
disabled={approveAllLoading}
|
|
|
|
|
className="flex items-center gap-1.5 px-4 py-2 rounded-lg bg-emerald-500 hover:bg-emerald-600 text-white text-sm font-medium disabled:opacity-50 transition"
|
|
|
|
|
>
|
|
|
|
|
<button onClick={approveAll} disabled={approveAllLoading}
|
|
|
|
|
className="flex items-center gap-1.5 px-4 py-2 rounded-lg bg-emerald-500 hover:bg-emerald-600 text-white text-sm font-medium disabled:opacity-50 transition">
|
|
|
|
|
<CheckCircle className="w-4 h-4" />
|
|
|
|
|
{approveAllLoading ? 'Одобряю...' : `Одобрить все (${drafts.length})`}
|
|
|
|
|
</button>
|
|
|
|
@@ -305,11 +223,7 @@ export default function AdminDraftsPage() {
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{msg && (
|
|
|
|
|
<div className="text-sm font-medium text-neutral-700 dark:text-neutral-300 bg-neutral-50 dark:bg-neutral-800 rounded-lg px-4 py-2.5">
|
|
|
|
|
{msg}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
{msg && <div className="text-sm font-medium bg-neutral-50 dark:bg-neutral-800 rounded-lg px-4 py-2.5">{msg}</div>}
|
|
|
|
|
|
|
|
|
|
{loading ? (
|
|
|
|
|
<div className="text-center py-20 text-neutral-400">Загружаю черновики...</div>
|
|
|
|
@@ -320,14 +234,11 @@ export default function AdminDraftsPage() {
|
|
|
|
|
<div className="text-sm text-neutral-400 mt-1">Следующие появятся после autogen</div>
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-2">
|
|
|
|
|
<div className="grid gap-4 sm:grid-cols-2">
|
|
|
|
|
{drafts.map(draft => (
|
|
|
|
|
<DraftCard
|
|
|
|
|
key={draft.id}
|
|
|
|
|
draft={draft}
|
|
|
|
|
onApproved={handleApproved}
|
|
|
|
|
onCoverRegenerated={handleCoverRegenerated}
|
|
|
|
|
/>
|
|
|
|
|
<DraftCard key={draft.id} draft={draft}
|
|
|
|
|
onApproved={id => setDrafts(prev => prev.filter(d => d.id !== id))}
|
|
|
|
|
onCoverRegenerated={(id, url) => setDrafts(prev => prev.map(d => d.id === id ? {...d, cover_url: url} : d))} />
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|