test ract
This commit is contained in:
131
web/editor-ui/src/components/MediaModal.jsx
Normal file
131
web/editor-ui/src/components/MediaModal.jsx
Normal file
@@ -0,0 +1,131 @@
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
import { Upload, X, ImageIcon, Video, Loader2, Check } from 'lucide-react'
|
||||
import { api } from '../api'
|
||||
|
||||
export function MediaModal({ type, token, onSelect, onClose }) {
|
||||
const [items, setItems] = useState([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [uploading, setUploading] = useState(false)
|
||||
const fileRef = useRef(null)
|
||||
|
||||
useEffect(() => {
|
||||
api.listMedia(token, type)
|
||||
.then(data => { setItems(data); setLoading(false) })
|
||||
.catch(() => setLoading(false))
|
||||
}, [])
|
||||
|
||||
const handleUpload = async file => {
|
||||
setUploading(true)
|
||||
try {
|
||||
const res = await api.uploadMedia(file, token)
|
||||
setItems(prev => [res, ...prev])
|
||||
} catch {}
|
||||
setUploading(false)
|
||||
}
|
||||
|
||||
const accept = type === 'image' ? 'image/*' : 'video/*'
|
||||
const isEmpty = !loading && items.length === 0
|
||||
const isImage = type === 'image'
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fixed inset-0 z-50 bg-black/60 backdrop-blur-sm flex items-center justify-center p-4"
|
||||
onClick={e => { if (e.target === e.currentTarget) onClose() }}
|
||||
>
|
||||
<div className="bg-white rounded-2xl shadow-2xl w-full max-w-3xl max-h-[80vh] flex flex-col overflow-hidden">
|
||||
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between px-5 py-4 border-b border-slate-100">
|
||||
<div>
|
||||
<h2 className="font-semibold text-slate-900 text-base">Медиатека</h2>
|
||||
<p className="text-xs text-slate-400 mt-0.5">
|
||||
{isImage ? 'Изображения' : 'Видео'} · кликните для вставки
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => fileRef.current?.click()}
|
||||
disabled={uploading}
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 bg-indigo-600 hover:bg-indigo-700 text-white text-sm font-medium rounded-lg transition-colors disabled:opacity-50"
|
||||
>
|
||||
{uploading
|
||||
? <Loader2 size={14} className="animate-spin" />
|
||||
: <Upload size={14} />}
|
||||
Загрузить
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="p-1.5 hover:bg-slate-100 rounded-lg transition-colors"
|
||||
>
|
||||
<X size={18} className="text-slate-500" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 overflow-y-auto p-4">
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center h-48">
|
||||
<Loader2 size={28} className="animate-spin text-slate-300" />
|
||||
</div>
|
||||
) : 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>
|
||||
</div>
|
||||
) : (
|
||||
<div className={`grid gap-2.5 ${isImage ? 'grid-cols-4 sm:grid-cols-5' : 'grid-cols-2 sm:grid-cols-3'}`}>
|
||||
{items.map(item => (
|
||||
<button
|
||||
key={item.id}
|
||||
type="button"
|
||||
onClick={() => onSelect(item.url)}
|
||||
className="group relative rounded-xl overflow-hidden border-2 border-transparent hover:border-indigo-500 transition-all bg-slate-100 aspect-square"
|
||||
>
|
||||
{isImage ? (
|
||||
<img
|
||||
src={item.url}
|
||||
alt={item.filename}
|
||||
className="w-full h-full object-cover"
|
||||
loading="lazy"
|
||||
/>
|
||||
) : (
|
||||
<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>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<input
|
||||
ref={fileRef}
|
||||
type="file"
|
||||
accept={accept}
|
||||
className="hidden"
|
||||
onChange={e => { if (e.target.files[0]) handleUpload(e.target.files[0]) }}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user