From 9be4f5b89336b814ef7ae76f95a0b2c163265e84 Mon Sep 17 00:00:00 2001 From: arsam Date: Fri, 18 Sep 2026 23:25:53 +0330 Subject: [PATCH] Import source: app/src/pages/TopicHubPage Import the existing local project snapshot. This commit organizes existing files; it does not represent newly developed functionality. --- app/src/pages/TopicHubPage.jsx | 166 +++++++++++++++++++++++++++++++++ 1 file changed, 166 insertions(+) create mode 100644 app/src/pages/TopicHubPage.jsx diff --git a/app/src/pages/TopicHubPage.jsx b/app/src/pages/TopicHubPage.jsx new file mode 100644 index 0000000..c14c6c1 --- /dev/null +++ b/app/src/pages/TopicHubPage.jsx @@ -0,0 +1,166 @@ +import { useState, useEffect } from 'react'; +import { TeknavData } from '../../teknav-data.js'; +import { useNav, useToast, useAuth, AuthorAvatar, EmptyState, pillBtn } from '../../teknav-ui.jsx'; +import { ArticleRow } from '../../teknav-articles.jsx'; +import { contentApi } from '../../src/lib/content-api.js'; +import { engagementApi, pushApi } from '../../src/lib/engagement-api.js'; +import { authorProfilePath } from './shared.jsx'; + +function toDateKey(article) { + return article?.dateEn || article?.publishedAt || article?.date || ''; +} + +export 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 }); + }); + 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}

+
+ ))} +
+
+
+ ); +}