fix: goal+language in ChannelEdit, metrics 500 (await params)
ChannelEdit.js: - Добавлены goal (multi-select + кастомные, как в форме создания) - Добавлен language (select: ru/en/uk/kk) - Импортированы Plus, X иконки и GOALS константа app/api/metrics/channel/[channelId]/route.js: app/api/metrics/best-time/[channelId]/route.js: - await params (Next.js 16 требует), иначе 500
This commit is contained in:
@@ -7,7 +7,8 @@ export async function GET(request, { params }) {
|
|||||||
if (!user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
if (!user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||||
const { searchParams } = new URL(request.url);
|
const { searchParams } = new URL(request.url);
|
||||||
try {
|
try {
|
||||||
const data = await engine.getBestTime(params.channelId, Object.fromEntries(searchParams));
|
const { channelId } = await params;
|
||||||
|
const data = await engine.getBestTime(channelId, Object.fromEntries(searchParams));
|
||||||
return NextResponse.json(data);
|
return NextResponse.json(data);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return NextResponse.json({ error: err.message }, { status: err.status || 500 });
|
return NextResponse.json({ error: err.message }, { status: err.status || 500 });
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ export async function GET(request, { params }) {
|
|||||||
if (!user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
if (!user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||||
const { searchParams } = new URL(request.url);
|
const { searchParams } = new URL(request.url);
|
||||||
try {
|
try {
|
||||||
const data = await engine.getChannelMetrics(params.channelId, Object.fromEntries(searchParams));
|
const { channelId } = await params;
|
||||||
|
const data = await engine.getChannelMetrics(channelId, Object.fromEntries(searchParams));
|
||||||
return NextResponse.json(data);
|
return NextResponse.json(data);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return NextResponse.json({ error: err.message }, { status: err.status || 500 });
|
return NextResponse.json({ error: err.message }, { status: err.status || 500 });
|
||||||
|
|||||||
@@ -2,7 +2,15 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { ArrowLeft, Save, Trash2, Loader2, Image as ImageIcon, Type, Palette } from 'lucide-react';
|
import { ArrowLeft, Save, Trash2, Loader2, Image as ImageIcon, Type, Palette, Plus, X } from 'lucide-react';
|
||||||
|
|
||||||
|
const GOALS = [
|
||||||
|
{ v: 'educational', label: 'Обучение', desc: 'Объясняем, разбираем' },
|
||||||
|
{ v: 'news', label: 'Новости', desc: 'Что произошло' },
|
||||||
|
{ v: 'entertainment', label: 'Развлечение', desc: 'Лёгкий контент, мемы' },
|
||||||
|
{ v: 'expert', label: 'Экспертный', desc: 'Глубокий анализ, инсайты' },
|
||||||
|
{ v: 'sales', label: 'Продажи', desc: 'Подвести к покупке' },
|
||||||
|
];
|
||||||
|
|
||||||
const TONES = [
|
const TONES = [
|
||||||
{ v: 'friendly', label: 'Дружелюбный' },
|
{ v: 'friendly', label: 'Дружелюбный' },
|
||||||
@@ -66,6 +74,11 @@ export default function ChannelEdit({ channel }) {
|
|||||||
const [name, setName] = useState(channel.name || '');
|
const [name, setName] = useState(channel.name || '');
|
||||||
const [niche, setNiche] = useState(channel.niche || '');
|
const [niche, setNiche] = useState(channel.niche || '');
|
||||||
const [audience, setAudience] = useState(channel.audience || '');
|
const [audience, setAudience] = useState(channel.audience || '');
|
||||||
|
const [goals, setGoals] = useState(
|
||||||
|
channel.goal ? channel.goal.split(',').map(g => g.trim()).filter(Boolean) : ['educational']
|
||||||
|
);
|
||||||
|
const [customGoal, setCustomGoal] = useState('');
|
||||||
|
const [language, setLanguage] = useState(channel.language || 'ru');
|
||||||
const [tone, setTone] = useState(style.tone || 'friendly');
|
const [tone, setTone] = useState(style.tone || 'friendly');
|
||||||
const [formality, setFormality] = useState(style.formality || 'informal');
|
const [formality, setFormality] = useState(style.formality || 'informal');
|
||||||
const [humor, setHumor] = useState(style.humor || 'moderate');
|
const [humor, setHumor] = useState(style.humor || 'moderate');
|
||||||
@@ -92,7 +105,7 @@ export default function ChannelEdit({ channel }) {
|
|||||||
setError('');
|
setError('');
|
||||||
try {
|
try {
|
||||||
const data = {
|
const data = {
|
||||||
name, niche, audience,
|
name, niche, audience, goal: goals.join(','), language,
|
||||||
style: {
|
style: {
|
||||||
tone, formality, humor,
|
tone, formality, humor,
|
||||||
post_length: postLength,
|
post_length: postLength,
|
||||||
@@ -185,6 +198,47 @@ export default function ChannelEdit({ channel }) {
|
|||||||
<label className="label">Аудитория</label>
|
<label className="label">Аудитория</label>
|
||||||
<textarea className="input min-h-[70px]" value={audience} onChange={e => setAudience(e.target.value)} />
|
<textarea className="input min-h-[70px]" value={audience} onChange={e => setAudience(e.target.value)} />
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="label">Цель канала <span className="text-gray-500 font-normal">(можно несколько)</span></label>
|
||||||
|
<div className="grid grid-cols-2 sm:grid-cols-5 gap-2 mb-2">
|
||||||
|
{GOALS.map(g => {
|
||||||
|
const on = goals.includes(g.v);
|
||||||
|
return (
|
||||||
|
<button key={g.v} type="button"
|
||||||
|
onClick={() => setGoals(on ? goals.filter(x => x !== g.v) : [...goals, g.v])}
|
||||||
|
className={`p-2.5 rounded-lg border text-left transition-colors ${on ? 'border-accent bg-accent/10' : 'border-border bg-surface2 hover:border-gray-600'}`}>
|
||||||
|
<div className="text-sm font-medium">{g.label}</div>
|
||||||
|
<div className="text-xs text-gray-500 mt-0.5">{g.desc}</div>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<input className="input text-sm flex-1" placeholder="Своя цель — введи и нажми +"
|
||||||
|
value={customGoal} onChange={e => setCustomGoal(e.target.value)}
|
||||||
|
onKeyDown={e => { if (e.key === 'Enter') { e.preventDefault(); const v = customGoal.trim(); if (v && !goals.includes(v)) setGoals([...goals, v]); setCustomGoal(''); }}} />
|
||||||
|
<button type="button" onClick={() => { const v = customGoal.trim(); if (v && !goals.includes(v)) setGoals([...goals, v]); setCustomGoal(''); }}
|
||||||
|
disabled={!customGoal.trim()} className="btn-primary px-3 disabled:opacity-40"><Plus className="w-4 h-4" /></button>
|
||||||
|
</div>
|
||||||
|
{goals.filter(g => !GOALS.find(x => x.v === g)).length > 0 && (
|
||||||
|
<div className="flex flex-wrap gap-1.5 mt-2">
|
||||||
|
{goals.filter(g => !GOALS.find(x => x.v === g)).map(g => (
|
||||||
|
<span key={g} className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full bg-accent/15 border border-accent/40 text-xs">
|
||||||
|
{g}<button type="button" onClick={() => setGoals(goals.filter(x => x !== g))}><X className="w-3 h-3" /></button>
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="label">Язык постов</label>
|
||||||
|
<select className="input" value={language} onChange={e => setLanguage(e.target.value)}>
|
||||||
|
<option value="ru">Русский</option>
|
||||||
|
<option value="en">English</option>
|
||||||
|
<option value="uk">Українська</option>
|
||||||
|
<option value="kk">Қазақша</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="card p-5 space-y-4">
|
<div className="card p-5 space-y-4">
|
||||||
|
|||||||
Reference in New Issue
Block a user