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 [name, setName] = useState('');
const [niche, setNiche] = useState(''); const [niche, setNiche] = useState('');
const [audience, setAudience] = 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'); const [language, setLanguage] = useState('ru');
// Шаг 2 — стиль // Шаг 2 — стиль
@@ -70,7 +71,7 @@ export default function NewChannelPage() {
setBusy(true); setBusy(true);
setError(''); setError('');
const data = { const data = {
name, niche, audience, goal, language, region: 'ru', name, niche, audience, goal: goals.join(','), language, region: 'ru',
style: { style: {
tone, formality, humor, tone, formality, humor,
post_length: postLength, post_length: postLength,
@@ -150,22 +151,67 @@ export default function NewChannelPage() {
/> />
</div> </div>
<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"> <div className="grid grid-cols-2 sm:grid-cols-5 gap-2">
{GOALS.map(g => ( {GOALS.map(g => {
<button const on = goals.includes(g.v);
key={g.v} return (
type="button" <button
onClick={() => setGoal(g.v)} key={g.v}
className={`p-2.5 rounded-lg border text-left transition-colors ${ type="button"
goal === g.v ? 'border-accent bg-accent/10' : 'border-border bg-surface2 hover:border-gray-600' 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 className="text-sm font-medium">{g.label}</div>
))} <div className="text-xs text-gray-500 mt-0.5">{g.desc}</div>
</button>
);
})}
</div> </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>
<div> <div>
<label className="label">Язык постов</label> <label className="label">Язык постов</label>
+1 -1
View File
@@ -68,7 +68,7 @@ export default async function HomePage() {
{ch.name} {ch.name}
</h3> </h3>
<span className="text-xs px-2 py-0.5 rounded-full bg-surface2 text-gray-400"> <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> </span>
</div> </div>
{ch.niche && ( {ch.niche && (
+2 -2
View File
@@ -4,7 +4,7 @@ import Link from 'next/link';
import { import {
ArrowLeft, Sparkles, Wand2, Copy, Check, Loader2, Settings, ArrowLeft, Sparkles, Wand2, Copy, Check, Loader2, Settings,
Image as ImageIcon, RefreshCw, Scissors, Maximize2, Zap, Heart, Image as ImageIcon, RefreshCw, Scissors, Maximize2, Zap, Heart,
MessageSquare, Pencil, X, Send, Clock, Search, Camera, ExternalLink MessageSquare, Pencil, X, Send, Clock, Search, Camera, ExternalLink, Link2
} from 'lucide-react'; } from 'lucide-react';
import PhotoSearchModal from './PhotoSearchModal'; import PhotoSearchModal from './PhotoSearchModal';
import PostPreview from './PostPreview'; import PostPreview from './PostPreview';
@@ -330,7 +330,7 @@ export default function ChannelView({ channel }) {
<Sparkles className="w-5 h-5 text-accent" /> <Sparkles className="w-5 h-5 text-accent" />
<h1 className="text-2xl font-bold">{channel.name}</h1> <h1 className="text-2xl font-bold">{channel.name}</h1>
<span className="text-xs px-2 py-0.5 rounded-full bg-surface2 text-gray-400"> <span className="text-xs px-2 py-0.5 rounded-full bg-surface2 text-gray-400">
{GOAL_LABELS[channel.goal] || channel.goal} {(channel.goal || '').split(',').map(g => GOAL_LABELS[g.trim()] || g.trim()).join(' · ')}
</span> </span>
</div> </div>
{channel.niche && <p className="text-sm text-gray-500 max-w-2xl">{channel.niche}</p>} {channel.niche && <p className="text-sm text-gray-500 max-w-2xl">{channel.niche}</p>}