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:
+62
-16
@@ -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
@@ -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 && (
|
||||||
|
|||||||
@@ -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>}
|
||||||
|
|||||||
Reference in New Issue
Block a user