صفحه تحلیل

الگوی ساخت صفحه تحلیل اینفلوئنسر با ترکیب کامپوننت‌های دامنه‌ای

صفحهٔ یک حساب: DetailPage

صفحه‌ای که یک حساب یا اینفلوئنسر را نشان می‌دهد DetailPage است: راه بازگشت، وضعیت، زبانه‌های نشانی‌دار، بخش‌ها و ستون کناری را قالب می‌چیند. این الگو اجزای دامنه‌ای داخل بخش‌ها را نشان می‌دهد.

چه زمانی از این الگو استفاده کنید؟

این الگو برای ساخت صفحاتی طراحی شده که اطلاعات جامع یک حساب کاربری یا اینفلوئنسر را نمایش می‌دهند. مواردی مانند:

  • صفحه پروفایل اینفلوئنسر: نمایش کامل اطلاعات، نرخ تعامل، احساسات مخاطبان و محتوای اخیر
  • تحلیل عمیق حساب کاربری: بررسی روند رشد دنبال‌کننده، عملکرد محتوا و توزیع احساسات
  • گزارش عملکرد: ارائه خلاصه‌ای از معیارهای کلیدی و تغییرات آن‌ها در بازه زمانی مشخص

نمونه بصری

نزدیک‌ترین پیاده‌سازی کامل و کارکنندهٔ این الگو بلاک پروفایل اینفلوئنسر است: قالب DetailPage با راه بازگشت، اقدام اصلی، ستون کناری پروفایل و DetailSectionها، در عرض جزئیات (1200). آن را در اندازه‌های مختلف صفحه و در هر دو تم ببینید؛ صفحه‌ی حاضر توضیح می‌دهد چرا این‌طور چیده شده.

بلوک آماده: پروفایل اینفلوئنسر

کد و نمای کامل

ساختار کلی صفحه

صفحهٔ تحلیل یک اینفلوئنسر صفحهٔ یک موجودیت است، پس قالبش DetailPage است: title نام اینفلوئنسر (و تا بارگذاری، «اینفلوئنسر»)، back برای بازگشت به فهرست، کارت پروفایل در aside (ستون کناری، و روی صفحهٔ باریک زیر بخش‌ها) و هر بخش تحلیل یک DetailSection. عرض، سرِ صفحه و ریتم را قالب می‌دهد و PageContainer یا PageHeader در صفحه نمی‌آید.

import { useAsync } from '@partodata/ui'
import { DetailPage, DetailSection, pageState } from '@partodata/ui/templates'

function AnalysisPage({ id }: { id: string }) {
  const { data: profile, isLoading, error, run } = useAsync<Influencer>()
  // … load = () => run(() => getInfluencer(id)) در یک effect

  return (
    <DetailPage
      title={profile?.name ?? 'اینفلوئنسر'}
      description="تحلیل عملکرد و محتوای اینفلوئنسر در شبکه‌های اجتماعی"
      back={{ href: '/influencers', label: 'اینفلوئنسرها' }}
      aside={<ProfileAside />}
      state={pageState({ data: profile, isLoading, error, onRetry: load })}
    >
      <DetailSection title="نرخ تعامل">{/* EngagementRate */}</DetailSection>
      <DetailSection title="احساسات مخاطبان">{/* SentimentDistribution */}</DetailSection>
      <DetailSection title="محتوای اخیر">{/* گرید پست‌ها */}</DetailSection>
      <DetailSection title="معیارها و آمار">{/* MetricCardها */}</DetailSection>
    </DetailPage>
  )
}

بخش پروفایل (Aside)

در ستون کناری، Account با چیدمان card خلاصهٔ حساب اینفلوئنسر را نمایش می‌دهد (نام، شناسه، نشان تأیید فقط وقتی منبع بگوید، آمار خود حساب با «—» برای مقدار نامعلوم) و PlatformMark مشخص می‌کند داده‌ها از کدام پلتفرم‌ها جمع‌آوری شده‌اند. کلیک روی کارت (onOpen) حساب را داخل محصول باز می‌کند.

'use client'

import { PlatformMark } from '@partodata/ui/social'
import { Account, type SocialAccount } from '@partodata/ui/social'

const account: SocialAccount = {
  id: 'sara_ahmadi',
  source: { key: 'instagram' },
  name: 'سارا احمدی',
  handle: 'sara.ahmadi',
  avatarUrl: '/avatars/sara-ahmadi.jpg',
  metrics: { followers: 245_000, following: 380, posts: 612 },
}

export function ProfileAside({ onOpenProfile }: { onOpenProfile: (account: SocialAccount) => void }) {
  return (
    <div className="space-y-4">
      <Account account={account} layout="card" onOpen={onOpenProfile} />

      <div className="flex flex-wrap gap-2">
        <PlatformMark source="instagram" variant="badge" showLabel />
        <PlatformMark source="twitter" variant="badge" showLabel />
        <PlatformMark source="youtube" variant="badge" showLabel />
      </div>
    </div>
  )
}

برای نمایش افقی و فشرده (یک سطر در فهرست یا بالای یک بخش) همان Account با layout="row" است؛ مقدار زمینه، مثل آخرین به‌روزرسانی، در aside می‌آید:

'use client'

import { Account, type SocialAccount } from '@partodata/ui/social'

export function ProfileRow({ account }: { account: SocialAccount }) {
  return (
    <Account
      account={account}
      layout="row"
      aside={<span className="text-meta">آخرین به‌روزرسانی: 15 فروردین 1405</span>}
    />
  )
}

بخش تحلیل نرخ تعامل

نسخه کامل — EngagementRate

برای تحلیل اصلی صفحه از نسخه کامل استفاده کنید. این کامپوننت نوار تقسیم‌بندی شده با 6 سطح، مثلث شاخص و کارت اطلاعات دسته‌بندی را نمایش می‌دهد.

import { EngagementRate } from '@partodata/ui'
;<EngagementRate currentRate={0.0342} followers={245000} locale="fa" showCategoryCard={true} />

مقدار currentRate به صورت اعشاری وارد می‌شود (مثلا 0.0342 برای 3.42%). کامپوننت به صورت خودکار بر اساس تعداد دنبال‌کننده‌ها، دسته مناسب (نانو، میکرو، متوسط، ماکرو، مگا) را تشخیص می‌دهد و سطح نرخ تعامل را مشخص می‌کند.

نسخه فشرده — EngagementRate

برای نمایش در کارت‌ها، جداول مقایسه‌ای یا فضاهای محدود از نسخه فشرده استفاده کنید:

import { EngagementRate } from '@partodata/ui'
;<EngagementRate display="bar" currentRate={0.0342} followers={245000} locale="fa" />

چه زمانی از کدام نسخه استفاده کنید؟

حالتکامپوننتدلیل
صفحه تحلیل اصلیEngagementRateنمایش کامل با کارت دسته‌بندی و جزئیات
کارت خلاصه در داشبوردEngagementRateفضای محدود، فقط نوار و درصد
جدول مقایسه اینفلوئنسرهاEngagementRateنمایش فشرده در سلول جدول
صفحه جزئیات پروفایلEngagementRateکاربر انتظار تحلیل عمیق دارد

بخش تحلیل احساسات

توزیع کلی احساسات

از Distribution kind="sentiment" با حالت bars برای نمایش توزیع کلی احساسات نظرات استفاده کنید:

import { Distribution } from '@partodata/ui'
;<Distribution kind="sentiment" data={{ positive: 3420, negative: 890, neutral: 1540 }} variant="bars" showCounts />

نشان‌های احساسات فردی

برای نمایش برچسب‌های احساسات در کنار محتوا یا نظرات:

import { SentimentBadge } from '@partodata/ui'
;<div className="flex flex-wrap gap-2">
  <SentimentBadge sentiment="positive" count={3420} />
  <SentimentBadge sentiment="negative" count={890} />
  <SentimentBadge sentiment="neutral" count={1540} />
  <SentimentBadge sentiment="mixed" count={230} />
</div>

نمودار تفکیک احساسات بر اساس موضوع

برای نمایش توزیع احساسات در موضوعات مختلف از PartoBarChart یا PartoPieChart استفاده کنید:

import { PartoBarChart } from '@partodata/ui'

const sentimentByTopic = [
  { موضوع: 'کیفیت محصول', مثبت: 820, منفی: 120, خنثی: 340 },
  { موضوع: 'خدمات پشتیبانی', مثبت: 540, منفی: 380, خنثی: 210 },
  { موضوع: 'قیمت‌گذاری', مثبت: 310, منفی: 490, خنثی: 180 },
  { موضوع: 'تجربه کاربری', مثبت: 670, منفی: 95, خنثی: 420 },
]

;<div className="h-[400px]">
  <PartoBarChart
    data={sentimentByTopic}
    keys={['مثبت', 'منفی', 'خنثی']}
    indexBy="موضوع"
    margin={{ top: 20, right: 20, bottom: 50, left: 80 }}
    padding={0.3}
    groupMode="stacked"
  />
</div>

برای نمایش نسبت کلی احساسات در نمودار دایره‌ای:

import { PartoPieChart } from '@partodata/ui'

const sentimentData = [
  { id: 'مثبت', label: 'مثبت', value: 3420 },
  { id: 'منفی', label: 'منفی', value: 890 },
  { id: 'خنثی', label: 'خنثی', value: 1540 },
]

;<div className="h-[300px]">
  <PartoPieChart
    data={sentimentData}
    innerRadius={0.5}
    padAngle={0.7}
    cornerRadius={3}
    activeOuterRadiusOffset={8}
    enableArcLinkLabels={false}
  />
</div>

بخش محتوا

پست‌های اخیر

پست‌های اخیر یک EntityCollection از Post است و مثل همه‌جا با کارت (card) باز می‌شود: کارت در ستون فید حدود 680 پیکسلی می‌ماند و رسانه را هرگز نمی‌بُرد: نسبت خود رسانه در بازهٔ 4:5 تا 1.91:1 با ارتفاع حداکثر حدود 500 پیکسل، و رسانه‌ای که بیرون از این بازه است یا بلندتر می‌شود، کامل روی نوارهای محو همان تصویر. ردیف (row) و شبکهٔ کاشی (tile) نماهای انتخابی‌اند که خواننده با دکمهٔ چیدمان مجموعه برمی‌گزیند: کاشی ستون پهن را با ستون‌های خودکار کمینهٔ حدود 280 پیکسل پر می‌کند و تنها چیدمانی است که رسانه را می‌بُرد (1:1 یا 4:5)؛ کلیک در هر چیدمان، جزئیات پست را داخل محصول باز می‌کند.

'use client'

import { useRouter } from 'next/navigation'
import { EntityCollection, Post, type SocialPost } from '@partodata/ui/social'

export function RecentPosts({ posts, loading }: { posts: SocialPost[]; loading: boolean }) {
  const router = useRouter()
  return (
    <EntityCollection
      entity="post"
      items={posts}
      getId={(post) => post.id}
      loading={loading}
      label="پست‌های اخیر"
      renderItem={(post, item) => <Post post={post} {...item} onOpen={(p) => router.push(`/posts/${p.id}`)} />}
    />
  )
}

دادهٔ API را پیش از نمایش یک بار به مدل تبدیل کنید: ردیف PostData با fromPostData از @partodata/ui/social/adapters. عدد نامعلوم «—» است و آمار حساب (دنبال‌کننده، رتبه) داخل پست نمی‌آید.

تحلیل نظرات

نظرهای تحلیل‌شده هم یک EntityCollection است، با entity="comment" و چیدمان‌های card و row از Comment: برچسب‌ها با اطمینانشان (عددی بین 0 و 1 که یک بار درصد نمایش داده می‌شود) و احساس نظر در signals.

'use client'

import { Comment, EntityCollection, type SocialComment } from '@partodata/ui/social'

const comments: SocialComment[] = [
  {
    id: 'c1',
    author: { handle: 'user_1' },
    text: 'محتوای خیلی خوبی بود. کیفیت تصاویر و نوع ارائه واقعاً حرفه‌ای است. منتظر محتوای بیشتر هستم.',
    labels: [
      { title: 'رضایت از کیفیت', confidence: 0.94 },
      { title: 'درخواست محتوای بیشتر', confidence: 0.87 },
    ],
    signals: { sentiment: 'positive' },
  },
  {
    id: 'c2',
    author: { handle: 'user_2' },
    text: 'متاسفانه اطلاعات ارائه‌شده دقیق نبود و منابع معتبری ذکر نشده بود. لطفاً بیشتر تحقیق کنید.',
    labels: [
      { title: 'عدم دقت اطلاعات', confidence: 0.91 },
      { title: 'نبود منبع معتبر', confidence: 0.85 },
    ],
    signals: { sentiment: 'negative' },
  },
  {
    id: 'c3',
    author: { handle: 'user_3' },
    text: 'موضوع جالبی بود ولی کاش بیشتر توضیح می‌دادید. نه خیلی خوب بود نه خیلی بد.',
    labels: [{ title: 'نظر ترکیبی', confidence: 0.78 }],
    signals: { sentiment: 'neutral' },
  },
]

export function CommentAnalysis() {
  return (
    <EntityCollection
      entity="comment"
      items={comments}
      getId={(comment) => comment.id}
      layouts={['card', 'row']}
      label="نظرهای تحلیل‌شده"
      renderItem={(comment, item) => <Comment comment={comment} {...item} />}
    />
  )
}

بخش معیارها و آمار

گرید کارت‌های متریک

از MetricCard برای نمایش معیارهای کلیدی مانند رشد دنبال‌کننده، روند تعامل و فراوانی انتشار استفاده کنید:

import {
  MetricCard,
  MetricCardHeader,
  MetricCardLabel,
  MetricCardContent,
  MetricCardValue,
  MetricCardDifferential,
  MetricCardSparkline,
} from '@partodata/ui'
import { Users, Heart, FileText } from 'lucide-react'

const followerData = [
  { value: 238000, timestamp: '1404-12-01' },
  { value: 239200, timestamp: '1404-12-08' },
  { value: 240100, timestamp: '1404-12-15' },
  { value: 241800, timestamp: '1404-12-22' },
  { value: 243500, timestamp: '1404-12-29' },
  { value: 245000, timestamp: '1405-01-07' },
]

const engagementData = [
  { value: 3.1, timestamp: '1404-12-01' },
  { value: 3.3, timestamp: '1404-12-08' },
  { value: 3.0, timestamp: '1404-12-15' },
  { value: 3.5, timestamp: '1404-12-22' },
  { value: 3.4, timestamp: '1404-12-29' },
  { value: 3.42, timestamp: '1405-01-07' },
]

const postFrequencyData = [
  { value: 12, timestamp: '1404-12-01' },
  { value: 15, timestamp: '1404-12-08' },
  { value: 10, timestamp: '1404-12-15' },
  { value: 14, timestamp: '1404-12-22' },
  { value: 18, timestamp: '1404-12-29' },
  { value: 16, timestamp: '1405-01-07' },
]

;<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
  <MetricCard>
    <MetricCardHeader>
      <MetricCardLabel icon={<Users className="h-3.5 w-3.5" />}>تعداد دنبال‌کننده‌ها</MetricCardLabel>
    </MetricCardHeader>
    <MetricCardContent>
      <MetricCardValue>245,000</MetricCardValue>
      <MetricCardDifferential direction="up" tone="positive">
        +2.9٪
      </MetricCardDifferential>
    </MetricCardContent>
    <MetricCardSparkline data={followerData} dataKey="value" />
  </MetricCard>

  <MetricCard>
    <MetricCardHeader>
      <MetricCardLabel icon={<Heart className="h-3.5 w-3.5" />}>نرخ تعامل</MetricCardLabel>
    </MetricCardHeader>
    <MetricCardContent>
      <MetricCardValue>3.42٪</MetricCardValue>
      <MetricCardDifferential direction="up" tone="positive">
        +0.12٪
      </MetricCardDifferential>
    </MetricCardContent>
    <MetricCardSparkline data={engagementData} dataKey="value" />
  </MetricCard>

  <MetricCard>
    <MetricCardHeader>
      <MetricCardLabel icon={<FileText className="h-3.5 w-3.5" />}>تعداد پست ماهانه</MetricCardLabel>
    </MetricCardHeader>
    <MetricCardContent>
      <MetricCardValue>16</MetricCardValue>
      <MetricCardDifferential direction="down" tone="negative">
        -1.2٪
      </MetricCardDifferential>
    </MetricCardContent>
    <MetricCardSparkline data={postFrequencyData} dataKey="value" />
  </MetricCard>
</div>

نشانگر روند و نمایش آمار

برای نمایش سریع اعداد کلیدی با نشانگر تغییر از StatDisplay و TrendIndicator استفاده کنید:

import { StatDisplay, TrendIndicator } from '@partodata/ui'
import { Eye, MessageCircle, Share2 } from 'lucide-react'
;<div className="grid grid-cols-2 gap-4 sm:grid-cols-4">
  <StatDisplay value={245000} label="دنبال‌کننده‌ها" icon={<Users />} trend={<TrendIndicator value={2.9} />} />

  <StatDisplay value={52300} label="میانگین بازدید" icon={<Eye />} trend={<TrendIndicator value={5.2} />} />

  <StatDisplay value={342} label="میانگین نظرات" icon={<MessageCircle />} trend={<TrendIndicator value={-1.3} />} />

  <StatDisplay value={128} label="میانگین اشتراک‌گذاری" icon={<Share2 />} trend={<TrendIndicator value={8.7} />} />
</div>

حالت بارگذاری

اسکلت صفحه را خودتان نمی‌سازید. state صفحه (pageState از فیلدهای useAsync) تا بارگذاری اینفلوئنسر اسکلت بخش‌ها را جای بخش‌ها و ستون کناری می‌گذارد و سرِ صفحه می‌ماند؛ خطا با «تلاش مجدد» جای همان‌ها می‌نشیند. بخشی که جدا بارگذاری می‌شود (نظرها، پست‌های اخیر) state و skeleton خودش را دارد؛ Account و EntityCollection اسکلت خودشان را به شکل همان موجودیت می‌سازند (state="loading" و loading):

import { useAsync } from '@partodata/ui'
import { DetailSection, pageState } from '@partodata/ui/templates'

const posts = useAsync<Post[]>()

;<DetailSection
  title="محتوای اخیر"
  skeleton="cards"
  state={pageState({
    data: posts.data,
    isLoading: posts.isLoading,
    error: posts.error,
    onRetry: loadPosts,
    emptyCopy: { title: 'هنوز پستی از این اینفلوئنسر ثبت نشده است' },
  })}
>
  {/* گرید پست‌ها */}
</DetailSection>

کامپوننت‌های دامنه‌ای prop isLoading خودشان را هم دارند؛ آن را فقط برای جزئی به کار ببرید که داده‌اش جدا از بخش می‌رسد، نه برای بارگذاری خود بخش یا صفحه.


بهترین شیوه‌ها

چیدمان

  • کارت حساب (Account با layout="card"، از @partodata/ui/social) را در aside قالب DetailPage بگذارید و هر بخش تحلیل را یک DetailSection؛ ستون کناری از 56rem عرض محتوا کنار بخش‌ها می‌نشیند و زیر آن، زیر بخش‌ها.
  • عرض صفحه را قالب می‌دهد (1200)؛ PageContainer، PageHeader یا PageSection با orientation در صفحه ننویسید.

نرخ تعامل

  • در صفحه تحلیل اصلی از EngagementRate (نسخه کامل با کارت دسته‌بندی) استفاده کنید.
  • از EngagementRate برای نماهای مقایسه‌ای، جداول و کارت‌های خلاصه استفاده کنید.
  • مقدار currentRate همیشه به صورت اعشاری وارد می‌شود (0.0342 نه 3.42).

پلتفرم‌ها

  • همیشه PlatformMark را نمایش دهید تا کاربر بداند داده‌ها از کدام پلتفرم جمع‌آوری شده‌اند.
  • اگر داده‌ها از چند پلتفرم هستند، همه نشان‌ها را کنار هم نمایش دهید.

بارگذاری

  • بارگذاری صفحه state قالب DetailPage است و بارگذاری یک بخش state همان DetailSection؛ اسکلت دست‌ساز ننویسید.
  • prop isLoading کامپوننت‌های دامنه‌ای (EngagementRate، Distribution، StatDisplay) فقط برای جزئی است که داده‌اش جدا از بخش می‌رسد.

داده‌ها

  • از اعداد واقع‌گرایانه در نمونه‌ها و تست‌ها استفاده کنید. نرخ تعامل 3.42% برای یک اینفلوئنسر با 245,000 دنبال‌کننده (دسته میکرو) واقع‌بینانه است.
  • برای Distribution داده‌های خام عددی ارسال کنید — کامپوننت به صورت خودکار درصدها را محاسبه می‌کند.
  • اطمینان برچسب‌های نظر (labels[].confidence) عددی بین 0 و 1 است و یک بار درصد نمایش داده می‌شود؛ دادهٔ درصدی (0 تا 100) را پیش از نمایش با toRatio(value, { unit: 'percent' }) از @partodata/ui/social/adapters تبدیل کنید.

دام‌های رایج

خواص فیزیکی CSS در کد اتصال صفحه

مشکل: استفاده از ml/pl/text-left در گریدها و wrapper هایی که ستون پروفایل را به بخش‌های تحلیل وصل می‌کنند.

// ❌ غلط — در چیدمان RTL فاصله و خط جداکننده به سمت اشتباه می‌روند
<div className="ml-4 border-l pl-6 text-left">{/* بخش‌های تحلیل */}</div>

// ✅ درست — Logical Properties در RTL و LTR هر دو درست عمل می‌کنند
<div className="ms-4 border-s ps-6 text-start">{/* بخش‌های تحلیل */}</div>

چرا اشتباه است: صفحه تحلیل با dir="rtl" رندر می‌شود و در RTL ابتدای خط سمت راست است؛ ml-4 حاشیه را به چپ می‌چسباند و چیدمان دو ستونی را به‌هم می‌ریزد. کامپوننت‌های @partodata/ui خودشان با قانون ESLint no-physical-css-properties محافظت می‌شوند، اما کد اتصالی که در صفحه خودتان می‌نویسید چنین محافظی ندارد — آن‌جا فقط انضباط خودتان جلوی این خطا را می‌گیرد.

رنگ احساسات به‌صورت hardcode

مشکل: نوشتن رنگ مستقیم (مثلا #22c55e یا text-green-500) برای برچسب‌ها و سلول‌های احساسات به‌جای توکن‌های معنایی.

// ❌ غلط — فقط در یکی از دو تم درست دیده می‌شود
<span style={{ color: '#22c55e' }}>مثبت</span>

// ✅ درست — نشان احساس رنگ و برچسبش را از توکن‌های معنایی می‌گیرد و در هر دو تم درست است
<SentimentBadge sentiment="positive" />

احساس یک نظر یا پست را هم با رنگ دستی نسازید: signals.sentiment را در مدل بدهید تا Comment و Post همین نشان را نمایش دهند.

چرا اشتباه است: توکن‌های این سیستم dark-first هستند — مقدار پایه :root تمِ تاریک است و تم روشن با [data-theme='light'] فعال می‌شود، بنابراین رنگ ثابت در تم مقابل کنتراست را از دست می‌دهد. علاوه بر این، نمودارها رنگ‌ها را در زمان اجرا از همین توکن‌ها می‌خوانند؛ مقدار hardcode با رنگ احساسات در بقیه بخش‌های صفحه ناهماهنگ می‌شود.

صفحه‌بندی سمت کلاینت برای نظرات حجیم

مشکل: دریافت همه نظرات یک اینفلوئنسر در یک درخواست و برش (slice) آن‌ها در مرورگر، یا نمایش کل لیست بدون صفحه‌بندی.

// ❌ غلط — کل داده به کلاینت منتقل می‌شود
const all = await fetchComments(influencerId) // ده‌ها هزار ردیف
const page = all.slice((currentPage - 1) * 20, currentPage * 20)

// ✅ درست — سرور یک صفحه برمی‌گرداند؛ حالت‌ها state بخش‌اند و جدول صفحه‌بندی سرور-محور خودش را دارد
<DetailSection
  title="نظرها"
  state={pageState({ data: pageData, isLoading, error, onRetry: load, emptyCopy: { title: 'هنوز نظری ثبت نشده است' } })}
>
  <DataTable
    columns={columns}
    data={pageData ?? []} // فقط ردیف‌های صفحه جاری
    pagination={{
      currentPage,
      totalPages,
      onPageChange: setCurrentPage,
      pageSize,
      onPageSizeChange: setPageSize,
      totalRows, // تعداد کل ردیف‌ها طبق گزارش سرور
    }}
  />
</DetailSection>

چرا اشتباه است: صفحات تحلیل روی داده‌های حجیم شنود اجتماعی می‌نشینند و یک حساب پرتعامل ممکن است ده‌ها هزار نظر داشته باشد؛ دریافت یک‌جای آن‌ها حجم پاسخ، مصرف حافظه و زمان رندر را چند برابر می‌کند. قرارداد DataTable از اساس server-paged است: کامپوننت تعداد صفحات را از داده استنتاج نمی‌کند و totalPages و totalRows را از سرور می‌گیرد.

نمودار بدون ارتفاع مشخص

مشکل: قرار دادن PartoBarChart یا PartoPieChart در یک div بدون ارتفاع صریح.

// ❌ غلط — نمودار با ارتفاع صفر رندر می‌شود و بدون هیچ خطایی خالی می‌ماند
<div>
  <PartoBarChart data={sentimentByTopic} keys={['مثبت', 'منفی', 'خنثی']} indexBy="موضوع" />
</div>

// ✅ درست — والد ارتفاع صریح دارد
<div className="h-[400px]">
  <PartoBarChart data={sentimentByTopic} keys={['مثبت', 'منفی', 'خنثی']} indexBy="موضوع" />
</div>

چرا اشتباه است: نمودارها داخل ResponsiveContainer با width="100%" height="100%" رندر می‌شوند؛ وقتی والد ارتفاع نداشته باشد، «100٪» از ارتفاعِ صفر محاسبه می‌شود و نمودار بدون هیچ پیام خطایی ناپدید می‌شود. به همین دلیل همه نمونه‌های این صفحه wrapper با h-[400px] یا h-[300px] دارند.

فارسی غیررسمی و ارقام ناهماهنگ در متن رابط

مشکل: لحن غیررسمی در متن‌های صفحه و ارقام لاتین در میان متن فارسی.

// ❌ غلط — لحن غیررسمی و ارقام لاتین
<PageHeaderDescription>عملکرد سارا رو ببین — 245000 دنبال‌کننده</PageHeaderDescription>

// ✅ درست — فارسی رسمی و ارقام فارسی هماهنگ با بقیه صفحه
<PageHeaderDescription>بررسی عملکرد و تحلیل محتوای سارا احمدی — 245,000 دنبال‌کننده</PageHeaderDescription>

چرا اشتباه است: متن رابط در این سیستم همیشه فارسی رسمی است («استفاده کنید» نه «استفاده کن») و کامپوننت‌های عددی مانند EngagementRate با locale="fa" ارقام فارسی تولید می‌کنند؛ اگر متن‌های دستی شما ارقام لاتین داشته باشند، صفحه ترکیبی ناهماهنگ از دو نوع رقم نمایش می‌دهد. برای تبدیل ارقام در مقادیر پویا از value استفاده کنید.


صفحات مرتبط

  • جست‌وجوی پست — اگر خروجی تحلیل فهرستی قابل جست‌وجو و فیلتر از پست‌ها یا نظرات است: EntityCollection زیر PageToolbar، در ستون فید. برای جدول داده با ستون و مرتب‌سازی (مثلاً فهرست حساب‌ها با شاخص‌هایشان) صفحه جدول داده.
  • ترکیب داشبورد — اگر به‌جای تحلیل عمیق یک حساب، نمای کلی چند متریک و چند حساب در یک صفحه لازم دارید، این الگو مناسب‌تر است.
  • DetailPage — قالب صفحهٔ یک موجودیت: راه بازگشت، زبانه‌ها، ستون کناری و DetailSectionها.
  • اشتباهات رایج — فهرست کامل ضدالگوهای سراسری (رنگ hardcode، خواص فیزیکی و…) که دام‌های بالا نمونه‌های تحلیلی آن‌ها هستند.