diff --git a/app/teknav-articles.jsx b/app/teknav-articles.jsx new file mode 100644 index 0000000..72a589f --- /dev/null +++ b/app/teknav-articles.jsx @@ -0,0 +1,1145 @@ +import { useState, useEffect, useRef, useMemo, useCallback } from 'react'; +import { TeknavData } from './teknav-data.js'; +import { + useNav, useToast, useAuth, + AuthorAvatar, CategoryBadge, TypeBadge, StatusBadge, EmptyState, Header, ReadingProgress, + ReadingListModal, + inputStyle, pillBtn, iconBtnStyle, useViewportPrefetch, +} from './teknav-ui.jsx'; +import { LazyDiagram } from './src/components/lazy-diagram.jsx'; +import { ArticleCard } from './teknav-home.jsx'; +import { contentApi } from './src/lib/content-api.js'; +import { engagementApi } from './src/lib/engagement-api.js'; +import { QuoteSharer } from './src/components/QuoteSharer.jsx'; +import { iconNode } from './src/lib/icon.jsx'; +import crownIcon from './src/assets/icons/crown.svg'; +import thumbsUpIcon from './src/assets/icons/thumbs-up.svg'; +import starIcon from './src/assets/icons/star.svg'; +import heartIcon from './src/assets/icons/heart.svg'; +import flameIcon from './src/assets/icons/flame.svg'; +import lightbulbIcon from './src/assets/icons/lightbulb.svg'; + +function articleDateValue(article) { + return article?.dateEn || article?.publishedAt || article?.date || ''; +} + +function articleDateLabel(article) { + return article?.date || (typeof article?.publishedAt === 'string' ? article.publishedAt.slice(0, 10) : '') || article?.dateEn || ''; +} + +const TEKRAG_SLUG = 'tekrag-persian-data-centric-rag-benchmark'; +const isTekRagArticle = (articleOrSlug) => (typeof articleOrSlug === 'string' ? articleOrSlug : articleOrSlug?.slug) === TEKRAG_SLUG; + +function numberLabel(value, english = false) { + return Number(value ?? 0).toLocaleString(english ? 'en-US' : 'fa-IR'); +} + +function ArticleRow({ article, idx }) { + const { navigate } = useNav(); + const [vis, setVis] = useState(false); + const ref = useRef(); + useViewportPrefetch(ref, article.slug); + const cat = TeknavData.categories.find(c => c.id === article.category); + const author = TeknavData.authors.find(a => a.id === article.authorId); + + useEffect(() => { + const obs = new IntersectionObserver(([e]) => { if (e.isIntersecting) setVis(true); }, { threshold: 0.05 }); + if (ref.current) obs.observe(ref.current); + return () => obs.disconnect(); + }, []); + + return ( +
navigate('/article/' + article.slug)} + onMouseEnter={e => { e.currentTarget.style.opacity = '0.85'; }} + onMouseLeave={e => { e.currentTarget.style.opacity = '1'; }}> +
+
+ {articleDateLabel(article)} + · + + +
+

{article.title}

+ {article.subtitle &&
{article.subtitle}
} +
+ + {author?.specialty && · } + {author?.specialty && {author.specialty}} +
+

{article.summary}

+
+ {article.type} + · + {article.readTime} دقیقه مطالعه + · + {(article.views || 0).toLocaleString('fa')} بازدید + {article.tags?.slice(0, 2).map(t => ( + {t} + ))} +
+
+
+
+ +
+
+
+ ); +} + +function FilterBar({ filters, onChange }) { + const cats = [{ id: '', name: 'همه' }, ...TeknavData.categories]; + const types = ['همه', 'تحلیل عمیق', 'راهنمای فنی', 'داده‌نما', 'پژوهش', 'راستی‌آزمایی‌شده', 'پرونده']; + const sorts = [{ v: 'latest', l: 'جدیدترین' }, { v: 'popular', l: 'پربازدیدترین' }, { v: 'readtime', l: 'زمان مطالعه' }]; + + const chip = (active) => ({ + border: active ? '1.5px solid #0F6B73' : '1px solid #E4DDD2', + background: active ? '#0F6B7312' : '#fff', + color: active ? '#0F6B73' : '#5F6B6D', + padding: '5px 12px', borderRadius: 20, fontSize: 12, cursor: 'pointer', + fontFamily: 'Vazirmatn,sans-serif', fontWeight: active ? 700 : 400, + transition: 'all 0.15s', whiteSpace: 'nowrap', + }); + + return ( +
+
+ دسته: + {cats.map(c => ( + + ))} +
+
+ نوع: + {types.map(t => ( + + ))} +
+
+
+ مرتب‌سازی: + {sorts.map(s => ( + + ))} +
+ {filters.q && ( +
+ جستجو: «{filters.q}» + +
+ )} +
+
+ ); +} + +function ArticleListPage({ initQ = '' }) { + const [filters, setFilters] = useState({ cat: '', type: '', sort: 'latest', q: initQ }); + const [searchInput, setSearchInput] = useState(initQ); + const [page, setPage] = useState(1); + const [allArticles, setAllArticles] = useState(() => contentApi.fallbackArticles()); + const PER = 8; + + useEffect(() => { + let cancelled = false; + contentApi.listArticles({ limit: 100 }) + .then((res) => { if (!cancelled) setAllArticles(res.items ?? []); }) + .catch(() => {}); + return () => { cancelled = true; }; + }, []); + + const filtered = useMemo(() => { + let list = allArticles; + if (filters.q) { + const q = filters.q.toLowerCase(); + list = list.filter(a => a.title.toLowerCase().includes(q) || a.summary.toLowerCase().includes(q) || a.authorName?.toLowerCase().includes(q) || a.categoryName?.toLowerCase().includes(q)); + } + if (filters.cat) list = list.filter(a => a.category === filters.cat); + if (filters.type) list = list.filter(a => a.type === filters.type); + if (filters.sort === 'popular') list = [...list].sort((a, b) => b.views - a.views); + else if (filters.sort === 'readtime') list = [...list].sort((a, b) => b.readTime - a.readTime); + else list = [...list].sort((a, b) => articleDateValue(b).localeCompare(articleDateValue(a))); + return list; + }, [filters, allArticles]); + + const visible = filtered.slice(0, page * PER); + const hasMore = visible.length < filtered.length; + + const doSearch = (e) => { e.preventDefault(); setFilters(f => ({ ...f, q: searchInput })); setPage(1); }; + + return ( +
+
+
+

همه مقاله‌ها

+

{filtered.length} مقاله یافت شد

+
+ setSearchInput(e.target.value)} + placeholder="جستجو در عنوان، خلاصه، نویسنده..." + style={{ flex: 1, ...inputStyle }} dir="rtl" /> + +
+
+ { setFilters(f); setPage(1); }} /> + + {visible.length === 0 ? ( + + ) : ( + <> + {visible.map((a, i) => )} + {hasMore && ( +
+ +
+ )} + + )} +
+
+ ); +} + +function TableOfContents({ headings, english = false }) { + const [active, setActive] = useState(null); + useEffect(() => { + const obs = new IntersectionObserver(entries => { + entries.forEach(e => { if (e.isIntersecting) setActive(e.target.id); }); + }, { rootMargin: '-20% 0px -70% 0px' }); + headings.forEach(h => { const el = document.getElementById(h.id); if (el) obs.observe(el); }); + return () => obs.disconnect(); + }, [headings]); + + return ( + + ); +} + +function EmbeddedDiagramCard({ title, subtitle, type, english = false }) { + return ( +
+
+
{title}
+ {subtitle &&
{subtitle}
} +
+ +
+ ); +} + +function ArticleEmbeddedDiagrams({ slug }) { + if (isTekRagArticle(slug)) { + return null; + } + if (slug === 'ai-clean-data') { + return ( + + ); + } + if (slug === 'ai-agents-2026' || slug === 'agentic-ai-production') { + return ( +
+ + + + +
+ ); + } + return null; +} + +function SeriesNavBanner({ series, currentSlug }) { + const { navigate } = useNav(); + const idx = series.articles.findIndex(a => a.slug === currentSlug); + const prev = idx > 0 ? series.articles[idx - 1] : null; + const next = idx < series.articles.length - 1 ? series.articles[idx + 1] : null; + if (!prev && !next) return null; + const btnBase = { + background: 'transparent', border: '1px solid #0F6B7330', borderRadius: 8, + padding: '10px 16px', cursor: 'pointer', fontFamily: 'Vazirmatn,sans-serif', + color: '#0F6B73', fontSize: 13, fontWeight: 600, maxWidth: 280, textAlign: 'right', + transition: 'all 0.15s', lineHeight: 1.5, + }; + return ( +
+ {prev ? ( + + ) :
} + {next ? ( + + ) :
} +
+ ); +} + +function ArticleQaSection({ slug, english = false }) { + const { user } = useAuth(); + const [items, setItems] = useState([]); + const [loading, setLoading] = useState(true); + const [body, setBody] = useState(''); + const [replyTo, setReplyTo] = useState(null); + const toast = useToast(); + + const load = useCallback(() => { + setLoading(true); + engagementApi.listQa(slug) + .then(res => setItems(res.items ?? [])) + .finally(() => setLoading(false)); + }, [slug]); + + useEffect(() => { load(); }, [load]); + + const submit = async (e) => { + e.preventDefault(); + if (!body.trim()) return; + try { + if (replyTo) await engagementApi.postAnswer(replyTo.id, body); + else await engagementApi.postQuestion(slug, body); + setBody(''); + setReplyTo(null); + toast(english ? 'Submitted' : 'ثبت شد'); + load(); + } catch { + toast(english ? 'Submission failed' : 'خطا در ثبت', 'error'); + } + }; + + return ( +
+

{english ? 'Questions and Answers' : 'پرسش و پاسخ'}

+ {user ? ( +
+ {replyTo &&
{english ? 'Replying to' : 'پاسخ به'}: {replyTo.author?.name}
} +