Files
teknav-bak/app/src/admin/CommentModeration.jsx
T
arsam 06f3e68d9c Import source: app/src/admin/CommentModeration
Import the existing local project snapshot. This commit organizes existing files; it does not represent newly developed functionality.
2026-09-18 23:23:58 +03:30

142 lines
7.8 KiB
React
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 };