fix: Link2 undefined crash + goal multi-select + custom goal

ChannelView.js:
- Добавлен Link2 в import lucide-react (ReferenceError при открытии канала)
- Отображение goal учитывает множественные значения через split(',')

app/page.js:
- Аналогичный фикс отображения goal (split → map → join)

channels/new/page.js:
- Цель канала: single-select → multi-select (можно выбрать несколько)
- Кастомная цель: поле + кнопка «+», Enter, чипы с удалением
- Сохраняется как CSV строка (goal: goals.join(','))

DB:
- channels.goal varchar(50) → varchar(255) для длинных кастомных значений
This commit is contained in:
Ник (Claude)
2026-06-09 08:39:32 +03:00
parent 8f4dc1a386
commit 69226cbbde
3 changed files with 65 additions and 19 deletions
+62 -16
View File
@@ -49,7 +49,8 @@ export default function NewChannelPage() {
const [name, setName] = useState('');
const [niche, setNiche] = useState('');
const [audience, setAudience] = useState('');
const [goal, setGoal] = useState('educational');
const [goals, setGoals] = useState(['educational']); // multi-select, отправляем как CSV
const [customGoal, setCustomGoal] = useState(''); // поле для своей цели
const [language, setLanguage] = useState('ru');
// Шаг 2 — стиль
@@ -70,7 +71,7 @@ export default function NewChannelPage() {
setBusy(true);
setError('');
const data = {
name, niche, audience, goal, language, region: 'ru',
name, niche, audience, goal: goals.join(','), language, region: 'ru',
style: {
tone, formality, humor,
post_length: postLength,
@@ -150,22 +151,67 @@ export default function NewChannelPage() {
/>
</div>
<div>
<label className="label">Цель канала</label>
<label className="label">Цель канала <span className="text-gray-500 font-normal">(можно несколько)</span></label>
<div className="grid grid-cols-2 sm:grid-cols-5 gap-2">
{GOALS.map(g => (
<button
key={g.v}
type="button"
onClick={() => setGoal(g.v)}
className={`p-2.5 rounded-lg border text-left transition-colors ${
goal === g.v ? '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>
))}
{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 mt-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))} className="hover:text-red-400">
<X className="w-3 h-3" />
</button>
</span>
))}
</div>
)}
</div>
<div>
<label className="label">Язык постов</label>
+1 -1
View File
@@ -68,7 +68,7 @@ export default async function HomePage() {
{ch.name}
</h3>
<span className="text-xs px-2 py-0.5 rounded-full bg-surface2 text-gray-400">
{GOAL_LABELS[ch.goal] || ch.goal}
{(ch.goal || '').split(',').map(g => GOAL_LABELS[g.trim()] || g.trim()).join(' · ')}
</span>
</div>
{ch.niche && (