diff --git a/app/api/channels/[id]/poll/route.js b/app/api/channels/[id]/poll/route.js new file mode 100644 index 0000000..299cdd2 --- /dev/null +++ b/app/api/channels/[id]/poll/route.js @@ -0,0 +1,26 @@ +import { NextResponse } from 'next/server'; +import { requireUser } from '@/lib/session'; + +const ENGINE_URL = process.env.ENGINE_URL || 'http://localhost:3030'; +const ENGINE_SECRET = process.env.ENGINE_SECRET || ''; + +export async function POST(req, { params }) { + const user = await requireUser(); + if (!user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + const body = await req.json(); + try { + const res = await fetch(`${ENGINE_URL}/api/channels/${params.id}/poll`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'x-internal-secret': ENGINE_SECRET, + 'x-user-id': String(user.id), + }, + body: JSON.stringify(body), + }); + const data = await res.json(); + return NextResponse.json(data, { status: res.status }); + } catch (err) { + return NextResponse.json({ error: err.message }, { status: 500 }); + } +} diff --git a/app/api/generate/hashtags/route.js b/app/api/generate/hashtags/route.js new file mode 100644 index 0000000..bc8b260 --- /dev/null +++ b/app/api/generate/hashtags/route.js @@ -0,0 +1,26 @@ +import { NextResponse } from 'next/server'; +import { requireUser } from '@/lib/session'; + +const ENGINE_URL = process.env.ENGINE_URL || 'http://localhost:3030'; +const ENGINE_SECRET = process.env.ENGINE_SECRET || ''; + +export async function POST(req) { + const user = await requireUser(); + if (!user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + const body = await req.json(); + try { + const res = await fetch(`${ENGINE_URL}/api/generate/hashtags`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'x-internal-secret': ENGINE_SECRET, + 'x-user-id': String(user.id), + }, + body: JSON.stringify(body), + }); + const data = await res.json(); + return NextResponse.json(data, { status: res.status }); + } catch (err) { + return NextResponse.json({ error: err.message }, { status: 500 }); + } +} diff --git a/components/ChannelView.js b/components/ChannelView.js index 4584257..4564601 100644 --- a/components/ChannelView.js +++ b/components/ChannelView.js @@ -11,6 +11,8 @@ import PostPreview from './PostPreview'; import PostTemplates from './PostTemplates'; import ChannelAnalytics from './ChannelAnalytics'; import FromUrlModal from './FromUrlModal'; +import PollModal from './PollModal'; +import HashtagSuggest from './HashtagSuggest'; const GOAL_LABELS = { educational: 'Обучение', news: 'Новости', @@ -58,6 +60,7 @@ export default function ChannelView({ channel }) { // Photo search modal const [showPhotoSearch, setShowPhotoSearch] = useState(false); const [showFromUrl, setShowFromUrl] = useState(false); + const [showPoll, setShowPoll] = useState(false); // Трансформации const [transforming, setTransforming] = useState(false); @@ -383,6 +386,15 @@ export default function ChannelView({ channel }) { По ссылке + {channel.platform === 'telegram' && ( + + )} + ) : ( +
+
+ + Хештеги + +
+ + +
+
+ + {loading && ( +
+ Генерирую хештеги... +
+ )} + + {!loading && tags.length > 0 && ( + <> +
+ {tags.map(tag => ( + + ))} +
+ + {selected.size > 0 && ( + + )} + + )} + + {!loading && tags.length === 0 && ( +

Нет результатов. Попробуйте ещё раз.

+ )} +
+ )} + + ); +} diff --git a/components/PollModal.js b/components/PollModal.js new file mode 100644 index 0000000..2c5f3fe --- /dev/null +++ b/components/PollModal.js @@ -0,0 +1,175 @@ +'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 ( +
+
+ {/* Header */} +
+

Создать опрос

+ +
+ + {/* Body */} +
+ {/* Вопрос */} +
+ +