Import source: app/src/admin/CommentModeration
Import the existing local project snapshot. This commit organizes existing files; it does not represent newly developed functionality.
This commit is contained in:
@@ -0,0 +1,141 @@
|
|||||||
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
|
import { useNav, useToast, EmptyState } from '../../teknav-ui.jsx';
|
||||||
|
import { adminApi } from '../lib/admin-api.js';
|
||||||
|
import { BRAND, PanelState, actionBtn } from './Shared.jsx';
|
||||||
|
|
||||||
|
function CommentsModeration() {
|
||||||
|
const toast = useToast();
|
||||||
|
const { navigate } = useNav();
|
||||||
|
const [flaggedOnly, setFlaggedOnly] = useState(false);
|
||||||
|
const [items, setItems] = useState([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState(false);
|
||||||
|
const [selected, setSelected] = useState(new Set());
|
||||||
|
|
||||||
|
const refresh = useCallback(() => {
|
||||||
|
setLoading(true);
|
||||||
|
setError(false);
|
||||||
|
setSelected(new Set());
|
||||||
|
return adminApi.listComments({ flagged: flaggedOnly, limit: 50 })
|
||||||
|
.then(res => setItems(res.items ?? []))
|
||||||
|
.catch(() => { setItems([]); setError(true); })
|
||||||
|
.finally(() => setLoading(false));
|
||||||
|
}, [flaggedOnly]);
|
||||||
|
|
||||||
|
useEffect(() => { refresh(); }, [refresh]);
|
||||||
|
|
||||||
|
const toggleSelect = (id) => setSelected(prev => {
|
||||||
|
const next = new Set(prev);
|
||||||
|
next.has(id) ? next.delete(id) : next.add(id);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
|
||||||
|
const toggleAll = () => setSelected(prev => prev.size === items.length ? new Set() : new Set(items.map(i => i.id)));
|
||||||
|
|
||||||
|
const remove = async (id) => {
|
||||||
|
try {
|
||||||
|
await adminApi.deleteComment(id);
|
||||||
|
toast('نظر حذف شد');
|
||||||
|
refresh();
|
||||||
|
} catch {
|
||||||
|
toast('حذف نظر ناموفق بود', 'error');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const unflag = async (id) => {
|
||||||
|
try {
|
||||||
|
await adminApi.unflagComment(id);
|
||||||
|
toast('گزارشهای نظر پاک شد');
|
||||||
|
refresh();
|
||||||
|
} catch {
|
||||||
|
toast('پاک کردن گزارشها ناموفق بود', 'error');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const bulkAction = async (action) => {
|
||||||
|
const ids = [...selected];
|
||||||
|
if (!ids.length) return;
|
||||||
|
try {
|
||||||
|
await adminApi.bulkModerateComments(ids, action);
|
||||||
|
toast(`${ids.length.toLocaleString('fa-IR')} نظر ${action === 'delete' ? 'حذف شد' : action === 'approve' ? 'تأیید شد' : 'علامتگذاری شد'}`);
|
||||||
|
refresh();
|
||||||
|
} catch {
|
||||||
|
toast('عملیات دستهای ناموفق بود', 'error');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const bulkDeleteFlagged = async () => {
|
||||||
|
if (!window.confirm('همه نظرهای گزارششده حذف شوند؟')) return;
|
||||||
|
try {
|
||||||
|
const res = await adminApi.deleteFlaggedComments();
|
||||||
|
toast(`${(res?.deleted ?? 0).toLocaleString('fa-IR')} نظر حذف شد`);
|
||||||
|
refresh();
|
||||||
|
} catch {
|
||||||
|
toast('حذف دستهای ناموفق بود', 'error');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ position: 'relative' }}>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: 16, marginBottom: 16, flexWrap: 'wrap' }}>
|
||||||
|
<label style={{ display: 'inline-flex', alignItems: 'center', gap: 8, fontSize: 13, color: BRAND.ink }}>
|
||||||
|
<input type="checkbox" checked={flaggedOnly} onChange={e => setFlaggedOnly(e.target.checked)} />
|
||||||
|
فقط گزارششده
|
||||||
|
</label>
|
||||||
|
{flaggedOnly && items.length > 0 && (
|
||||||
|
<button onClick={bulkDeleteFlagged} style={{ ...actionBtn, background: '#C94C4C', color: '#fff', fontSize: 12 }}>
|
||||||
|
حذف همه گزارششده
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<PanelState loading={loading} error={error} empty={!loading && !error && items.length === 0} emptyTitle="هنوز نظری برای نمایش وجود ندارد" />
|
||||||
|
{!loading && !error && items.length > 0 && (
|
||||||
|
<>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 10 }}>
|
||||||
|
<label style={{ display: 'inline-flex', alignItems: 'center', gap: 6, fontSize: 12, color: '#5F6B6D', cursor: 'pointer' }}>
|
||||||
|
<input type="checkbox" checked={selected.size === items.length} onChange={toggleAll} />
|
||||||
|
انتخاب همه
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'grid', gap: 12 }}>
|
||||||
|
{items.map(item => (
|
||||||
|
<div key={item.id} style={{ background: '#fff', border: `1px solid ${selected.has(item.id) ? BRAND.primary : item.flagged ? '#C94C4C' : '#E4DDD2'}`, borderRadius: 12, padding: 16, direction: 'rtl', transition: 'border-color 0.15s' }}>
|
||||||
|
<div style={{ display: 'flex', gap: 12, alignItems: 'flex-start' }}>
|
||||||
|
<input type="checkbox" checked={selected.has(item.id)} onChange={() => toggleSelect(item.id)} style={{ marginTop: 4, flexShrink: 0, accentColor: BRAND.primary }} />
|
||||||
|
<div style={{ flex: 1 }}>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'space-between', gap: 12, flexWrap: 'wrap' }}>
|
||||||
|
<div>
|
||||||
|
<div style={{ fontSize: 13, fontWeight: 800, color: BRAND.ink }}>{item.authorName} <span style={{ color: '#5F6B6D', fontWeight: 500 }}>@{item.authorUsername}</span></div>
|
||||||
|
<button onClick={() => navigate('/article/' + item.articleSlug)} style={{ background: 'none', border: 'none', padding: 0, color: BRAND.primary, cursor: 'pointer', fontSize: 12, fontFamily: 'Vazirmatn,sans-serif' }}>{item.articleTitle}</button>
|
||||||
|
</div>
|
||||||
|
<span style={{ fontSize: 12, color: '#5F6B6D' }}>{item.time}</span>
|
||||||
|
</div>
|
||||||
|
<p style={{ fontSize: 13, lineHeight: 1.8, color: '#263238', margin: '12px 0' }}>{item.body}</p>
|
||||||
|
<div style={{ display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap' }}>
|
||||||
|
<span style={{ fontSize: 12, color: item.flagged ? '#C94C4C' : '#5F6B6D' }}>{item.flagCount.toLocaleString('fa-IR')} گزارش</span>
|
||||||
|
<button onClick={() => unflag(item.id)} style={{ ...actionBtn, background: '#0F6B7318', color: '#0F6B73' }}>پاک کردن گزارش</button>
|
||||||
|
<button onClick={() => remove(item.id)} style={{ ...actionBtn, background: '#C94C4C18', color: '#C94C4C' }}>حذف</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{selected.size > 0 && (
|
||||||
|
<div style={{ position: 'fixed', bottom: 24, left: '50%', transform: 'translateX(-50%)', background: BRAND.ink, color: '#fff', borderRadius: 14, padding: '12px 20px', display: 'flex', alignItems: 'center', gap: 12, fontSize: 13, fontFamily: 'Vazirmatn,sans-serif', zIndex: 600, boxShadow: '0 8px 32px rgba(0,0,0,0.3)', direction: 'rtl', flexWrap: 'wrap', justifyContent: 'center' }}>
|
||||||
|
<span style={{ fontWeight: 700 }}>{selected.size.toLocaleString('fa-IR')} مورد انتخاب شده</span>
|
||||||
|
<div style={{ width: 1, height: 20, background: 'rgba(255,255,255,0.2)' }} />
|
||||||
|
<button onClick={() => bulkAction('delete')} style={{ background: '#C94C4C', color: '#fff', border: 'none', borderRadius: 8, padding: '6px 14px', cursor: 'pointer', fontSize: 12, fontFamily: 'Vazirmatn,sans-serif', fontWeight: 700 }}>حذف</button>
|
||||||
|
<button onClick={() => bulkAction('approve')} style={{ background: '#2F8F6B', color: '#fff', border: 'none', borderRadius: 8, padding: '6px 14px', cursor: 'pointer', fontSize: 12, fontFamily: 'Vazirmatn,sans-serif', fontWeight: 700 }}>تأیید</button>
|
||||||
|
<button onClick={() => bulkAction('flag')} style={{ background: '#D49A2A', color: '#fff', border: 'none', borderRadius: 8, padding: '6px 14px', cursor: 'pointer', fontSize: 12, fontFamily: 'Vazirmatn,sans-serif', fontWeight: 700 }}>علامتگذاری</button>
|
||||||
|
<button onClick={() => setSelected(new Set())} style={{ background: 'rgba(255,255,255,0.1)', color: '#fff', border: '1px solid rgba(255,255,255,0.2)', borderRadius: 8, padding: '6px 14px', cursor: 'pointer', fontSize: 12, fontFamily: 'Vazirmatn,sans-serif' }}>لغو انتخاب</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { CommentsModeration };
|
||||||
Reference in New Issue
Block a user