fix teg
This commit is contained in:
@@ -46,6 +46,7 @@ const DEFAULT_META = {
|
||||
slug: '',
|
||||
excerpt: '',
|
||||
cover_url: null,
|
||||
source_url: null,
|
||||
font_family: 'Merriweather',
|
||||
category_id: null,
|
||||
tag_names: [],
|
||||
@@ -67,6 +68,7 @@ export default function App() {
|
||||
slug: data.slug,
|
||||
excerpt: data.excerpt || '',
|
||||
cover_url: data.cover_url || null,
|
||||
source_url: data.source_url || null,
|
||||
font_family: data.font_family || 'Merriweather',
|
||||
category_id: data.category?.id || null,
|
||||
tag_names: (data.tags || []).map(t => t.name),
|
||||
|
||||
@@ -1,24 +1,35 @@
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
import { Upload, X, ImageIcon, Video, Loader2, Check } from 'lucide-react'
|
||||
import { Upload, X, ImageIcon, Video, Loader2, Check, Search } from 'lucide-react'
|
||||
import { api } from '../api'
|
||||
|
||||
export function MediaModal({ type, token, onSelect, onClose }) {
|
||||
const [items, setItems] = useState([])
|
||||
const [allItems, setAllItems] = useState([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [uploading, setUploading] = useState(false)
|
||||
const [search, setSearch] = useState('')
|
||||
const fileRef = useRef(null)
|
||||
|
||||
useEffect(() => {
|
||||
api.listMedia(token, type)
|
||||
.then(data => { setItems(data); setLoading(false) })
|
||||
.then(data => { setAllItems(data); setItems(data); setLoading(false) })
|
||||
.catch(() => setLoading(false))
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (!search.trim()) {
|
||||
setItems(allItems)
|
||||
} else {
|
||||
const q = search.toLowerCase()
|
||||
setItems(allItems.filter(it => it.filename?.toLowerCase().includes(q)))
|
||||
}
|
||||
}, [search, allItems])
|
||||
|
||||
const handleUpload = async file => {
|
||||
setUploading(true)
|
||||
try {
|
||||
const res = await api.uploadMedia(file, token)
|
||||
setItems(prev => [res, ...prev])
|
||||
setAllItems(prev => [res, ...prev])
|
||||
} catch {}
|
||||
setUploading(false)
|
||||
}
|
||||
@@ -64,6 +75,29 @@ export function MediaModal({ type, token, onSelect, onClose }) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Search bar */}
|
||||
<div className="px-5 py-3 border-b border-slate-100">
|
||||
<div className="relative">
|
||||
<Search size={14} className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-400" />
|
||||
<input
|
||||
type="text"
|
||||
value={search}
|
||||
onChange={e => setSearch(e.target.value)}
|
||||
placeholder="Поиск по имени файла..."
|
||||
className="w-full pl-8 pr-4 py-1.5 text-sm border border-slate-200 rounded-lg outline-none focus:border-indigo-400 focus:ring-1 focus:ring-indigo-200"
|
||||
/>
|
||||
{search && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setSearch('')}
|
||||
className="absolute right-2 top-1/2 -translate-y-1/2 p-0.5 hover:bg-slate-100 rounded"
|
||||
>
|
||||
<X size={12} className="text-slate-400" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 overflow-y-auto p-4">
|
||||
{loading ? (
|
||||
@@ -73,14 +107,16 @@ export function MediaModal({ type, token, onSelect, onClose }) {
|
||||
) : isEmpty ? (
|
||||
<div className="flex flex-col items-center justify-center h-48 text-slate-300 gap-3">
|
||||
{isImage ? <ImageIcon size={36} /> : <Video size={36} />}
|
||||
<p className="text-sm">Файлы не найдены</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => fileRef.current?.click()}
|
||||
className="text-xs text-indigo-500 hover:text-indigo-600 underline"
|
||||
>
|
||||
Загрузить первый файл
|
||||
</button>
|
||||
<p className="text-sm">{search ? 'Ничего не найдено' : 'Файлы не найдены'}</p>
|
||||
{!search && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => fileRef.current?.click()}
|
||||
className="text-xs text-indigo-500 hover:text-indigo-600 underline"
|
||||
>
|
||||
Загрузить первый файл
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className={`grid gap-2.5 ${isImage ? 'grid-cols-4 sm:grid-cols-5' : 'grid-cols-2 sm:grid-cols-3'}`}>
|
||||
@@ -101,14 +137,12 @@ export function MediaModal({ type, token, onSelect, onClose }) {
|
||||
) : (
|
||||
<video src={item.url} className="w-full h-full object-cover" />
|
||||
)}
|
||||
{/* Hover overlay */}
|
||||
<div className="absolute inset-0 bg-indigo-600/0 group-hover:bg-indigo-600/25 transition-colors flex items-center justify-center">
|
||||
<Check
|
||||
size={22}
|
||||
className="text-white opacity-0 group-hover:opacity-100 drop-shadow-lg transition-opacity"
|
||||
/>
|
||||
</div>
|
||||
{/* Filename tooltip */}
|
||||
<div className="absolute bottom-0 inset-x-0 px-2 py-1.5 bg-gradient-to-t from-black/60 to-transparent opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<p className="text-white text-[10px] truncate leading-none">{item.filename}</p>
|
||||
</div>
|
||||
|
||||
@@ -48,6 +48,17 @@ export function Sidebar({ meta, categories, onChange, onUploadCover, wordCount,
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Source URL */}
|
||||
<div>
|
||||
<Label>Первоисточник</Label>
|
||||
<input
|
||||
className="input-dark text-xs"
|
||||
placeholder="https://vk.com/..."
|
||||
value={meta.source_url || ''}
|
||||
onChange={e => onChange({ source_url: e.target.value || null })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Excerpt */}
|
||||
<div>
|
||||
<Label>Анонс</Label>
|
||||
|
||||
@@ -157,7 +157,24 @@ function SizeDropdown({ editor }) {
|
||||
/* ── Table menu ── */
|
||||
|
||||
function TableMenu({ editor }) {
|
||||
const { open, setOpen, pos, btnRef, toggle } = useFixedDropdown()
|
||||
const [open, setOpen] = useState(false)
|
||||
const [pos, setPos] = useState({ top: 0, left: 0 })
|
||||
const wrapRef = useRef(null)
|
||||
|
||||
const toggle = () => {
|
||||
if (!open && wrapRef.current) {
|
||||
const r = wrapRef.current.getBoundingClientRect()
|
||||
setPos({ top: r.bottom + 4, left: r.left })
|
||||
}
|
||||
setOpen(o => !o)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return
|
||||
const close = e => { if (!wrapRef.current?.contains(e.target)) setOpen(false) }
|
||||
document.addEventListener('mousedown', close)
|
||||
return () => document.removeEventListener('mousedown', close)
|
||||
}, [open])
|
||||
|
||||
const items = [
|
||||
{ label: 'Вставить таблицу 3×3', fn: () => editor.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run() },
|
||||
@@ -174,16 +191,14 @@ function TableMenu({ editor }) {
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="flex-shrink-0">
|
||||
<div ref={wrapRef} className="flex-shrink-0">
|
||||
<Btn
|
||||
onClick={e => toggle({ preventDefault: () => {}, ...e })}
|
||||
onClick={toggle}
|
||||
active={editor.isActive('table')}
|
||||
title="Таблица"
|
||||
>
|
||||
<Table2 size={14} />
|
||||
</Btn>
|
||||
{/* Используем ref на обёртке для Btn */}
|
||||
<span ref={btnRef} style={{ display: 'none' }} />
|
||||
|
||||
{open && (
|
||||
<div
|
||||
|
||||
Reference in New Issue
Block a user