Files
zeropost-tool/components/PollModal.js
T
Ник (Claude) d0fd328011 feat: PollModal + HashtagSuggest UI
PollModal:
- Вопрос + 2-10 вариантов ответа
- Типы: обычный / викторина (с правильным ответом и объяснением)
- Настройки: анонимность, несколько ответов
- Отложенная публикация через datetime-local
- Кнопка «📊 Опрос» в ChannelView (только для TG каналов)

HashtagSuggest:
- Появляется под сгенерированным постом
- Запрос в /api/generate/hashtags → Claude Haiku
- Клик по тегу — выбор, кнопка «Добавить в пост»
- Обновление тегов, закрытие панели
2026-06-11 19:55:24 +03:00

176 lines
7.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client';
import { useState } from 'react';
import { X, Plus, Trash2, Loader2, ChevronDown } from 'lucide-react';
export default function PollModal({ channel, onClose, onPublished }) {
const [question, setQuestion] = useState('');
const [options, setOptions] = useState(['', '']);
const [isAnonymous, setAnonymous] = useState(true);
const [isMultiple, setMultiple] = useState(false);
const [type, setType] = useState('regular');
const [correctId, setCorrectId] = useState(0);
const [explanation, setExplanation] = useState('');
const [scheduleAt, setScheduleAt] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
function addOption() { if (options.length < 10) setOptions([...options, '']); }
function removeOption(i) { if (options.length > 2) setOptions(options.filter((_, idx) => idx !== i)); }
function setOption(i, val) { setOptions(options.map((o, idx) => idx === i ? val : o)); }
async function submit() {
if (!question.trim()) return setError('Введите вопрос');
if (options.some(o => !o.trim())) return setError('Заполните все варианты ответа');
setLoading(true);
setError('');
try {
const res = await fetch(`/api/channels/${channel.id}/poll`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
question: question.trim(),
options: options.map(o => o.trim()),
is_anonymous: isAnonymous,
allows_multiple_answers: isMultiple,
type,
correct_option_id: type === 'quiz' ? correctId : undefined,
explanation: type === 'quiz' ? explanation : undefined,
schedule_at: scheduleAt || null,
}),
}).then(r => r.json());
if (res.error) throw new Error(res.error);
onPublished?.(res);
onClose();
} catch (err) {
setError(err.message);
}
setLoading(false);
}
return (
<div className="fixed inset-0 bg-black/60 backdrop-blur-sm z-50 flex items-center justify-center p-4">
<div className="bg-surface w-full max-w-lg rounded-2xl shadow-2xl border border-border flex flex-col max-h-[90vh]">
{/* Header */}
<div className="flex items-center justify-between px-5 py-4 border-b border-border shrink-0">
<h2 className="font-semibold">Создать опрос</h2>
<button onClick={onClose} className="btn-ghost p-1.5"><X className="w-4 h-4" /></button>
</div>
{/* Body */}
<div className="overflow-y-auto flex-1 px-5 py-4 space-y-4">
{/* Вопрос */}
<div>
<label className="label mb-1.5">Вопрос <span className="text-gray-500 text-xs">({question.length}/300)</span></label>
<textarea
rows={2}
maxLength={300}
value={question}
onChange={e => setQuestion(e.target.value)}
className="input w-full resize-none"
placeholder="Что думаете о..."
/>
</div>
{/* Тип */}
<div className="flex gap-2">
{[['regular', '📊 Обычный'], ['quiz', '🎯 Викторина']].map(([v, l]) => (
<button key={v} type="button" onClick={() => setType(v)}
className={`flex-1 py-2 rounded-lg text-sm font-medium transition-colors border ${
type === v ? 'border-accent bg-accent/10 text-accent' : 'border-border hover:border-accent/40'
}`}>
{l}
</button>
))}
</div>
{/* Варианты */}
<div>
<label className="label mb-1.5">Варианты ответа</label>
<div className="space-y-2">
{options.map((opt, i) => (
<div key={i} className="flex items-center gap-2">
{type === 'quiz' && (
<button type="button" onClick={() => setCorrectId(i)}
className={`w-5 h-5 rounded-full border-2 shrink-0 transition-colors ${
correctId === i ? 'border-green-500 bg-green-500' : 'border-gray-500'
}`} title="Правильный ответ" />
)}
<input
value={opt}
onChange={e => setOption(i, e.target.value)}
maxLength={100}
className="input flex-1 py-1.5 text-sm"
placeholder={`Вариант ${i + 1}`}
/>
{options.length > 2 && (
<button onClick={() => removeOption(i)} className="btn-ghost p-1.5 text-gray-500">
<Trash2 className="w-3.5 h-3.5" />
</button>
)}
</div>
))}
</div>
{options.length < 10 && (
<button onClick={addOption} className="mt-2 text-sm text-accent hover:text-accent/80 flex items-center gap-1">
<Plus className="w-3.5 h-3.5" /> Добавить вариант
</button>
)}
</div>
{/* Объяснение для викторины */}
{type === 'quiz' && (
<div>
<label className="label mb-1.5">Объяснение <span className="text-gray-500 text-xs">(показывается после ответа)</span></label>
<input
value={explanation}
onChange={e => setExplanation(e.target.value)}
maxLength={200}
className="input w-full text-sm"
placeholder="Правильный ответ, потому что..."
/>
</div>
)}
{/* Настройки */}
<div className="space-y-2.5">
<label className="label">Настройки</label>
<label className="flex items-center gap-2.5 cursor-pointer">
<input type="checkbox" checked={isAnonymous} onChange={e => setAnonymous(e.target.checked)}
className="w-4 h-4 rounded accent-accent" />
<span className="text-sm">Анонимное голосование</span>
</label>
{type === 'regular' && (
<label className="flex items-center gap-2.5 cursor-pointer">
<input type="checkbox" checked={isMultiple} onChange={e => setMultiple(e.target.checked)}
className="w-4 h-4 rounded accent-accent" />
<span className="text-sm">Несколько ответов</span>
</label>
)}
</div>
{/* Отложенная публикация */}
<div>
<label className="label mb-1.5">Запланировать <span className="text-gray-500 text-xs">(оставьте пустым чтобы опубликовать сейчас)</span></label>
<input
type="datetime-local"
value={scheduleAt}
onChange={e => setScheduleAt(e.target.value)}
className="input w-full text-sm"
/>
</div>
{error && <p className="text-red-400 text-sm">{error}</p>}
</div>
{/* Footer */}
<div className="px-5 py-4 border-t border-border flex justify-end gap-2 shrink-0">
<button onClick={onClose} className="btn-ghost px-4">Отмена</button>
<button onClick={submit} disabled={loading} className="btn-primary px-5">
{loading ? <Loader2 className="w-4 h-4 animate-spin" /> : scheduleAt ? '📅 Запланировать' : '📊 Опубликовать'}
</button>
</div>
</div>
</div>
);
}