From dacbf7f9eb5ee6ea3e461c6856ad729e5b428300 Mon Sep 17 00:00:00 2001 From: arsam Date: Fri, 18 Sep 2026 23:26:24 +0330 Subject: [PATCH] Import source: app/teknav-pages Import the existing local project snapshot. This commit organizes existing files; it does not represent newly developed functionality. --- app/teknav-pages.jsx | 2027 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 2027 insertions(+) create mode 100644 app/teknav-pages.jsx diff --git a/app/teknav-pages.jsx b/app/teknav-pages.jsx new file mode 100644 index 0000000..bad110d --- /dev/null +++ b/app/teknav-pages.jsx @@ -0,0 +1,2027 @@ +// 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" /> + +