forked from admin/zeropost-tool
feat: P3 PostTemplates — 7 post structure presets in ChannelView
This commit is contained in:
+22
-10
@@ -8,6 +8,7 @@ import {
|
|||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import PhotoSearchModal from './PhotoSearchModal';
|
import PhotoSearchModal from './PhotoSearchModal';
|
||||||
import PostPreview from './PostPreview';
|
import PostPreview from './PostPreview';
|
||||||
|
import PostTemplates from './PostTemplates';
|
||||||
|
|
||||||
const GOAL_LABELS = {
|
const GOAL_LABELS = {
|
||||||
educational: 'Обучение', news: 'Новости',
|
educational: 'Обучение', news: 'Новости',
|
||||||
@@ -191,6 +192,14 @@ export default function ChannelView({ channel }) {
|
|||||||
setTopic('');
|
setTopic('');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function applyTemplate({ topicHint, structure }) {
|
||||||
|
// Если тема не задана — подставляем подсказку
|
||||||
|
if (!topic.trim()) setTopic(topicHint);
|
||||||
|
// В textarea вставляем структуру как отправную точку
|
||||||
|
setPost(structure);
|
||||||
|
setSavedPostId(null);
|
||||||
|
}
|
||||||
|
|
||||||
async function generate(asVariant = false) {
|
async function generate(asVariant = false) {
|
||||||
if (!topic.trim() && !asVariant) return;
|
if (!topic.trim() && !asVariant) return;
|
||||||
if (asVariant && !post) return;
|
if (asVariant && !post) return;
|
||||||
@@ -328,14 +337,17 @@ export default function ChannelView({ channel }) {
|
|||||||
<Wand2 className="w-4 h-4 text-accent" />
|
<Wand2 className="w-4 h-4 text-accent" />
|
||||||
Сгенерировать пост
|
Сгенерировать пост
|
||||||
</h2>
|
</h2>
|
||||||
<button
|
<div className="flex items-center gap-3">
|
||||||
onClick={fetchIdeas}
|
<PostTemplates onSelect={applyTemplate} disabled={generating} />
|
||||||
disabled={loadingIdeas}
|
<button
|
||||||
className="text-xs inline-flex items-center gap-1 text-accent hover:underline disabled:opacity-50"
|
onClick={fetchIdeas}
|
||||||
>
|
disabled={loadingIdeas}
|
||||||
{loadingIdeas ? <Loader2 className="w-3.5 h-3.5 animate-spin" /> : <Sparkles className="w-3.5 h-3.5" />}
|
className="text-xs inline-flex items-center gap-1 text-accent hover:underline disabled:opacity-50"
|
||||||
Идеи тем
|
>
|
||||||
</button>
|
{loadingIdeas ? <Loader2 className="w-3.5 h-3.5 animate-spin" /> : <Sparkles className="w-3.5 h-3.5" />}
|
||||||
|
Идеи тем
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{showIdeas && ideas.length > 0 && (
|
{showIdeas && ideas.length > 0 && (
|
||||||
@@ -588,7 +600,7 @@ export default function ChannelView({ channel }) {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>{/* конец левой колонки */}
|
</div>
|
||||||
{/* Правая колонка — превью */}
|
{/* Правая колонка — превью */}
|
||||||
<div className="card p-4 sticky top-20">
|
<div className="card p-4 sticky top-20">
|
||||||
<PostPreview
|
<PostPreview
|
||||||
@@ -598,7 +610,7 @@ export default function ChannelView({ channel }) {
|
|||||||
channelName={channel.name}
|
channelName={channel.name}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>{/* конец грида */}
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Photo search modal */}
|
{/* Photo search modal */}
|
||||||
|
|||||||
@@ -0,0 +1,170 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PostTemplates — 7 кнопок-пресетов структуры поста.
|
||||||
|
* Props:
|
||||||
|
* onSelect(template) — вызывается с объектом { label, topic, structure }
|
||||||
|
* disabled — bool
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { ChevronDown, ChevronUp, Newspaper, Megaphone,
|
||||||
|
Briefcase, BookOpen, List, HelpCircle, User } from 'lucide-react';
|
||||||
|
|
||||||
|
const TEMPLATES = [
|
||||||
|
{
|
||||||
|
id: 'news',
|
||||||
|
label: 'Новость',
|
||||||
|
Icon: Newspaper,
|
||||||
|
hint: 'Факт → контекст → вывод',
|
||||||
|
topicHint: 'Новость или событие в нише',
|
||||||
|
structure: `[ЗАГОЛОВОК — суть в одной строке]
|
||||||
|
|
||||||
|
[2–3 предложения: что произошло, ключевые цифры]
|
||||||
|
|
||||||
|
[Почему это важно для читателя]
|
||||||
|
|
||||||
|
[Личный вывод или вопрос к аудитории]`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'announce',
|
||||||
|
label: 'Анонс',
|
||||||
|
Icon: Megaphone,
|
||||||
|
hint: 'Интрига → суть → CTA',
|
||||||
|
topicHint: 'Анонс события, релиза, запуска',
|
||||||
|
structure: `[Интригующий первый абзац — зачем читать дальше]
|
||||||
|
|
||||||
|
📅 [Дата и что именно происходит]
|
||||||
|
|
||||||
|
✅ [3–4 буллита: что будет / что получит читатель]
|
||||||
|
|
||||||
|
👉 [Призыв к действию со ссылкой]`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'case',
|
||||||
|
label: 'Кейс',
|
||||||
|
Icon: Briefcase,
|
||||||
|
hint: 'Ситуация → решение → результат',
|
||||||
|
topicHint: 'Реальный пример из практики',
|
||||||
|
structure: `[Задача: что было за проблема]
|
||||||
|
|
||||||
|
[Что попробовал, что не сработало]
|
||||||
|
|
||||||
|
[Что сработало — конкретные шаги]
|
||||||
|
|
||||||
|
📊 Результат: [цифры или конкретный итог]
|
||||||
|
|
||||||
|
[Вывод — что можно повторить]`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'longread',
|
||||||
|
label: 'Лонгрид',
|
||||||
|
Icon: BookOpen,
|
||||||
|
hint: 'Глубокий разбор темы',
|
||||||
|
topicHint: 'Тема для развёрнутого объяснения',
|
||||||
|
structure: `[Провокационный или неожиданный тезис]
|
||||||
|
|
||||||
|
[Почему стандартный взгляд ошибается]
|
||||||
|
|
||||||
|
[Аргумент 1 + пример]
|
||||||
|
|
||||||
|
[Аргумент 2 + пример]
|
||||||
|
|
||||||
|
[Аргумент 3 + пример]
|
||||||
|
|
||||||
|
[Заключение: к чему приходим]`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'list',
|
||||||
|
label: 'Подборка',
|
||||||
|
Icon: List,
|
||||||
|
hint: 'N полезных штук',
|
||||||
|
topicHint: 'Список инструментов, советов, ресурсов',
|
||||||
|
structure: `[Почему эта подборка полезна]
|
||||||
|
|
||||||
|
1. [Название] — [1 предложение почему]
|
||||||
|
2. [Название] — [1 предложение почему]
|
||||||
|
3. [Название] — [1 предложение почему]
|
||||||
|
4. [Название] — [1 предложение почему]
|
||||||
|
5. [Название] — [1 предложение почему]
|
||||||
|
|
||||||
|
[Итог или личная рекомендация #1]`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'poll',
|
||||||
|
label: 'Опрос-разбор',
|
||||||
|
Icon: HelpCircle,
|
||||||
|
hint: 'Вопрос → варианты → разбор',
|
||||||
|
topicHint: 'Дискуссионный вопрос для аудитории',
|
||||||
|
structure: `[Провокационный вопрос к читателю]
|
||||||
|
|
||||||
|
Как бы ты поступил?
|
||||||
|
|
||||||
|
А) [Вариант 1]
|
||||||
|
Б) [Вариант 2]
|
||||||
|
В) [Вариант 3]
|
||||||
|
|
||||||
|
[Мой ответ и почему именно так]
|
||||||
|
|
||||||
|
[Приглашение высказаться в комментариях]`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'personal',
|
||||||
|
label: 'Личное',
|
||||||
|
Icon: User,
|
||||||
|
hint: 'История → урок → применение',
|
||||||
|
topicHint: 'Личный опыт или наблюдение',
|
||||||
|
structure: `[Конкретная ситуация из жизни — детали, дата, место]
|
||||||
|
|
||||||
|
[Что почувствовал / что понял в тот момент]
|
||||||
|
|
||||||
|
[Урок, который из этого вынес]
|
||||||
|
|
||||||
|
[Как это меняет то, что я делаю сейчас]
|
||||||
|
|
||||||
|
[Вопрос читателю — было ли у него похожее?]`,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function PostTemplates({ onSelect, disabled }) {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
function pick(tpl) {
|
||||||
|
onSelect({ label: tpl.label, topicHint: tpl.topicHint, structure: tpl.structure });
|
||||||
|
setOpen(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative">
|
||||||
|
<button
|
||||||
|
onClick={() => setOpen(v => !v)}
|
||||||
|
disabled={disabled}
|
||||||
|
className="text-xs inline-flex items-center gap-1 text-accent hover:underline disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{open ? <ChevronUp className="w-3.5 h-3.5" /> : <ChevronDown className="w-3.5 h-3.5" />}
|
||||||
|
Шаблоны
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{open && (
|
||||||
|
<div className="absolute left-0 top-6 z-20 w-72 rounded-xl border border-border bg-surface shadow-xl p-2">
|
||||||
|
<div className="text-xs font-semibold text-text-mute uppercase tracking-wide px-2 py-1 mb-1">
|
||||||
|
Выбери структуру поста
|
||||||
|
</div>
|
||||||
|
{TEMPLATES.map(tpl => (
|
||||||
|
<button
|
||||||
|
key={tpl.id}
|
||||||
|
onClick={() => pick(tpl)}
|
||||||
|
className="w-full flex items-start gap-2.5 px-2.5 py-2 rounded-lg hover:bg-surface2 text-left transition-colors"
|
||||||
|
>
|
||||||
|
<tpl.Icon className="w-4 h-4 text-accent mt-0.5 shrink-0" />
|
||||||
|
<div>
|
||||||
|
<div className="text-sm font-medium text-text">{tpl.label}</div>
|
||||||
|
<div className="text-xs text-text-mute">{tpl.hint}</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user