diff --git a/app/admin/(protected)/drafts/page.js b/app/admin/(protected)/drafts/page.js new file mode 100644 index 0000000..27fab42 --- /dev/null +++ b/app/admin/(protected)/drafts/page.js @@ -0,0 +1,337 @@ +'use client'; +import { useState, useEffect, useCallback } from 'react'; +import { CheckCircle, RefreshCw, Pencil, Clock, ImageIcon, ChevronDown, ChevronUp, X, Save } from 'lucide-react'; + +export const metadata = { title: 'Черновики' }; + +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 — кинематографичный' }, +]; + +const CATEGORY_LABELS = { + 'ai-tools': '🤖 AI Tools', + 'cybersec': '🔒 Cybersec', + 'automation': '⚡ Automation', + 'ai-dev': '💻 AI Dev', +}; + +function DraftCard({ draft, onApproved, onCoverRegenerated }) { + const [loading, setLoading] = useState(false); + const [regenLoading, setRegenLoading] = useState(false); + const [expanded, setExpanded] = useState(false); + const [editing, setEditing] = useState(false); + const [editTitle, setEditTitle] = useState(draft.title); + const [editExcerpt, setEditExcerpt] = useState(draft.excerpt || ''); + const [saveLoading, setSaveLoading] = useState(false); + const [selectedStyle, setSelectedStyle] = useState(''); + 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(''); + try { + const r = await fetch(`${engineUrl}/api/drafts/${draft.id}/approve`, { + method: 'PATCH', + headers: { 'x-internal-secret': process.env.NEXT_PUBLIC_ENGINE_SECRET || '' }, + }); + const d = await r.json(); + if (d.ok) { + const slot = d.scheduled_at + ? new Date(d.scheduled_at).toLocaleTimeString('ru', { hour: '2-digit', minute: '2-digit' }) + : null; + setMsg(slot ? `✅ Одобрено, выйдет в ${slot}` : '✅ Одобрено'); + setTimeout(() => onApproved(draft.id), 1200); + } else { + setMsg('❌ ' + (d.error || 'Ошибка')); + } + } catch { setMsg('❌ Ошибка сети'); } + setLoading(false); + } + + async function regenCover() { + setRegenLoading(true); + setMsg(''); + try { + const r = await fetch(`${engineUrl}/api/drafts/${draft.id}/regenerate-cover`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'x-internal-secret': process.env.NEXT_PUBLIC_ENGINE_SECRET || '', + }, + body: JSON.stringify({ style: selectedStyle || undefined }), + }); + const d = await r.json(); + if (d.ok && d.cover_url) { + setCoverUrl(d.cover_url); + setMsg('🎨 Обложка обновлена'); + onCoverRegenerated(draft.id, d.cover_url); + } else { + setMsg('❌ ' + (d.error || 'Ошибка генерации')); + } + } catch { setMsg('❌ Ошибка сети'); } + setRegenLoading(false); + } + + async function saveEdit() { + setSaveLoading(true); + try { + const r = await fetch(`${engineUrl}/api/drafts/${draft.id}`, { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json', + 'x-internal-secret': process.env.NEXT_PUBLIC_ENGINE_SECRET || '', + }, + body: JSON.stringify({ title: editTitle, excerpt: editExcerpt }), + }); + const d = await r.json(); + if (d.ok) { setEditing(false); setMsg('💾 Сохранено'); } + else setMsg('❌ ' + (d.error || 'Ошибка')); + } catch { setMsg('❌ Ошибка сети'); } + setSaveLoading(false); + } + + const coverSrc = coverUrl + ? (coverUrl.startsWith('http') ? coverUrl : `${engineUrl}${coverUrl}`) + : null; + + return ( +
+ {/* Обложка + основная инфо */} +
+
+ {coverSrc + ? + :
+ } +
+
+
+ + {CATEGORY_LABELS[draft.category] || draft.category} + + + {draft.reading_time} мин + +
+ {editing ? ( + 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" + /> + ) : ( +
+ {editTitle} +
+ )} +
+
+ + {/* Анонс */} +
+ {editing ? ( +