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}
+
+ ))}
+
+
+
+ );
+}