پرتوپرتو

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

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

معرفی

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

پیش‌نیاز

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

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

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

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

کل ذکرها

۱۲,۴۵۰
+۸.۳٪

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

۳۴۸
+۱۲٪

نرخ تعامل

۴.۲٪
-۰.۳٪

بازدید کل

۲.۴M
+۱۵٪

روند ذکرها

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

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

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

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

'use client'

import { useState } from 'react'
import {
  PeriodSelector,
  MetricCard,
  MetricCardHeader,
  MetricCardLabel,
  MetricCardContent,
  MetricCardValue,
  MetricCardDifferential,
  MetricCardSparkline,
  SentimentDistribution,
  PartoLineChart,
  EngagementRate,
} from '@parto-system-design/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: '2024-01-01' },
              { value: 1250, timestamp: '2024-01-02' },
              { value: 1180, timestamp: '2024-01-03' },
              { value: 1320, timestamp: '2024-01-04' },
              { value: 1400, timestamp: '2024-01-05' },
              { value: 1350, timestamp: '2024-01-06' },
              { value: 1500, timestamp: '2024-01-07' },
            ]}
            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: '2024-01-01' },
              { value: 295, timestamp: '2024-01-02' },
              { value: 310, timestamp: '2024-01-03' },
              { value: 305, timestamp: '2024-01-04' },
              { value: 320, timestamp: '2024-01-05' },
              { value: 335, timestamp: '2024-01-06' },
              { value: 348, timestamp: '2024-01-07' },
            ]}
            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: '2024-01-01' },
              { value: 4.5, timestamp: '2024-01-02' },
              { value: 4.6, timestamp: '2024-01-03' },
              { value: 4.3, timestamp: '2024-01-04' },
              { value: 4.1, timestamp: '2024-01-05' },
              { value: 4.4, timestamp: '2024-01-06' },
              { value: 4.2, timestamp: '2024-01-07' },
            ]}
            dataKey="value"
          />
        </MetricCard>

        <MetricCard>
          <MetricCardHeader>
            <MetricCardLabel icon={<Eye className="h-3.5 w-3.5" />}>بازدید کل</MetricCardLabel>
          </MetricCardHeader>
          <MetricCardContent>
            <MetricCardValue>۲.۴M</MetricCardValue>
            <MetricCardDifferential variant="positive">+۱۵.۷٪</MetricCardDifferential>
          </MetricCardContent>
          <MetricCardSparkline
            data={[
              { value: 1800000, timestamp: '2024-01-01' },
              { value: 1950000, timestamp: '2024-01-02' },
              { value: 2100000, timestamp: '2024-01-03' },
              { value: 2050000, timestamp: '2024-01-04' },
              { value: 2200000, timestamp: '2024-01-05' },
              { value: 2300000, timestamp: '2024-01-06' },
              { value: 2400000, timestamp: '2024-01-07' },
            ]}
            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>۲.۴M</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: '۲.۴M',
    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 '@parto-system-design/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('fa-IR')}</MetricCardValue>
              <MetricCardDifferential variant={metric.trend > 0 ? 'positive' : 'negative'}>
                {metric.trend > 0 ? '+' : ''}
                {metric.trend.toLocaleString('fa-IR')}٪
              </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 برای کانتینر اصلی استفاده کنید.


صفحات مرتبط