test ract
This commit is contained in:
156
web/editor-ui/src/components/Sidebar.jsx
Normal file
156
web/editor-ui/src/components/Sidebar.jsx
Normal file
@@ -0,0 +1,156 @@
|
||||
import { useRef } from 'react'
|
||||
import { X, ImageIcon, ChevronDown, BarChart2 } from 'lucide-react'
|
||||
import { TagInput } from './TagInput'
|
||||
|
||||
export function Sidebar({ meta, categories, onChange, onUploadCover, wordCount, charCount, fonts }) {
|
||||
const coverRef = useRef(null)
|
||||
|
||||
return (
|
||||
<aside className="w-[280px] flex-shrink-0 bg-slate-900 border-l border-slate-800 flex flex-col overflow-y-auto">
|
||||
|
||||
{/* Stats bar */}
|
||||
<div className="flex items-center gap-4 px-4 py-3 border-b border-slate-800">
|
||||
<BarChart2 size={14} className="text-slate-500 flex-shrink-0" />
|
||||
<div className="flex gap-4 text-xs">
|
||||
<span>
|
||||
<span className="font-semibold text-white">{wordCount}</span>
|
||||
<span className="text-slate-400 ml-1">слов</span>
|
||||
</span>
|
||||
<span>
|
||||
<span className="font-semibold text-white">{charCount}</span>
|
||||
<span className="text-slate-400 ml-1">симв.</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Status */}
|
||||
<div className="px-4 pt-4 pb-1">
|
||||
<span className={`inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-medium
|
||||
${meta.status === 'published'
|
||||
? 'bg-emerald-900/60 text-emerald-300 border border-emerald-800'
|
||||
: 'bg-amber-900/40 text-amber-300 border border-amber-800/60'}`}
|
||||
>
|
||||
<span className={`w-1.5 h-1.5 rounded-full ${meta.status === 'published' ? 'bg-emerald-400' : 'bg-amber-400'}`} />
|
||||
{meta.status === 'published' ? 'Опубликовано' : 'Черновик'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="p-4 space-y-5">
|
||||
|
||||
{/* Slug */}
|
||||
<div>
|
||||
<Label>URL (slug)</Label>
|
||||
<input
|
||||
className="input-dark font-mono text-xs"
|
||||
placeholder="url-stati"
|
||||
value={meta.slug}
|
||||
onChange={e => onChange({ slug: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Excerpt */}
|
||||
<div>
|
||||
<Label>Анонс</Label>
|
||||
<textarea
|
||||
className="input-dark resize-none text-sm"
|
||||
placeholder="Краткое описание статьи…"
|
||||
rows={3}
|
||||
value={meta.excerpt}
|
||||
onChange={e => onChange({ excerpt: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Category */}
|
||||
<div>
|
||||
<Label>Категория</Label>
|
||||
<div className="relative">
|
||||
<select
|
||||
className="input-dark appearance-none cursor-pointer pr-8 text-sm"
|
||||
value={meta.category_id || ''}
|
||||
onChange={e => onChange({ category_id: e.target.value || null })}
|
||||
>
|
||||
<option value="">Без категории</option>
|
||||
{categories.map(c => (
|
||||
<option key={c.id} value={c.id}>{c.name}</option>
|
||||
))}
|
||||
</select>
|
||||
<ChevronDown size={13} className="absolute right-2.5 top-1/2 -translate-y-1/2 text-slate-400 pointer-events-none" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tags */}
|
||||
<div>
|
||||
<Label>Теги</Label>
|
||||
<TagInput tags={meta.tag_names} onChange={tags => onChange({ tag_names: tags })} />
|
||||
<p className="text-[11px] text-slate-500 mt-1.5">Enter или запятая для добавления</p>
|
||||
</div>
|
||||
|
||||
{/* Font */}
|
||||
<div>
|
||||
<Label>Шрифт статьи</Label>
|
||||
<div className="relative">
|
||||
<select
|
||||
className="input-dark appearance-none cursor-pointer pr-8 text-sm"
|
||||
style={{ fontFamily: meta.font_family }}
|
||||
value={meta.font_family}
|
||||
onChange={e => onChange({ font_family: e.target.value })}
|
||||
>
|
||||
{fonts.map(f => (
|
||||
<option key={f} value={f} style={{ fontFamily: f }}>{f}</option>
|
||||
))}
|
||||
</select>
|
||||
<ChevronDown size={13} className="absolute right-2.5 top-1/2 -translate-y-1/2 text-slate-400 pointer-events-none" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Cover image */}
|
||||
<div>
|
||||
<Label>Обложка</Label>
|
||||
{meta.cover_url ? (
|
||||
<div className="relative rounded-xl overflow-hidden ring-1 ring-slate-700">
|
||||
<img
|
||||
src={meta.cover_url}
|
||||
alt="cover"
|
||||
className="w-full h-36 object-cover"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onChange({ cover_url: null })}
|
||||
className="absolute top-2 right-2 p-1.5 bg-slate-900/80 backdrop-blur-sm rounded-full hover:bg-red-600 transition-colors"
|
||||
>
|
||||
<X size={12} className="text-white" />
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => coverRef.current?.click()}
|
||||
className="w-full h-28 border-2 border-dashed border-slate-700 rounded-xl flex flex-col items-center justify-center gap-2 hover:border-indigo-500 hover:bg-slate-800/50 transition-all group"
|
||||
>
|
||||
<ImageIcon size={20} className="text-slate-500 group-hover:text-indigo-400 transition-colors" />
|
||||
<span className="text-xs text-slate-500 group-hover:text-slate-300 transition-colors">
|
||||
Загрузить обложку
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
<input
|
||||
ref={coverRef}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
className="hidden"
|
||||
onChange={e => e.target.files[0] && onUploadCover(e.target.files[0])}
|
||||
/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</aside>
|
||||
)
|
||||
}
|
||||
|
||||
function Label({ children }) {
|
||||
return (
|
||||
<p className="text-[11px] font-semibold uppercase tracking-wider text-slate-400 mb-1.5">
|
||||
{children}
|
||||
</p>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user