Import source: app/src/admin/MediaLibrary

Import the existing local project snapshot. This commit organizes existing files; it does not represent newly developed functionality.
This commit is contained in:
arsam
2026-09-18 23:24:04 +03:30
parent d029447bdf
commit e240a92914
+89
View File
@@ -0,0 +1,89 @@
import { useState, useEffect, useCallback } from 'react';
import { useToast, ConfirmModal, inputStyle } from '../../teknav-ui.jsx';
import { adminApi } from '../lib/admin-api.js';
import { PanelState, actionBtn } from './Shared.jsx';
function MediaLibrary({ role }) {
const toast = useToast();
const [items, setItems] = useState([]);
const [query, setQuery] = useState('');
const [mimeType, setMimeType] = useState('');
const [loading, setLoading] = useState(true);
const [error, setError] = useState(false);
const [confirm, setConfirm] = useState(null);
const refresh = useCallback(() => {
setLoading(true);
setError(false);
return adminApi.listMedia({ q: query, mimeType, limit: 60 })
.then(res => setItems(res.items ?? []))
.catch(() => { setItems([]); setError(true); })
.finally(() => setLoading(false));
}, [query, mimeType]);
useEffect(() => { refresh(); }, [refresh]);
const copyUrl = async (url) => {
try {
await navigator.clipboard.writeText(url);
toast('آدرس رسانه کپی شد');
} catch {
toast('کپی آدرس رسانه ناموفق بود', 'error');
}
};
const deleteMedia = async (id) => {
try {
await adminApi.deleteMedia(id);
toast('رسانه حذف شد');
refresh();
} catch (e) {
toast(e.body?.error === 'media_in_use' ? 'این رسانه در محتوا یا پروفایل استفاده شده است' : 'حذف رسانه ناموفق بود', 'error');
} finally {
setConfirm(null);
}
};
return (
<div>
{confirm && <ConfirmModal msg="آیا از حذف این رسانه مطمئن هستید؟" onConfirm={() => deleteMedia(confirm)} onCancel={() => setConfirm(null)} />}
<div style={{ display: 'flex', gap: 12, marginBottom: 20, flexWrap: 'wrap' }}>
<input value={query} onChange={e => setQuery(e.target.value)} placeholder="جستجوی نام فایل..." style={{ ...inputStyle, maxWidth: 260 }} dir="rtl" />
<select value={mimeType} onChange={e => setMimeType(e.target.value)} style={{ ...inputStyle, maxWidth: 180 }} dir="rtl">
<option value="">همه فایل‌ها</option>
<option value="image/">تصویرها</option>
<option value="image/webp">WebP</option>
<option value="image/png">PNG</option>
<option value="image/jpeg">JPEG</option>
</select>
</div>
<PanelState loading={loading} error={error} empty={!loading && !error && items.length === 0} emptyTitle="هنوز رسانه‌ای در بک‌اند ثبت نشده است" />
{!loading && !error && items.length > 0 && (
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill,minmax(220px,1fr))', gap: 16 }}>
{items.map(item => (
<div key={item.id} style={{ background: '#fff', border: '1px solid #E4DDD2', borderRadius: 12, overflow: 'hidden', direction: 'rtl' }}>
<div style={{ height: 138, background: '#F4EFE6', display: 'flex', alignItems: 'center', justifyContent: 'center', overflow: 'hidden' }}>
{item.mimeType?.startsWith('image/')
? <img src={item.url} alt={item.filename} style={{ width: '100%', height: '100%', objectFit: 'cover' }} loading="lazy" />
: <span style={{ color: '#5F6B6D', fontSize: 13 }}>{item.mimeType}</span>}
</div>
<div style={{ padding: 14 }}>
<div title={item.filename} style={{ fontSize: 13, fontWeight: 700, color: '#263238', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{item.filename}</div>
<div style={{ fontSize: 11, color: '#5F6B6D', marginTop: 6 }}>
{item.sizeBytes ? `${Math.round(item.sizeBytes / 1024).toLocaleString('fa-IR')} KB` : 'اندازه نامشخص'}
{item.width && item.height ? ` · ${item.width}×${item.height}` : ''}
</div>
<div style={{ display: 'flex', gap: 8, marginTop: 12, flexWrap: 'wrap' }}>
<button onClick={() => copyUrl(item.url)} style={{ ...actionBtn, background: '#0F6B7318', color: '#0F6B73' }}>کپی URL</button>
{role === 'admin' && <button onClick={() => setConfirm(item.id)} style={{ ...actionBtn, background: '#C94C4C18', color: '#C94C4C' }}>حذف</button>}
</div>
</div>
</div>
))}
</div>
)}
</div>
);
}
export { MediaLibrary };