From fd84842f8aff03ebf67b972b363e4ac639675469 Mon Sep 17 00:00:00 2001 From: arsam Date: Fri, 18 Sep 2026 23:23:52 +0330 Subject: [PATCH] Import source: app/src/admin/AnalyticsPanel Import the existing local project snapshot. This commit organizes existing files; it does not represent newly developed functionality. --- app/src/admin/AnalyticsPanel.jsx | 375 +++++++++++++++++++++++++++++++ 1 file changed, 375 insertions(+) create mode 100644 app/src/admin/AnalyticsPanel.jsx diff --git a/app/src/admin/AnalyticsPanel.jsx b/app/src/admin/AnalyticsPanel.jsx new file mode 100644 index 0000000..da19c1e --- /dev/null +++ b/app/src/admin/AnalyticsPanel.jsx @@ -0,0 +1,375 @@ +import { useState, useEffect } from 'react'; +import { useNav, useToast, EmptyState, pillBtn, inputStyle } from '../../teknav-ui.jsx'; +import { adminApi } from '../lib/admin-api.js'; +import { BRAND, PanelState, SEO_SEVERITY, actionBtn } from './Shared.jsx'; + +function SeoAuditPanel() { + const { navigate } = useNav(); + const [audit, setAudit] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(false); + const [page, setPage] = useState(0); + const PAGE_SIZE = 15; + + useEffect(() => { + setLoading(true); + setError(false); + adminApi.getSeoAudit() + .then(setAudit) + .catch(() => setError(true)) + .finally(() => setLoading(false)); + }, []); + + if (loading || error || !audit) return ; + + const issues = (audit.items ?? []).filter(item => item.missing?.length || item.needsRefresh); + const totalIssues = issues.reduce((sum, item) => sum + (item.missing?.length ?? 0) + (item.needsRefresh ? 1 : 0), 0); + const pageItems = issues.slice(page * PAGE_SIZE, (page + 1) * PAGE_SIZE); + const totalPages = Math.ceil(issues.length / PAGE_SIZE); + + const getSeverity = (key) => { + if (['metaDescription', 'ogTitle', 'ogDescription'].includes(key)) return 'error'; + if (['keywords', 'ogImage', 'canonicalPath'].includes(key)) return 'warning'; + return 'info'; + }; + + return ( +
+
+
+ {totalIssues.toLocaleString('fa-IR')} مشکل یافت شد در {issues.length.toLocaleString('fa-IR')} مقاله +
+ {issues.length === 0 && ✓ همه مقاله‌ها سالم هستند} +
+ {issues.length > 0 && ( +
+ + + + {['مقاله', 'مشکلات', 'شدت'].map(h => )} + + + + {pageItems.map((item, i) => { + const allKeys = [...(item.missing ?? []), ...(item.needsRefresh ? ['needsRefresh'] : [])]; + const worstSeverity = allKeys.some(k => getSeverity(k) === 'error') ? 'error' : allKeys.some(k => getSeverity(k) === 'warning') ? 'warning' : 'info'; + const sev = SEO_SEVERITY[worstSeverity]; + return ( + + + + + + ); + })} + +
{h}
+ + +
+ {allKeys.map(k => { + const s = SEO_SEVERITY[getSeverity(k)]; + return {k}; + })} +
+
+ {sev.label} +
+ {totalPages > 1 && ( +
+ + صفحه {(page + 1).toLocaleString('fa-IR')} از {totalPages.toLocaleString('fa-IR')} + +
+ )} +
+ )} +
+ ); +} + +function EngagementAnalyticsPanel() { + const [engagement, setEngagement] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(false); + + useEffect(() => { + setLoading(true); + setError(false); + adminApi.getEngagementAnalytics() + .then(setEngagement) + .catch(() => setError(true)) + .finally(() => setLoading(false)); + }, []); + + if (loading || error || !engagement) return ; + + const byType = engagement.byType ?? []; + const totalEvents = byType.reduce((sum, item) => sum + Number(item.count ?? 0), 0); + const topArticles = (engagement.topArticles ?? []).slice(0, 5); + const weeklyData = engagement.weeklyData ?? []; + + const barMax = Math.max(...weeklyData.map(d => d.count ?? 0), 1); + + return ( +
+ {weeklyData.length > 0 && ( +
+

رویدادهای ۷ روز گذشته

+
+ {weeklyData.map((d, i) => ( +
+
{Number(d.count ?? 0).toLocaleString('fa-IR')}
+
+
{d.date ?? ''}
+
+ ))} +
+
+ )} + +
+

انواع رویداد

+ {byType.length === 0 ? : ( + + + + {['نوع رویداد', 'تعداد', 'درصد'].map(h => )} + + + + {byType.map(item => ( + + + + + + ))} + +
{h}
{item.type}{Number(item.count ?? 0).toLocaleString('fa-IR')} + {totalEvents > 0 ? `${Math.round(Number(item.count ?? 0) / totalEvents * 100)}٪` : '—'} +
+ )} +
+ + {topArticles.length > 0 && ( +
+

۵ مقاله برتر بر اساس تعامل

+ {topArticles.map((article, i) => ( +
+ {article.title} + {Number(article.eventCount ?? 0).toLocaleString('fa-IR')} رویداد +
+ ))} +
+ )} +
+ ); +} + +function Phase9AnalyticsPanel() { + const [subTab, setSubTab] = useState('overview'); + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(false); + useEffect(() => { + let cancelled = false; + adminApi.getAnalytics() + .then(analytics => { if (!cancelled) setData(analytics); }) + .catch(() => { if (!cancelled) setError(true); }) + .finally(() => { if (!cancelled) setLoading(false); }); + return () => { cancelled = true; }; + }, []); + + const tabs = [ + { id: 'overview', label: 'مروری' }, + { id: 'engagement', label: 'تعامل' }, + ]; + + const tabBtn = (id) => ({ + background: subTab === id ? BRAND.primary : 'transparent', + color: subTab === id ? '#fff' : '#5F6B6D', + border: `1px solid ${subTab === id ? BRAND.primary : '#E4DDD2'}`, + borderRadius: 8, padding: '6px 16px', fontSize: 12, fontWeight: 600, + cursor: 'pointer', fontFamily: 'Vazirmatn,sans-serif', transition: 'all 0.15s', + }); + + return ( +
+
+ {tabs.map(t => )} +
+ + {subTab === 'engagement' ? : ( + <> + {(loading || error || !data) ? : ( +
+
+ {[['نظرها', data.comments], ['ذخیره‌ها', data.saved], ['مشترکان خبرنامه', data.subscribers]].map(([label, value]) => ( +
+
{label}
+
{Number(value ?? 0).toLocaleString('fa-IR')}
+
+ ))} +
+
+

مقاله‌های برتر

+ {(data.topArticles ?? []).length === 0 ? : data.topArticles.map((article) => ( +
+ {article.title} + {Number(article.views ?? 0).toLocaleString('fa-IR')} بازدید +
+ ))} +
+
+ )} + + )} +
+ ); +} + +function JobsPanel() { + const [items, setItems] = useState([]); + const [filter, setFilter] = useState(''); + const [loading, setLoading] = useState(true); + const [actionMsg, setActionMsg] = useState(''); + + const load = (status = filter) => { + setLoading(true); + adminApi.listJobs(status ? { status } : {}).then((data) => { + setItems(data); + setLoading(false); + }).catch(() => setLoading(false)); + }; + + useEffect(() => { load(); }, []); + + const moderate = async (id, status) => { + await adminApi.moderateJob(id, { status }); + setActionMsg(`وضعیت به "${status}" تغییر یافت`); + setTimeout(() => setActionMsg(''), 2500); + load(); + }; + + const statusLabel = { pending: 'در انتظار', approved: 'تأیید شده', rejected: 'رد شده' }; + const statusColor = { pending: '#D49A2A', approved: '#0F6B73', rejected: '#C46A4D' }; + + return ( +
+
+ + {actionMsg && {actionMsg}} +
+ {loading &&
در حال بارگذاری...
} + {!loading && items.length === 0 &&
هیچ آگهی یافت نشد
} + {!loading && items.length > 0 && ( +
+ + + + {['شرکت', 'عنوان', 'موقعیت', 'وضعیت', 'تاریخ', 'عملیات'].map((h) => ( + + ))} + + + + {items.map((job) => ( + + + + + + + + + ))} + +
{h}
{job.company}{job.title}{job.location}{job.remote ? ' (ریموت)' : ''} + + {statusLabel[job.status] ?? job.status} + + {new Date(job.createdAt).toLocaleDateString('fa-IR')} +
+ {job.status !== 'approved' && ( + + )} + {job.status !== 'rejected' && ( + + )} +
+
+
+ )} +
+ ); +} + +function ClientErrorsPanel() { + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + adminApi.getClientErrors().then((d) => { setData(d); setLoading(false); }).catch(() => setLoading(false)); + }, []); + + if (loading) return
در حال بارگذاری...
; + if (!data) return
خطا در بارگذاری اطلاعات
; + + const maxCount = Math.max(...(data.topErrors?.map((e) => e.count) ?? [1]), 1); + + return ( +
+ {data.dailyCounts?.length > 0 && ( +
+
خطاها در ۳۰ روز گذشته
+
+ {data.dailyCounts.slice(-30).map((d) => { + const h = Math.max(4, Math.round((d.count / maxCount) * 56)); + return ( +
+ ); + })} +
+
+ )} + +
+
پرتکرارترین خطاها
+ {(data.topErrors ?? []).length === 0 &&
خطایی ثبت نشده است
} + {(data.topErrors ?? []).map((err, i) => ( +
+ {err.message} +
+ {err.count} بار + {new Date(err.lastSeen).toLocaleDateString('fa-IR')} +
+
+ ))} +
+ +
+
آخرین خطاهای کلاینت
+ {(data.recent ?? []).length === 0 &&
خطایی ثبت نشده است
} + {(data.recent ?? []).map((err) => ( +
+
{err.message}
+
+ {err.url && {err.url}} + {new Date(err.createdAt).toLocaleDateString('fa-IR')} +
+
+ ))} +
+
+ ); +} + +export { SeoAuditPanel, EngagementAnalyticsPanel, Phase9AnalyticsPanel, JobsPanel, ClientErrorsPanel };