From 3ed1386d353bf61a267926abae5c7270cb130029 Mon Sep 17 00:00:00 2001 From: arsam Date: Fri, 18 Sep 2026 23:24:17 +0330 Subject: [PATCH] Import source: app/src/admin/WriterWorkspace Import the existing local project snapshot. This commit organizes existing files; it does not represent newly developed functionality. --- app/src/admin/WriterWorkspace.jsx | 657 ++++++++++++++++++++++++++++++ 1 file changed, 657 insertions(+) create mode 100644 app/src/admin/WriterWorkspace.jsx diff --git a/app/src/admin/WriterWorkspace.jsx b/app/src/admin/WriterWorkspace.jsx new file mode 100644 index 0000000..d1e20b5 --- /dev/null +++ b/app/src/admin/WriterWorkspace.jsx @@ -0,0 +1,657 @@ +import { useState, useEffect, useRef, useCallback, Fragment } from 'react'; +import { useAuth, useNav, useToast, StatusBadge, EmptyState, inputStyle, pillBtn } from '../../teknav-ui.jsx'; +import { adminApi } from '../lib/admin-api.js'; +import { engagementApi } from '../lib/engagement-api.js'; +import triangleAlertIcon from '../assets/icons/triangle-alert.svg'; +import { PanelState, actionBtn } from './Shared.jsx'; +import { ArticleEditor } from './ArticleEditor.jsx'; +import { ArticlesManagement } from './ArticleList.jsx'; + +const WW = { + terra: '#C46A4D', terraLight: '#FAF0EB', terraBorder: 'rgba(196,106,77,0.22)', + gold: '#C8951C', goldLight: '#FBF6E8', + ink: '#1A2E30', muted: '#7B8C8E', faint: '#9AA5A6', + cream: '#FAF7F0', border: '#E4DDD2', + success: '#3A7D5E', successLight: '#EFF8F4', + purple: '#7B5EA7', purpleLight: '#F5F0FB', +}; +const wwPrimaryBtn = { + padding: '10px 22px', borderRadius: 10, border: 'none', + background: WW.terra, color: '#fff', fontSize: 13, fontWeight: 700, + cursor: 'pointer', fontFamily: 'Vazirmatn,sans-serif', + boxShadow: '0 3px 12px rgba(196,106,77,0.25)', + display: 'inline-flex', alignItems: 'center', gap: 6, +}; +const wwGhostBtn = { + padding: '9px 16px', borderRadius: 10, border: `1px solid ${WW.border}`, + background: '#fff', color: WW.ink, fontSize: 13, fontWeight: 600, + cursor: 'pointer', fontFamily: 'Vazirmatn,sans-serif', +}; +const wwDangerBtn = { + padding: '9px 16px', borderRadius: 10, border: 'none', + background: 'rgba(201,76,76,0.08)', color: '#C94C4C', + fontSize: 13, fontWeight: 600, cursor: 'pointer', fontFamily: 'Vazirmatn,sans-serif', +}; +const wwLinkBtn = { + background: WW.terraLight, border: 'none', cursor: 'pointer', + color: WW.terra, fontSize: 11, fontWeight: 700, + fontFamily: 'Vazirmatn,sans-serif', padding: '3px 10px', borderRadius: 6, +}; + +const WRITING_TIPS = [ + { icon: '✦', tip: 'با یک سوال باز شروع کنید. بهترین مقاله‌ها خواننده را وارد گفتگو می‌کنند.' }, + { icon: '◈', tip: 'عنوان را آخر بنویسید. وقتی محتوا کامل است، عنوان دقیق‌تر انتخاب می‌شود.' }, + { icon: '◉', tip: 'یک مفهوم پیچیده را با یک مثال واقعی توضیح دهید تا خواننده آن را به خاطر بسپارد.' }, + { icon: '◇', tip: 'پاراگراف اول تعیین‌کننده است. اگر خواننده را نگه ندارد، ادامه را نمی‌خواند.' }, + { icon: '◎', tip: 'از اعداد استفاده کنید. "۳ روش" قابل اعتمادتر از "چند روش" به نظر می‌رسد.' }, + { icon: '✦', tip: 'مقاله را با صدای بلند بخوانید. اگر گیر کردید، جمله نیاز به بازنویسی دارد.' }, +]; + +const WEEK_DAYS = ['ش', 'ی', 'د', 'س', 'چ', 'پ', 'ج']; + +function WritingTipCard() { + const [idx, setIdx] = useState(() => Math.floor(Math.random() * WRITING_TIPS.length)); + const [fading, setFading] = useState(false); + const timerRef = useRef(null); + + const advance = useCallback((step = 1) => { + setFading(true); + clearInterval(timerRef.current); + setTimeout(() => { + setIdx(i => (i + step + WRITING_TIPS.length) % WRITING_TIPS.length); + setFading(false); + timerRef.current = setInterval(autoAdvance, 7000); + }, 350); + }, []); + + function autoAdvance() { advance(1); } + + useEffect(() => { + timerRef.current = setInterval(autoAdvance, 7000); + return () => clearInterval(timerRef.current); + }, []); + + const { icon, tip } = WRITING_TIPS[idx]; + return ( +
+
{icon}
+
+
+
+ {'نکته نویسندگی'} +
+
+ + {idx + 1}/{WRITING_TIPS.length} + + + +
+
+

{tip}

+
+
+ ); +} + +function WeeklyChart({ data }) { + const values = (data && data.length === 7) ? data : [2, 5, 3, 8, 4, 6, 1]; + const max = Math.max(...values, 1); + const today = new Date().getDay(); + return ( +
+
+ {values.map((v, i) => ( +
+
+
+ ))} +
+
+ {WEEK_DAYS.map((d, i) => ( +
{d}
+ ))} +
+
+ ); +} + +function WriterAnalyticsPanel() { + const [articles, setArticles] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(false); + const [expandedSlug, setExpandedSlug] = useState(null); + const [sparklineData, setSparklineData] = useState({}); + + useEffect(() => { + let cancelled = false; + engagementApi.writerAnalyticsArticles() + .then(res => { if (!cancelled) setArticles(res.items ?? []); }) + .catch(() => { if (!cancelled) setError(true); }) + .finally(() => { if (!cancelled) setLoading(false); }); + return () => { cancelled = true; }; + }, []); + + useEffect(() => { + if (articles.length === 0) return; + let cancelled = false; + articles.forEach(a => { + engagementApi.writerAnalyticsArticle(a.slug) + .then(res => { + if (!cancelled) setSparklineData(prev => ({ ...prev, [a.slug]: res.viewsByDay.map(d => d.views) })); + }) + .catch(() => {}); + }); + return () => { cancelled = true; }; + }, [articles]); + + const toggleSparkline = (slug) => { + setExpandedSlug(prev => prev === slug ? null : slug); + }; + + if (loading) return
در حال بارگذاری تحلیل‌ها...
; + if (error) return ; + if (articles.length === 0) return ; + + return ( +
+

عملکرد نوشته‌های من

+ +
+ + + + + + + + + + + + {articles.map((a, i) => { + const spark = sparklineData[a.slug]; + const max = spark ? Math.max(...spark, 1) : 1; + return ( + + toggleSparkline(a.slug)}> + + + + + + + {expandedSlug === a.slug && ( + + + + )} + + ); + })} + + + + + + + + + +
عنوان مقالهبازدیدواکنشنظرروند اخیر
{a.title}{Number(a.views || 0).toLocaleString('fa-IR')}{Number(a.reactions || 0).toLocaleString('fa-IR')}{Number(a.comments || 0).toLocaleString('fa-IR')} + {(() => { + const s = sparklineData[a.slug]; + if (!s) return ...; + if (s.length < 2) return —; + const W = 80, H = 20; + const mx = Math.max(...s, 1); + const pts = s.map((v, si) => [ + (si / (s.length - 1)) * W, + H - 2 - (v / mx) * (H - 4), + ]); + const d = pts.map(([x, y], si) => `${si === 0 ? 'M' : 'L'}${x.toFixed(1)},${y.toFixed(1)}`).join(' '); + const last = pts[pts.length - 1]; + return ( + + + + + ); + })()} +
{expandedSlug === a.slug ? '▴' : '▾'}
+
+ {spark ? ( + spark.length < 2 ? ( +
داده کافی برای رسم نمودار ۳۰ روزه وجود ندارد.
+ ) : (() => { + const W = 560, H = 80, pad = 4; + const n = spark.length; + const pts = spark.map((v, si) => [ + (si / (n - 1)) * (W - pad * 2) + pad, + H - pad - (v / max) * (H - pad * 2 - 4), + ]); + const line = pts.map(([x, y], si) => `${si === 0 ? 'M' : 'L'} ${x.toFixed(1)},${y.toFixed(1)}`).join(' '); + const area = `${line} L ${pts[pts.length-1][0].toFixed(1)},${H} L ${pts[0][0].toFixed(1)},${H} Z`; + return ( +
+ + + + + + + + + + {pts[pts.length - 1] && ( + + )} + +
+ ۳۰ روز پیش + امروز +
+
+ ); + })() + ) : ( +
در حال بارگذاری روند...
+ )} +
جمع کل + {articles.reduce((s, a) => s + (a.views || 0), 0).toLocaleString('fa-IR')} + + {articles.reduce((s, a) => s + (a.reactions || 0), 0).toLocaleString('fa-IR')} + + {articles.reduce((s, a) => s + (a.comments || 0), 0).toLocaleString('fa-IR')} + +
+
+
+ ); +} + +function WriterWorkspace() { + const { user, logout } = useAuth(); + const { navigate } = useNav(); + const [tab, setTab] = useState('overview'); + const [editingArticle, setEditingArticle] = useState(null); + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(false); + + const load = useCallback(() => { + setError(false); + return adminApi.getWriterDashboard() + .then(setData) + .catch(() => { setData(null); setError(true); }) + .finally(() => setLoading(false)); + }, []); + + useEffect(() => { if (!user) navigate('/login'); }, [user, navigate]); + useEffect(() => { + if (!user) return undefined; + let cancelled = false; + load(); + const timer = setInterval(() => { if (!cancelled) load(); }, 20000); + return () => { cancelled = true; clearInterval(timer); }; + }, [load, user]); + + if (!user) return null; + + const stats = data?.stats ?? {}; + const openEditor = (article = null) => { setEditingArticle(article); setTab('write'); }; + + const tabs = [ + { id: 'overview', label: 'آمار من', icon: '◈' }, + { id: 'analytics', label: 'تحلیل‌ها', icon: '▤' }, + { id: 'write', label: editingArticle ? 'ویرایش' : 'نوشتن', icon: '✦' }, + { id: 'articles', label: 'نوشته‌ها', icon: '◉' }, + { id: 'comments', label: 'نظرها', icon: '◎' }, + ]; + + const statCards = [ + { label: 'بازدید', value: stats.views, color: WW.terra, bg: WW.terraLight, icon: '◉' }, + { label: 'دنبال‌کننده', value: stats.followers, color: WW.success, bg: WW.successLight, icon: '◎' }, + { label: 'نظر', value: stats.comments, color: WW.gold, bg: WW.goldLight, icon: '◇' }, + { label: 'در انتظار بررسی', value: stats.pending, color: WW.purple, bg: WW.purpleLight, icon: '◈' }, + { label: 'پیش‌نویس', value: stats.drafts, color: WW.muted, bg: '#F2F4F4', icon: '◻' }, + ]; + + const renderOverview = () => ( +
+ + {data && ( + <> + {(stats.needsRevision > 0) && ( +
+
+
+
+ {Number(stats.needsRevision).toLocaleString('fa-IR')} {'مقاله نیاز به بازبینی دارد'} +
+
+ {'ویراستار بازخوردی ثبت کرده است. برای مشاهده توضیحات به بخش «نوشته‌ها» بروید.'} +
+
+ +
+ )} + + + +
+ {statCards.map(({ label, value, color, bg, icon }) => ( +
+
+
{icon}
+
+ {Number(value ?? 0).toLocaleString('fa-IR')} +
+
{label}
+
+ ))} +
+ +
+
+
+

+ {'آخرین نوشته‌ها'} +

+ +
+ {(data.recentArticles ?? []).length === 0 + ? + : data.recentArticles.map(article => ( + + ))} +
+ +
+
+

+ {'نظرهای تازه'} +

+ +
+ {(data.recentComments ?? []).length === 0 + ? + : data.recentComments.slice(0, 5).map(comment => ( +
+
+
{comment.author?.name}
+
{comment.time}
+
+
{comment.body}
+
{comment.article?.title}
+
+ ))} +
+
+ +
+
+
+ {'فعالیت هفتگی'} +
+ d.views ?? 0)} /> +
+ {'بازدید روزانه در این هفته'} +
+
+ +
+
+ {'هدف ماهانه'} +
+
+ {'این ماه: '}{Number(stats.published ?? 0).toLocaleString('fa-IR')} + {' از ۴ مقاله'} +
+
+
+
+
+ {[1, 2, 3, 4].map(n => ( +
{n <= (stats.published ?? 0) ? '✔' : n.toLocaleString('fa-IR')}
+ ))} +
+
+ {'هر ماه ۴ مقاله بنویسید'} +
+
+
+ + )} +
+ ); + + return ( +
+
+
+
+
+
+ {'✦ استودیوی نویسنده · تکناو'} +
+

+ {(() => { + const h = new Date().getHours(); + if (h < 6) return 'شب بخیر، '; + if (h < 12) return 'صبح بخیر، '; + if (h < 17) return 'ظهر بخیر، '; + if (h < 21) return 'عصر بخیر، '; + return 'شب بخیر، '; + })()}{user.name} +

+

+ {(() => { + const h = new Date().getHours(); + if (h < 6) return 'ساعت کار است یا الهام گرفتن؟'; + if (h < 12) return 'صبح بهترین وقت نوشتن است.'; + if (h < 17) return 'امروز چه می‌نویسید؟'; + return 'شب‌های خلاق را به یاد داشته باشید.'; + })()} +

+
+
+ + + + +
+
+ +
+ {tabs.map(({ id, label, icon }) => ( + + ))} +
+
+
+ +
+ {tab === 'write' ? ( + { setEditingArticle(null); setTab('overview'); load(); }} /> + ) : tab === 'articles' ? ( + + ) : tab === 'analytics' ? ( + + ) : tab === 'comments' ? ( +
+
+

+ {'نظرها روی نوشته‌های من'} +

+ {(data?.recentComments ?? []).length > 0 && ( + + {(data.recentComments.length).toLocaleString('fa-IR')} {'نظر اخیر'} + + )} +
+ + {(data?.recentComments ?? []).map(comment => ( +
+
+
+ {comment.author?.name} + + {comment.article?.title?.slice(0, 28)}{comment.article?.title?.length > 28 ? '…' : ''} + +
+ {comment.time} +
+

{comment.body}

+ +
+ ))} + {(data?.recentComments ?? []).length >= 8 && ( +
+ فقط ۸ نظر اخیر نمایش داده می‌شود. برای مشاهده همه نظرها روی مقاله مربوطه بروید. +
+ )} +
+ ) : renderOverview()} +
+ + +
+ ); +} + +export { WriterWorkspace };