پرتوپرتو

ترکیب داشبورد

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

معرفی

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

پیش‌نیاز

برای layout کلی صفحه (سایدبار + نوار بالا) ابتدا الگوی پوسته برنامه (AppShell) را پیاده‌سازی کنید. این الگو فقط محتوای داخل content area را پوشش می‌دهد.

از این الگو زمانی استفاده کنید که نیاز به ساخت صفحه‌ای دارید که چندین متریک، نمودار روند، و تحلیل احساسات را در کنار هم نمایش می‌دهد. کامپوننت‌های استفاده‌شده در این الگو عبارتند از: MetricCard برای نمایش اعداد کلیدی، SentimentDistribution برای توزیع احساسات، PartoLineChart برای روند زمانی، و EngagementRate برای تحلیل نرخ تعامل.

داشبورد سوشال لیسنینگ

خلاصه وضعیت برند

کل منشن‌ها

۱۲,۴۵۰
+۸.۳٪

اینفلوئنسرها

۳۴۸
+۱۲٪

نرخ تعامل

۴.۲٪
-۰.۳٪

بازدید کل

۲.۴ میلیون
+۱۵٪

روند منشن‌ها

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

مثبت
۴۲٪
خنثی
۴۳٪
منفی
۱۵٪

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


ترکیب صفحه کامل

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

'use client'

import { useState } from 'react'
import {
  PeriodSelector,
  MetricCard,
  MetricCardHeader,
  MetricCardLabel,
  MetricCardContent,
  MetricCardValue,
  MetricCardDifferential,
  MetricCardSparkline,
  SentimentDistribution,
  PartoLineChart,
  EngagementRate,
} from '@partodata/ui'
import { Users, MessageCircle, TrendingUp, Eye } from 'lucide-react'

export default function DashboardPage() {
  const [period, setPeriod] = useState('7d')

  return (
    <div className="w-full space-y-6 p-6">
      {/* هدر داشبورد */}
      <div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
        <div>
          <h1 className="text-2xl font-bold text-foreground">داشبورد سوشال لیسنینگ</h1>
          <p className="text-sm text-muted-foreground">خلاصه وضعیت برند در شبکه‌های اجتماعی</p>
        </div>
        <PeriodSelector value={period} onValueChange={setPeriod} />
      </div>

      {/* گرید متریک‌ها */}
      <div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
        <MetricCard>
          <MetricCardHeader>
            <MetricCardLabel icon={<MessageCircle className="h-3.5 w-3.5" />}>کل منشن‌ها</MetricCardLabel>
          </MetricCardHeader>
          <MetricCardContent>
            <MetricCardValue>۱۲,۴۵۰</MetricCardValue>
            <MetricCardDifferential variant="positive">+۸.۳٪</MetricCardDifferential>
          </MetricCardContent>
          <MetricCardSparkline
            data={[
              { value: 1100, timestamp: 1704067200 },
              { value: 1250, timestamp: 1704153600 },
              { value: 1180, timestamp: 1704240000 },
              { value: 1320, timestamp: 1704326400 },
              { value: 1400, timestamp: 1704412800 },
              { value: 1350, timestamp: 1704499200 },
              { value: 1500, timestamp: 1704585600 },
            ]}
            dataKey="value"
          />
        </MetricCard>

        <MetricCard>
          <MetricCardHeader>
            <MetricCardLabel icon={<Users className="h-3.5 w-3.5" />}>اینفلوئنسرهای فعال</MetricCardLabel>
          </MetricCardHeader>
          <MetricCardContent>
            <MetricCardValue>۳۴۸</MetricCardValue>
            <MetricCardDifferential variant="positive">+۱۲.۱٪</MetricCardDifferential>
          </MetricCardContent>
          <MetricCardSparkline
            data={[
              { value: 280, timestamp: 1704067200 },
              { value: 295, timestamp: 1704153600 },
              { value: 310, timestamp: 1704240000 },
              { value: 305, timestamp: 1704326400 },
              { value: 320, timestamp: 1704412800 },
              { value: 335, timestamp: 1704499200 },
              { value: 348, timestamp: 1704585600 },
            ]}
            dataKey="value"
          />
        </MetricCard>

        <MetricCard>
          <MetricCardHeader>
            <MetricCardLabel icon={<TrendingUp className="h-3.5 w-3.5" />}>نرخ تعامل میانگین</MetricCardLabel>
          </MetricCardHeader>
          <MetricCardContent>
            <MetricCardValue>۴.۲٪</MetricCardValue>
            <MetricCardDifferential variant="negative">-۰.۳٪</MetricCardDifferential>
          </MetricCardContent>
          <MetricCardSparkline
            data={[
              { value: 4.8, timestamp: 1704067200 },
              { value: 4.5, timestamp: 1704153600 },
              { value: 4.6, timestamp: 1704240000 },
              { value: 4.3, timestamp: 1704326400 },
              { value: 4.1, timestamp: 1704412800 },
              { value: 4.4, timestamp: 1704499200 },
              { value: 4.2, timestamp: 1704585600 },
            ]}
            dataKey="value"
          />
        </MetricCard>

        <MetricCard>
          <MetricCardHeader>
            <MetricCardLabel icon={<Eye className="h-3.5 w-3.5" />}>بازدید کل</MetricCardLabel>
          </MetricCardHeader>
          <MetricCardContent>
            <MetricCardValue>۲.۴ میلیون</MetricCardValue>
            <MetricCardDifferential variant="positive">+۱۵.۷٪</MetricCardDifferential>
          </MetricCardContent>
          <MetricCardSparkline
            data={[
              { value: 1800000, timestamp: 1704067200 },
              { value: 1950000, timestamp: 1704153600 },
              { value: 2100000, timestamp: 1704240000 },
              { value: 2050000, timestamp: 1704326400 },
              { value: 2200000, timestamp: 1704412800 },
              { value: 2300000, timestamp: 1704499200 },
              { value: 2400000, timestamp: 1704585600 },
            ]}
            dataKey="value"
          />
        </MetricCard>
      </div>

      {/* ردیف نمودار و احساسات */}
      <div className="grid grid-cols-1 gap-4 lg:grid-cols-3">
        <div className="lg:col-span-2 rounded-lg border border-border bg-card p-4">
          <h3 className="mb-4 text-sm font-medium text-foreground">روند منشن‌ها در طول زمان</h3>
          <div className="h-[300px]">
            <PartoLineChart
              data={[
                {
                  id: 'منشن‌ها',
                  data: [
                    { x: 'فروردین', y: 1200 },
                    { x: 'اردیبهشت', y: 1450 },
                    { x: 'خرداد', y: 1380 },
                    { x: 'تیر', y: 1620 },
                    { x: 'مرداد', y: 1800 },
                    { x: 'شهریور', y: 1750 },
                    { x: 'مهر', y: 1920 },
                  ],
                },
              ]}
              margin={{ top: 20, right: 20, bottom: 50, left: 60 }}
              xScale={{ type: 'point' }}
              yScale={{ type: 'linear', min: 'auto', max: 'auto' }}
              enableArea
              enablePoints={false}
              curve="monotoneX"
            />
          </div>
        </div>

        <div className="rounded-lg border border-border bg-card p-4">
          <h3 className="mb-4 text-sm font-medium text-foreground">توزیع احساسات</h3>
          <SentimentDistribution data={{ positive: 5200, negative: 1840, neutral: 5410 }} />
        </div>
      </div>

      {/* بخش نرخ تعامل */}
      <div className="rounded-lg border border-border bg-card p-4">
        <h3 className="mb-4 text-sm font-medium text-foreground">تحلیل نرخ تعامل</h3>
        <EngagementRate currentRate={0.0421} followers={85000} />
      </div>
    </div>
  )
}

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

برای نمایش متریک‌های کلیدی از یک گرید واکنش‌گرا استفاده کنید که در موبایل تک‌ستونی، در تبلت دوستونی، و در دسکتاپ چهارستونی باشد:

<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
  <MetricCard>
    <MetricCardHeader>
      <MetricCardLabel>کل منشن‌ها</MetricCardLabel>
    </MetricCardHeader>
    <MetricCardContent>
      <MetricCardValue>۱۲,۴۵۰</MetricCardValue>
      <MetricCardDifferential variant="positive">+۸.۳٪</MetricCardDifferential>
    </MetricCardContent>
    <MetricCardSparkline data={mentionsData} dataKey="value" />
  </MetricCard>

  <MetricCard>
    <MetricCardHeader>
      <MetricCardLabel>احساسات مثبت</MetricCardLabel>
    </MetricCardHeader>
    <MetricCardContent>
      <MetricCardValue>۶۲٪</MetricCardValue>
      <MetricCardDifferential variant="positive">+۳.۱٪</MetricCardDifferential>
    </MetricCardContent>
    <MetricCardSparkline data={sentimentData} dataKey="value" />
  </MetricCard>

  <MetricCard>
    <MetricCardHeader>
      <MetricCardLabel>نرخ تعامل</MetricCardLabel>
    </MetricCardHeader>
    <MetricCardContent>
      <MetricCardValue>۴.۲٪</MetricCardValue>
      <MetricCardDifferential variant="negative">-۰.۳٪</MetricCardDifferential>
    </MetricCardContent>
    <MetricCardSparkline data={engagementData} dataKey="value" />
  </MetricCard>

  <MetricCard>
    <MetricCardHeader>
      <MetricCardLabel>دسترسی کل</MetricCardLabel>
    </MetricCardHeader>
    <MetricCardContent>
      <MetricCardValue>۲.۴ میلیون</MetricCardValue>
      <MetricCardDifferential variant="positive">+۱۵.۷٪</MetricCardDifferential>
    </MetricCardContent>
    <MetricCardSparkline data={reachData} dataKey="value" />
  </MetricCard>
</div>

داده‌های دینامیک با حلقه

اگر متریک‌ها از API دریافت می‌شوند، می‌توانید از حلقه استفاده کنید:

const metrics = [
  {
    label: 'کل منشن‌ها',
    value: '۱۲,۴۵۰',
    differential: '+۸.۳٪',
    variant: 'positive' as const,
    data: mentionsData,
  },
  {
    label: 'احساسات مثبت',
    value: '۶۲٪',
    differential: '+۳.۱٪',
    variant: 'positive' as const,
    data: sentimentData,
  },
  {
    label: 'نرخ تعامل',
    value: '۴.۲٪',
    differential: '-۰.۳٪',
    variant: 'negative' as const,
    data: engagementData,
  },
  {
    label: 'دسترسی کل',
    value: '۲.۴ میلیون',
    differential: '+۱۵.۷٪',
    variant: 'positive' as const,
    data: reachData,
  },
]

;<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
  {metrics.map((metric) => (
    <MetricCard key={metric.label}>
      <MetricCardHeader>
        <MetricCardLabel>{metric.label}</MetricCardLabel>
      </MetricCardHeader>
      <MetricCardContent>
        <MetricCardValue>{metric.value}</MetricCardValue>
        <MetricCardDifferential variant={metric.variant}>{metric.differential}</MetricCardDifferential>
      </MetricCardContent>
      <MetricCardSparkline data={metric.data} dataKey="value" />
    </MetricCard>
  ))}
</div>

ردیف نمودار و احساسات

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

<div className="grid grid-cols-1 gap-4 lg:grid-cols-3">
  {/* نمودار روند — دو ستون */}
  <div className="lg:col-span-2 rounded-lg border border-border bg-card p-4">
    <h3 className="mb-4 text-sm font-medium text-foreground">روند منشن‌ها در طول زمان</h3>
    <div className="h-[300px]">
      <PartoLineChart
        data={[
          {
            id: 'منشن‌ها',
            data: [
              { x: 'فروردین', y: 1200 },
              { x: 'اردیبهشت', y: 1450 },
              { x: 'خرداد', y: 1380 },
              { x: 'تیر', y: 1620 },
              { x: 'مرداد', y: 1800 },
              { x: 'شهریور', y: 1750 },
              { x: 'مهر', y: 1920 },
            ],
          },
        ]}
        margin={{ top: 20, right: 20, bottom: 50, left: 60 }}
        xScale={{ type: 'point' }}
        yScale={{ type: 'linear', min: 'auto', max: 'auto' }}
        enableArea
        enablePoints={false}
        curve="monotoneX"
      />
    </div>
  </div>

  {/* توزیع احساسات — یک ستون */}
  <div className="rounded-lg border border-border bg-card p-4">
    <h3 className="mb-4 text-sm font-medium text-foreground">توزیع احساسات</h3>
    <SentimentDistribution data={{ positive: 5200, negative: 1840, neutral: 5410 }} />
  </div>
</div>

جایگزینی احساسات با نرخ تعامل

به جای توزیع احساسات می‌توانید از EngagementRate در ستون کناری استفاده کنید:

<div className="grid grid-cols-1 gap-4 lg:grid-cols-3">
  <div className="lg:col-span-2 rounded-lg border border-border bg-card p-4">{/* نمودار روند */}</div>

  <div className="rounded-lg border border-border bg-card p-4">
    <h3 className="mb-4 text-sm font-medium text-foreground">نرخ تعامل</h3>
    <EngagementRate currentRate={0.0421} followers={85000} />
  </div>
</div>

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

همه کامپوننت‌های داشبورد از حالت بارگذاری پشتیبانی می‌کنند. قبل از رسیدن داده‌ها، اسکلت‌بندی نمایش دهید:

import { MetricCard, PartoLineChart, SentimentDistribution, Skeleton } from '@partodata/ui'

function DashboardSkeleton() {
  return (
    <div className="w-full space-y-6 p-6">
      {/* اسکلت هدر */}
      <div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
        <div className="space-y-2">
          <Skeleton className="h-8 w-48" />
          <Skeleton className="h-4 w-64" />
        </div>
        <Skeleton className="h-9 w-72" />
      </div>

      {/* اسکلت متریک‌ها */}
      <div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
        {Array.from({ length: 4 }).map((_, i) => (
          <MetricCard key={i} isLoading />
        ))}
      </div>

      {/* اسکلت نمودار و احساسات */}
      <div className="grid grid-cols-1 gap-4 lg:grid-cols-3">
        <div className="lg:col-span-2 rounded-lg border border-border bg-card p-4">
          <Skeleton className="mb-4 h-4 w-32" />
          <PartoLineChart data={[]} isLoading />
        </div>
        <div className="rounded-lg border border-border bg-card p-4">
          <Skeleton className="mb-4 h-4 w-24" />
          <div className="space-y-3">
            <Skeleton className="h-6 w-full" />
            <Skeleton className="h-6 w-full" />
            <Skeleton className="h-6 w-full" />
          </div>
        </div>
      </div>
    </div>
  )
}

استفاده با React Query

import { useQuery } from '@tanstack/react-query'

function SocialListeningDashboard() {
  const { data, isLoading } = useQuery({
    queryKey: ['dashboard', period],
    queryFn: () => fetchDashboardData(period),
  })

  if (isLoading) {
    return <DashboardSkeleton />
  }

  return (
    <div className="w-full space-y-6 p-6">
      <div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
        {data.metrics.map((metric) => (
          <MetricCard key={metric.label}>
            <MetricCardHeader>
              <MetricCardLabel>{metric.label}</MetricCardLabel>
            </MetricCardHeader>
            <MetricCardContent>
              <MetricCardValue>{metric.value.toLocaleString('en-US')}</MetricCardValue>
              <MetricCardDifferential variant={metric.trend > 0 ? 'positive' : 'negative'}>
                {metric.trend > 0 ? '+' : ''}
                {metric.trend.toLocaleString('en-US')}٪
              </MetricCardDifferential>
            </MetricCardContent>
            <MetricCardSparkline data={metric.history} dataKey="value" />
          </MetricCard>
        ))}
      </div>

      {/* بقیه محتوای داشبورد */}
    </div>
  )
}

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

انتخابگر دوره زمانی

از PeriodSelector برای کنترل بازه زمانی همه داده‌های داشبورد استفاده کنید. مقدار انتخاب‌شده را به عنوان پارامتر به API بفرستید تا همه بخش‌ها هماهنگ باشند:

const [period, setPeriod] = useState('7d')
const { data } = useQuery({
  queryKey: ['dashboard', period],
  queryFn: () => fetchDashboardData(period),
})

;<PeriodSelector value={period} onValueChange={setPeriod} />

نمایش اسکلت‌بندی قبل از بارگذاری

همیشه قبل از رسیدن داده‌ها، اسکلت‌بندی نمایش دهید. از isLoading در MetricCard و PartoLineChart استفاده کنید. هرگز صفحه خالی نمایش ندهید.

درصد تغییر نسبت به دوره قبل

از MetricCardDifferential برای نمایش تغییر نسبت به دوره قبلی استفاده کنید. variant="positive" را برای رشد و variant="negative" را برای کاهش تنظیم کنید.

نمودار Sparkline در هر کارت

از MetricCardSparkline برای نمایش روند تغییرات در هر کارت متریک استفاده کنید. این نمودار کوچک به کاربر امکان می‌دهد بدون مراجعه به نمودارهای بزرگ، روند کلی را ببیند.

محدودیت تعداد کارت‌ها

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

چیدمان تمام‌عرض

برای صفحات داشبورد همیشه از چیدمان تمام‌عرض استفاده کنید تا نمودارها و متریک‌ها فضای کافی داشته باشند. از w-full برای کانتینر اصلی استفاده کنید.


دام‌های رایج

اشتباهات زیر در پیاده‌سازی داشبورد بارها دیده شده‌اند. برای هر مورد، اشتباه، دلیل مشکل‌ساز بودن، و الگوی درست را بررسی کنید.

کلاس‌های جهت‌دار فیزیکی به جای Logical Properties

اشتباه: استفاده از کلاس‌های فیزیکی مانند ml-*، mr-*، pl-*، text-left یا left-0 برای فاصله‌گذاری و تراز در هدر کارت‌ها و چیدمان داشبورد.

چرا مشکل‌ساز است: کل صفحه با dir="rtl" رندر می‌شود؛ کلاس فیزیکی آیکون یا برچسب را به سمت اشتباه می‌چسباند و اگر روزی نسخه چندزبانه (LTR) اضافه شود، چیدمان به‌کلی می‌شکند. خود کتابخانه پرتو این قاعده را با قانون ESLint اختصاصی (no-physical-css-properties) اجبار می‌کند — کد مصرف‌کننده نیز باید همان انضباط را رعایت کند.

الگوی درست: همیشه معادل منطقی را به کار ببرید: ms/me به جای ml/mr، ps/pe به جای pl/pr، start/end به جای left/right، و text-start به جای text-left.

// ❌ نادرست — mr-2 در صفحه RTL آیکون را به سمت اشتباه می‌چسباند
<h3 className="mb-4 text-left text-sm font-medium text-foreground">
  <TrendingUp className="mr-2 inline h-4 w-4" />
  روند منشن‌ها در طول زمان
</h3>

// ✅ درست — معادل‌های منطقی در هر دو جهت صفحه درست کار می‌کنند
<h3 className="mb-4 text-start text-sm font-medium text-foreground">
  <TrendingUp className="me-2 inline h-4 w-4" />
  روند منشن‌ها در طول زمان
</h3>

نکته: prop مربوط به margin در PartoLineChart (با کلیدهای left و right) بخشی از API بوم نمودار است، نه CSS — نیازی به تغییر آن نیست.

رنگ هاردکد به جای توکن‌های معنایی

اشتباه: استفاده از text-green-500 برای رشد، text-red-500 یا مقدار hex مانند #ef4444 برای کاهش، یا آرایه رنگ ثابت برای سری‌های نمودار.

چرا مشکل‌ساز است: سیستم دو تم دارد و رنگی که برای یک تم انتخاب شده در تم دیگر کنتراست کافی ندارد. نمودارهای پرتو رنگ‌ها را در زمان اجرا از متغیرهای CSS می‌خوانند و با تغییر تم به‌روز می‌شوند؛ رنگ هاردکد از این چرخه بیرون می‌ماند و هنگام تغییر تم ثابت باقی می‌ماند.

الگوی درست: برای درصد تغییر از variant خود کامپوننت استفاده کنید و برای عناصر سفارشی، توکن‌های معنایی (--sentiment-*، --chart-*) را به کار ببرید:

// ❌ نادرست — رنگ ثابت Tailwind خارج از سیستم توکن
<span className="text-green-500">+۸.۳٪</span>

// ✅ درست — variant توکن معنایی مناسب را اعمال می‌کند
<MetricCardDifferential variant="positive">+۸.۳٪</MetricCardDifferential>

// ✅ درست — برای عناصر سفارشی، توکن احساسات
<span className="text-[var(--sentiment-positive-text)]">+۸.۳٪</span>

آزمودن فقط در تم روشن

اشتباه: ساخت کارت‌ها با bg-white و border-gray-200 و بررسی خروجی فقط در تم روشن.

چرا مشکل‌ساز است: در @partodata/ui تم تیره پیش‌فرض است — مصرف‌کننده‌ای که هیچ تمی تنظیم نکند، تیره رندر می‌شود. کارت bg-white روی پس‌زمینه تیره به شکل لکه‌ای روشن ظاهر می‌شود و ترکیب آن با متن text-gray-900 خوانایی را از بین می‌برد.

الگوی درست: فقط از توکن‌های سطح استفاده کنید (bg-card، border-border، text-foreground، text-muted-foreground) — همان الگویی که در نمونه‌های همین صفحه به کار رفته است — و پیش از انتشار، داشبورد را در هر دو تم بررسی کنید.

// ❌ نادرست — کارت روشنِ ثابت؛ در تم تیره پیش‌فرض می‌شکند
<div className="rounded-lg border border-gray-200 bg-white p-4">
  <h3 className="text-gray-900">توزیع احساسات</h3>
</div>

// ✅ درست — توکن‌های سطح در هر دو تم رنگ صحیح می‌گیرند
<div className="rounded-lg border border-border bg-card p-4">
  <h3 className="text-foreground">توزیع احساسات</h3>
</div>

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

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

چرا مشکل‌ساز است: داده‌های سوشال لیسنینگ بزرگ‌اند — یک کمپین فعال ممکن است ده‌ها هزار منشن داشته باشد. دریافت کامل، بارگذاری اولیه را کند و حافظه مرورگر را سنگین می‌کند. DataTable پرتو از اساس برای صفحه‌بندی سمت سرور طراحی شده است: prop مربوط به pagination مقادیر currentPage، totalPages و totalRows را از بیرون می‌گیرد و نمی‌تواند آن‌ها را از داده استنتاج کند.

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

import { useState } from 'react'
import { useQuery } from '@tanstack/react-query'
import { DataTable, type DataTableColumn } from '@partodata/ui'

interface Mention {
  author: string
  text: string
  platform: string
}

const columns: DataTableColumn<Mention>[] = [
  { id: 'author', header: 'نویسنده', cell: (row) => row.author },
  { id: 'text', header: 'متن منشن', cell: (row) => row.text },
  { id: 'platform', header: 'پلتفرم', cell: (row) => row.platform },
]

function MentionsTable({ period }: { period: string }) {
  const [page, setPage] = useState(1)
  const { data, isLoading } = useQuery({
    queryKey: ['mentions', period, page],
    queryFn: () => fetchMentions({ period, page, pageSize: 20 }),
  })

  return (
    <DataTable
      columns={columns}
      data={data?.rows ?? []}
      isLoading={isLoading}
      pagination={{
        currentPage: page,
        totalPages: data?.totalPages ?? 1,
        onPageChange: setPage,
        pageSize: 20,
        totalRows: data?.totalRows,
      }}
    />
  )
}

لحن غیررسمی و ارقام ناهماهنگ

اشتباه: نوشتن متن‌های حالت خالی یا بارگذاری با لحن محاوره‌ای و نمایش هم‌زمان ارقام لاتین و فارسی در یک نما (مثلاً 12,450 در یک کارت و ۴.۲٪ در کارت کناری).

چرا مشکل‌ساز است: داشبورد یک محصول گزارش‌محور سازمانی است و مخاطب آن تحلیل‌گران و مدیران‌اند؛ لحن محاوره‌ای اعتبار گزارش را کم می‌کند و قاعده محتوایی این دیزاین‌سیستم، فارسی رسمی است. ارقام ناهماهنگ نیز مقایسه سریع متریک‌ها را دشوار می‌کند.

الگوی درست: همه متن‌ها با فارسی رسمی، و اعداد از یک مسیر قالب‌بندی واحد. ارقام را تبدیل نکنید — فونت این کار را می‌کند (فارسی‌محور بودن). برای جداکنندهٔ هزار formatNumber و برای مقدار مختصر formatLargeNumber(n, 'fa'):

import { formatNumber, formatLargeNumber } from '@partodata/ui'

// ❌ نادرست — لحن محاوره‌ای، عددِ بدون جداکننده، و تبدیل رقم در کد
<p>هنوز دیتایی نیومده، یه کم صبر کن!</p>
<MetricCardValue>12450</MetricCardValue>
<MetricCardValue>{(12450).toLocaleString('fa-IR')}</MetricCardValue>

// ✅ درست — فارسی رسمی، جداکننده از formatNumber، و ارقام از فونت
<p>داده‌ای برای این بازه زمانی ثبت نشده است.</p>
<MetricCardValue>{formatNumber(12450)}</MetricCardValue>

// ✅ مقدار مختصر، با پسوند فارسی نه پسوند لاتین
<MetricCardValue>{formatLargeNumber(12450, 'fa')}</MetricCardValue>

صفحات مرتبط