// teknav-pages.jsx — Search, Category, Authors pages (ES module) import { useState, useMemo, useEffect, useCallback } from 'react'; import { TeknavData } from './teknav-data.js'; function authorProfilePath(author) { const username = author.username || TeknavData.authors.find(a => a.slug === author.slug)?.username; return username ? `/profile/@${username}` : `/author/${author.slug}`; } import { useNav, useToast, useAuth, AuthorAvatar, CategoryBadge, TypeBadge, EmptyState, SkeletonLoader, inputStyle, pillBtn, } from './teknav-ui.jsx'; import { DiagramRenderer } from './teknav-diagrams.jsx'; import { ArticleRow } from './teknav-articles.jsx'; import { contentApi } from './src/lib/content-api.js'; import { engagementApi, pushApi } from './src/lib/engagement-api.js'; // ── Search Page ───────────────────────────────────────────────────────────── function SearchPage({ initQ = '' }) { const [q, setQ] = useState(initQ); const [input, setInput] = useState(initQ); const [filters, setFilters] = useState({ category: '', author: '', type: '', tag: '', dateRange: '' }); const [items, setItems] = useState([]); const [loading, setLoading] = useState(false); const { navigate } = useNav(); const load = useCallback(() => { setLoading(true); const params = { q, category: filters.category, author: filters.author, type: filters.type, tag: filters.tag, limit: 50, }; const apiCall = q ? contentApi.search(params) : contentApi.listArticles(params); apiCall .then(res => setItems(res.items ?? [])) .finally(() => setLoading(false)); }, [q, filters]); useEffect(() => { load(); }, [load]); const doSearch = (e) => { e.preventDefault(); setQ(input); }; const highlightText = (text, query) => { if (!query) return text; const parts = text.split(new RegExp(`(${query})`, 'gi')); return parts.map((part, i) => part.toLowerCase() === query.toLowerCase() ? `${part}` : part ).join(''); }; return (

جستجوی پیشرفته

setInput(e.target.value)} placeholder="جستجو در مقاله‌ها..." style={{ flex: 1, ...inputStyle, fontSize: 16 }} dir="rtl" autoFocus />
{loading ? : ( <> {items.length === 0 ? : (
{items.length.toLocaleString('fa-IR')} نتیجه پیدا شد
{items.map((a, i) => )}
)} )}
); } function SearchResult({ article, q, highlight, idx }) { const { navigate } = useNav(); return (
navigate('/article/' + article.slug)} style={{ padding: '24px 0', borderBottom: '1px solid #E4DDD2', cursor: 'pointer', opacity: 0, animation: `fadeIn 0.3s ${idx * 40}ms forwards`, }}>
{article.date} · {article.readTime} دقیقه

); } // ── Category Page ─────────────────────────────────────────────────────────── function CategoryPage({ slug }) { const { navigate } = useNav(); const [cat, setCat] = useState(() => TeknavData.categories.find(c => c.slug === slug)); const [articles, setArticles] = useState(() => contentApi.fallbackArticles().filter(a => a.category === slug)); useEffect(() => { let cancelled = false; setCat(TeknavData.categories.find(c => c.slug === slug)); setArticles(contentApi.fallbackArticles().filter(a => a.category === slug)); Promise.all([ contentApi.getCategory(slug).catch(() => null), contentApi.listArticles({ category: slug, limit: 100 }).catch(() => null), ]).then(([category, result]) => { if (cancelled) return; if (category) setCat({ ...(TeknavData.categories.find(c => c.slug === slug) ?? {}), ...category, id: category.slug }); if (result?.items) setArticles(result.items); }); return () => { cancelled = true; }; }, [slug]); if (!cat) return ; return (
{/* Category Hero */} {(() => { const HERO_IMAGES = { ai: '/images/category-hero-ai-v2.jpg', startup: '/images/category-hero-startup.jpg', hardware: '/images/category-hero-hardware.jpg', security: '/images/category-hero-security.jpg', software: '/images/category-hero-software.jpg', future: '/images/category-hero-future.jpg', data: '/images/category-hero-data.jpg', }; const heroImg = HERO_IMAGES[cat.slug]; return (
{/* Background: photo for AI, soft tinted gradient for others */} {heroImg ? ); } // ── Authors List Page ──────────────────────────────────────────────────────── function TagPage({ tag }) { const decoded = decodeURIComponent(tag); const [articles, setArticles] = useState(() => contentApi.fallbackArticles().filter((item) => item.tags?.includes(decoded))); useEffect(() => { let cancelled = false; setArticles(contentApi.fallbackArticles().filter((item) => item.tags?.includes(decoded))); contentApi.listArticles({ tag: decoded, limit: 100 }) .then((res) => { if (!cancelled) setArticles(res.items ?? []); }) .catch(() => {}); return () => { cancelled = true; }; }, [decoded]); return (

برچسب {decoded}

همه مقاله‌هایی که با این برچسب منتشر شده‌اند.

{articles.length === 0 ? : articles.map((article, index) => ( ))}
); } function GlossaryPage({ slug }) { const { navigate } = useNav(); const term = TeknavData.glossary?.find((item) => item.slug === slug); const articles = contentApi.fallbackArticles().filter((article) => ( article.tags?.some((tag) => tag.toLowerCase() === term?.term?.toLowerCase() || tag.toLowerCase() === term?.english?.toLowerCase()) || article.summary?.toLowerCase().includes(term?.english?.toLowerCase() || '') || article.title?.toLowerCase().includes(term?.english?.toLowerCase() || '') )).slice(0, 6); if (!term) return ; const related = (term.related ?? []).map((item) => TeknavData.glossary?.find((g) => g.slug === item)).filter(Boolean); const topic = TeknavData.topicHubs?.find((item) => item.slug === term.topic); return (
فرهنگ واژگان تکنّاو

{term.term}

{term.english}

{term.summary}

پرسش‌های رایج

{(term.faqs ?? []).map(([q, a]) => (
{q}

{a}

))}

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

{articles.length === 0 ? : articles.map((article, index) => )}
); } function toDateKey(article) { return article?.dateEn || article?.publishedAt || article?.date || ''; } function AuthorsListPage() { const { navigate } = useNav(); const [authors, setAuthors] = useState(() => contentApi.fallbackAuthors()); const [articles, setArticles] = useState(() => contentApi.fallbackArticles()); useEffect(() => { let cancelled = false; Promise.all([ contentApi.listAuthors().catch(() => null), contentApi.listArticles({ limit: 100 }).catch(() => null), ]).then(([authorRows, articleRows]) => { if (cancelled) return; if (authorRows) setAuthors(authorRows); if (articleRows?.items) setArticles(articleRows.items); }); return () => { cancelled = true; }; }, []); return (
{/* Authors Hero */}
{authors.map(author => { const latestArt = articles.find(a => a.authorId === author.id || a.authorSlug === author.slug); return (
navigate(authorProfilePath(author))} style={{ background: '#fff', border: '1px solid #E4DDD2', borderRadius: 14, padding: 24, cursor: 'pointer', direction: 'rtl', transition: 'all 0.2s', }} onMouseEnter={e => { e.currentTarget.style.transform = 'translateY(-3px)'; e.currentTarget.style.boxShadow = '0 10px 32px rgba(0,0,0,0.08)'; }} onMouseLeave={e => { e.currentTarget.style.transform = 'translateY(0)'; e.currentTarget.style.boxShadow = 'none'; }}>
{author.name} {author.verifiedExpert && تاییدشده}
{author.specialty}
{author.liveArticleCount ?? author.articleCount} مقاله منتشرشده

{author.bio.slice(0, 120)}…

{latestArt && (
آخرین مقاله: {latestArt.title.slice(0, 50)}…
)}
); })}
); } // ── Author Profile Page ───────────────────────────────────────────────────── function SeriesPage({ slug }) { const { navigate } = useNav(); const [series, setSeries] = useState(null); const [error, setError] = useState(false); useEffect(() => { let cancelled = false; setError(false); contentApi.getSeries(slug) .then((item) => { if (!cancelled) setSeries(item); }) .catch(() => { if (!cancelled) setError(true); }); return () => { cancelled = true; }; }, [slug]); if (error) { return (
); } if (!series) { return
در حال بارگذاری...
; } return (

{series.title}

{series.description &&

{series.description}

} {(series.articles ?? []).length === 0 ? : (
{series.articles.map((item, index) => (
{(item.position ?? index + 1).toLocaleString('fa-IR')}
))}
)}
); } function TopicHubPage({ slug }) { const { navigate } = useNav(); const toast = useToast(); const { user } = useAuth(); const topic = TeknavData.topicHubs.find((item) => item.slug === slug); const category = TeknavData.categories.find((item) => item.slug === topic?.categorySlug); const [articles, setArticles] = useState(() => contentApi.fallbackArticles().filter((item) => item.category === topic?.categorySlug)); const [series, setSeries] = useState([]); const [follow, setFollow] = useState({ following: false, count: 0 }); const [guestPushEnabled, setGuestPushEnabled] = useState(false); const [guestPushBusy, setGuestPushBusy] = useState(false); useEffect(() => { if (!topic) return; let cancelled = false; setArticles(contentApi.fallbackArticles().filter((item) => item.category === topic.categorySlug)); Promise.all([ contentApi.listArticles({ category: topic.categorySlug, limit: 100 }).catch(() => null), contentApi.listSeries().catch(() => []), engagementApi.topicFollowers(topic.slug).catch(() => null), ]).then(([articleRows, seriesRows, followRow]) => { if (cancelled) return; if (articleRows?.items) setArticles(articleRows.items); setSeries((seriesRows ?? []).filter((row) => (row.articles ?? []).some((entry) => entry.article?.category === topic.categorySlug))); if (followRow) setFollow({ following: !!followRow.following, count: followRow.count ?? 0 }); }); // Check if guest push already subscribed if ('PushManager' in window && 'serviceWorker' in navigator) { pushApi.isSubscribed().then(setGuestPushEnabled).catch(() => {}); } return () => { cancelled = true; }; }, [topic?.slug]); if (!topic) { return (
); } const featured = articles.filter((item) => item.featured).slice(0, 3); const latest = articles.slice().sort((a, b) => String(toDateKey(b)).localeCompare(String(toDateKey(a)))).slice(0, 8); const authors = TeknavData.authors.filter((author) => articles.some((article) => article.authorId === author.id || article.authorSlug === author.slug)); const toggleTopicFollow = async () => { if (!user) { toast('برای دنبال کردن موضوع ابتدا وارد شوید', 'warning'); return; } try { const result = follow.following ? await engagementApi.unfollowTopic(topic.slug) : await engagementApi.followTopic(topic.slug); setFollow({ following: !!result.following, count: result.count ?? 0 }); toast(result.following ? 'موضوع دنبال شد' : 'دنبال کردن موضوع لغو شد'); } catch { toast('دنبال کردن موضوع انجام نشد', 'warning'); } }; const toggleGuestPush = async () => { if (!('PushManager' in window) || !('serviceWorker' in navigator)) { toast('مرورگر شما از اعلان‌های وب پشتیبانی نمی‌کند', 'warning'); return; } setGuestPushBusy(true); try { if (guestPushEnabled) { await pushApi.unsubscribeGuest(); setGuestPushEnabled(false); toast('اعلان‌های این موضوع غیرفعال شد'); } else { const perm = typeof Notification !== 'undefined' ? await Notification.requestPermission() : 'denied'; if (perm !== 'granted') { toast('اجازه اعلان داده نشد', 'warning'); return; } await pushApi.subscribeAsGuest([topic.slug]); setGuestPushEnabled(true); toast('اعلان‌های مقالات جدید این موضوع فعال شد'); } } catch { toast('عملیات ناموفق بود', 'error'); } finally { setGuestPushBusy(false); } }; return (

{topic.title}

{topic.intro}

{'PushManager' in window && ( )}

مقاله‌های شاخص

{featured.length === 0 ? : (
{featured.map((article, index) => )}
)}

تازه‌ترین تحلیل‌ها

{latest.length === 0 ? : latest.map((article, index) => )}

سری‌های مرتبط

{series.length === 0 ?

سری مقاله مرتبط پس از تایید سردبیر اینجا نمایش داده می‌شود.

: series.map((item) => ( ))}

نویسندگان مرتبط

{authors.length === 0 ?

نویسنده مرتبطی پیدا نشد.

: authors.map((author) => ( ))}

پرسش‌های رایج

{topic.faqs.map(([question, answer]) => (
{question}

{answer}

))}
); } function AuthorProfilePage({ slug }) { const { navigate } = useNav(); const [author, setAuthor] = useState(null); const [articles, setArticles] = useState([]); const [loading, setLoading] = useState(true); useEffect(() => { // Redirect /author/:slug → /profile/@:username const staticAuthor = TeknavData.authors.find(a => a.slug === slug); const username = staticAuthor?.username || slug; navigate(`/profile/@${username}`); }, [slug]); if (loading) return
در حال بارگذاری…
; if (!author) return ; return (
{/* Profile header */}

{author.name} {author.verifiedExpert && متخصص تاییدشده}

{author.verifiedExpert && author.verificationNote &&
{author.verificationNote}
}
{author.specialty}

{author.bio}

{author.articleCount}مقاله
{articles.reduce((s, a) => s + (a.views || 0), 0).toLocaleString('fa')}بازدید

مقاله‌های {author.name}

{articles.length === 0 ? : articles.map((a, i) => ) }
); } function NewsletterArchivePage() { const { navigate } = useNav(); const [items, setItems] = useState([]); const [loading, setLoading] = useState(true); useEffect(() => { let cancelled = false; contentApi.listNewsletterArchive() .then((rows) => { if (!cancelled) setItems(rows); }) .catch(() => { if (!cancelled) setItems([]); }) .finally(() => { if (!cancelled) setLoading(false); }); return () => { cancelled = true; }; }, []); return (

آرشیو خبرنامه تکنـاو

نسخه‌های ارسال‌شده خبرنامه برای دنبال کردن روندهای مهم فناوری، هوش مصنوعی و امنیت.

{loading ?
در حال بارگذاری...
: items.length === 0 ? : (
{items.map((item) => ( ))}
)}
); } function NewsletterIssuePage({ slug }) { const { navigate } = useNav(); const [issue, setIssue] = useState(null); const [error, setError] = useState(false); useEffect(() => { let cancelled = false; setError(false); contentApi.getNewsletterIssue(slug) .then((row) => { if (!cancelled) setIssue(row); }) .catch(() => { if (!cancelled) setError(true); }); return () => { cancelled = true; }; }, [slug]); if (error) return
; if (!issue) return
در حال بارگذاری...
; return (

{issue.subject}

{issue.sentAt ? new Date(issue.sentAt).toLocaleDateString('fa-IR') : ''}
); } function JobsPage() { const toast = useToast(); const [items, setItems] = useState([]); const [form, setForm] = useState({ company: '', title: '', location: 'Remote', remote: true, url: '', contactEmail: '', description: '' }); const [busy, setBusy] = useState(false); useEffect(() => { contentApi.listJobs().then(setItems).catch(() => setItems([])); }, []); const set = (key, value) => setForm((current) => ({ ...current, [key]: value })); const submit = async (event) => { event.preventDefault(); setBusy(true); try { await contentApi.submitJob(form); setForm({ company: '', title: '', location: 'Remote', remote: true, url: '', contactEmail: '', description: '' }); toast('آگهی شغلی برای بررسی ارسال شد'); } catch { toast('ارسال آگهی ناموفق بود', 'error'); } finally { setBusy(false); } }; return (

فرصت‌های شغلی فناوری

جایگاه‌های منتخب شرکت‌های فنی فارسی‌زبان. آگهی‌های جدید بعد از بررسی تحریریه منتشر می‌شوند.

{items.length === 0 ? : items.map((job) => ( ))}

ثبت آگهی شرکت

set('company', e.target.value)} placeholder="نام شرکت" style={inputStyle} /> set('title', e.target.value)} placeholder="عنوان شغل" style={inputStyle} /> set('location', e.target.value)} placeholder="موقعیت" style={inputStyle} /> set('contactEmail', e.target.value)} placeholder="ایمیل تماس" style={inputStyle} dir="ltr" /> set('url', e.target.value)} placeholder="لینک درخواست" style={inputStyle} dir="ltr" />