بلاک‌ها · پنل مدیریت

داشبوردِ بازاریابی

پیش‌نمایش تمام‌صفحه

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

۱. نصب کامپوننت‌ها

این بلاک از ۸ آیتم دیگ استفاده می‌کند: badgecardchart-presetscontrol-sizepersianprogresstabletoggle-group

پکیج‌منیجر پیش‌فرض Node.jsنصب سریع با لینک سخت و دیسک مشترکYarn نسخهٔ ۲ به بالا (Berry)رانتایم و پکیج‌منیجر Bun
npx digdesign@latest add https://docs.digdesign.ir/r/badge.json https://docs.digdesign.ir/r/card.json https://docs.digdesign.ir/r/chart-presets.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/progress.json https://docs.digdesign.ir/r/table.json https://docs.digdesign.ir/r/toggle-group.json
pnpm dlx digdesign@latest add https://docs.digdesign.ir/r/badge.json https://docs.digdesign.ir/r/card.json https://docs.digdesign.ir/r/chart-presets.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/progress.json https://docs.digdesign.ir/r/table.json https://docs.digdesign.ir/r/toggle-group.json
yarn dlx digdesign@latest add https://docs.digdesign.ir/r/badge.json https://docs.digdesign.ir/r/card.json https://docs.digdesign.ir/r/chart-presets.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/progress.json https://docs.digdesign.ir/r/table.json https://docs.digdesign.ir/r/toggle-group.json
bunx --bun digdesign@latest add https://docs.digdesign.ir/r/badge.json https://docs.digdesign.ir/r/card.json https://docs.digdesign.ir/r/chart-presets.json https://docs.digdesign.ir/r/control-size.json https://docs.digdesign.ir/r/persian.json https://docs.digdesign.ir/r/progress.json https://docs.digdesign.ir/r/table.json https://docs.digdesign.ir/r/toggle-group.json

۲. کد صفحه

کد را در app/<route>/page.tsx بگذارید و متن‌ها، داده و هندلرها را با محصول خودتان عوض کنید.

dashboard-marketing.tsx
"use client"

import * as React from "react"

import { Badge } from "@/components/ui/badge"
import { Card, CardAction, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { LineChart } from "@/components/ui/chart-presets"
import { ControlSizeProvider } from "@/components/ui/control-size"
import { ProgressCircle } from "@/components/ui/progress"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"
import { formatNumber, toFaDigits } from "@/lib/persian"
import { stockImages, type StockImageName } from "@/lib/stock-images"

const KPIS = [
  { label: "بازدید", value: "۹۸۲", delta: "+٪۵", good: true },
  { label: "بازگشتِ سرمایه", value: "+٪۱۵", delta: "+٪۳", good: true },
  { label: "تبدیل", value: "۴۷۳", delta: "+٪۹", good: true },
  { label: "هزینه", value: "۸۸ م", delta: "−٪۲", good: true },
  { label: "نرخِ کلیک", value: "٪۲٫۵", delta: "−٪۰٫۲", good: false },
  { label: "هزینهٔ هر جذب", value: "۵۴ ه", delta: "−٪۳", good: true },
]
const TREND = [
  { label: "تیر", traffic: 42, roi: 30, conversions: 22 },
  { label: "", traffic: 55, roi: 34, conversions: 25 },
  { label: "", traffic: 48, roi: 38, conversions: 28 },
  { label: "مرداد", traffic: 63, roi: 36, conversions: 31 },
  { label: "", traffic: 58, roi: 44, conversions: 30 },
  { label: "", traffic: 71, roi: 47, conversions: 36 },
  { label: "شهریور", traffic: 66, roi: 52, conversions: 39 },
  { label: "", traffic: 78, roi: 55, conversions: 42 },
]
type Quality = "review" | "good" | "neutral" | "irrelevant"
const QUALITY: Record<Quality, { label: string; color: "warning" | "success" | "default" | "destructive" }> = {
  review: { label: "نیاز به بررسی", color: "warning" },
  good: { label: "خوب", color: "success" },
  neutral: { label: "خنثی", color: "default" },
  irrelevant: { label: "نامرتبط", color: "destructive" },
}
const LEADS: { id: string; date: string; company: string; email: string; quality: Quality; city: string; fresh?: boolean }[] = [
  { id: "1", date: "امروز، ۱۴:۱۲", company: "—", email: "contact@helpdesk.ir", quality: "review", city: "تهران", fresh: true },
  { id: "2", date: "امروز، ۰۸:۳۴", company: "نوآوران", email: "s.chen@noavaran.ir", quality: "review", city: "شیراز", fresh: true },
  { id: "3", date: "۱۴ مهر، ۱۷:۵۷", company: "وب‌میل", email: "maria@webmail.ir", quality: "good", city: "اصفهان" },
  { id: "4", date: "۱۴ مهر، ۱۳:۴۰", company: "—", email: "john@startup.ir", quality: "good", city: "مشهد" },
  { id: "5", date: "۱۳ مهر، ۱۰:۲۲", company: "دیزاین‌هاب", email: "hello@designhub.ir", quality: "neutral", city: "تبریز" },
  { id: "6", date: "۱۲ مهر، ۰۹:۰۱", company: "پروتون", email: "k@proton.ir", quality: "irrelevant", city: "کرج" },
]
const POSTS: { image: StockImageName; date: string }[] = [
  { image: "projectMobileApp", date: "۳۰ شهریور" },
  { image: "projectDashboard", date: "۱ مهر" },
  { image: "coverWorkspace", date: "۳ مهر" },
  { image: "projectCafeBrand", date: "۵ مهر" },
]

/**
 * داشبوردِ بازاریابی: نوارِ شش شاخص با تغییرِ رنگی (با علامتِ + و − نه فقط رنگ)، نمودارِ روندِ
 * سه‌سری با بازهٔ سگمنت، جدولِ سرنخ‌ها با کیفیتِ برچسب‌دار و نشانِ تازه، و ستونِ شبکه‌های اجتماعی
 * با دنبال‌کننده، حلقهٔ تعامل، ریزِ آمار و بهترین پست‌ها. جدول در موبایل اسکرولِ افقیِ خودش را دارد.
 */
export default function DashboardMarketingBlock() {
  const [range, setRange] = React.useState("3m")
  const [tab, setTab] = React.useState("leads")
  const data = range === "1m" ? TREND.slice(-3) : TREND

  return (
    <ControlSizeProvider size="xl" asChild>
      <main className="mx-auto grid w-full max-w-6xl grid-cols-1 items-start gap-6 p-4 sm:p-6 xl:grid-cols-[minmax(0,1fr)_19rem]">
        <div className="flex min-w-0 flex-col gap-6">
          <header className="flex flex-col gap-1">
            <h1 className="text-2xl leading-snug font-black">نمای زنده</h1>
            <p className="text-muted-foreground">کمپینِ پاییز · به‌روز تا ۵ دقیقه پیش</p>
          </header>

          <ul className="grid grid-cols-2 divide-border rounded-xl border border-border bg-card shadow-card sm:grid-cols-3 lg:grid-cols-6 [&>li]:border-border max-sm:[&>li:nth-child(odd)]:border-e max-sm:[&>li:nth-child(-n+4)]:border-b sm:max-lg:[&>li:nth-child(-n+3)]:border-b sm:max-lg:[&>li:not(:nth-child(3n))]:border-e lg:[&>li:not(:last-child)]:border-e">
            {KPIS.map((kpi) => (
              <li key={kpi.label} className="flex flex-col gap-1 p-4">
                <span className="text-xs text-muted-foreground">{kpi.label}</span>
                <span className="flex items-baseline gap-1.5">
                  <span className="text-lg font-black tabular-nums">{kpi.value}</span>
                  <bdi dir="ltr" className={`text-xs font-semibold ${kpi.good ? "text-success" : "text-destructive"}`}>
                    {kpi.delta}
                  </bdi>
                </span>
              </li>
            ))}
          </ul>

          <Card>
            <CardHeader>
              <CardTitle className="text-lg">روندها</CardTitle>
              <CardAction>
                <ToggleGroup type="single" variant="segmented" value={range} onValueChange={(value) => value && setRange(value as string)} aria-label="بازه">
                  <ToggleGroupItem value="1m">۱ ماه</ToggleGroupItem>
                  <ToggleGroupItem value="3m">۳ ماه</ToggleGroupItem>
                </ToggleGroup>
              </CardAction>
            </CardHeader>
            <CardContent>
              <LineChart
                className="h-60"
                data={data}
                categoryKey="label"
                smooth
                showPoints={false}
                series={[
                  { key: "traffic", label: "بازدید", color: "var(--dig-orange-500)" },
                  { key: "roi", label: "بازگشتِ سرمایه", color: "var(--dig-blue-500)" },
                  { key: "conversions", label: "تبدیل", color: "var(--dig-violet-400)" },
                ]}
                valueFormatter={(value) => formatNumber(value)}
              />
            </CardContent>
          </Card>

          <Card className="gap-4">
            <CardHeader>
              <ToggleGroup type="single" variant="segmented" value={tab} onValueChange={(value) => value && setTab(value as string)} aria-label="گزارش" className="max-w-full overflow-x-auto">
                <ToggleGroupItem value="leads">کیفیتِ سرنخ</ToggleGroupItem>
                <ToggleGroupItem value="daily">روزبه‌روز</ToggleGroupItem>
                <ToggleGroupItem value="keywords">کلیدواژه‌ها</ToggleGroupItem>
              </ToggleGroup>
            </CardHeader>
            <CardContent>
              <Table stackOnMobile className="min-w-[38rem]">
                <TableHeader>
                  <TableRow className="hover:bg-card">
                    <TableHead>تاریخ</TableHead>
                    <TableHead>شرکت</TableHead>
                    <TableHead>ایمیل</TableHead>
                    <TableHead>کیفیت</TableHead>
                    <TableHead>شهر</TableHead>
                  </TableRow>
                </TableHeader>
                <TableBody>
                  {LEADS.map((lead) => (
                    <TableRow key={lead.id}>
                      <TableCell className="font-medium whitespace-nowrap">
                        <span className="flex items-center gap-2">
                          {lead.fresh && <span aria-label="تازه" className="size-2 rounded-full bg-[var(--dig-orange-500)]" />}
                          {lead.date}
                        </span>
                      </TableCell>
                      <TableCell className={lead.company === "—" ? "text-muted-foreground" : "font-semibold"}>{lead.company === "—" ? "نامشخص" : lead.company}</TableCell>
                      <TableCell>
                        <bdi dir="ltr" className="text-muted-foreground">
                          {lead.email}
                        </bdi>
                      </TableCell>
                      <TableCell>
                        <Badge variant="soft" color={QUALITY[lead.quality].color}>
                          {QUALITY[lead.quality].label}
                        </Badge>
                      </TableCell>
                      <TableCell>{lead.city}</TableCell>
                    </TableRow>
                  ))}
                </TableBody>
              </Table>
            </CardContent>
          </Card>
        </div>

        <aside className="flex flex-col gap-6">
          <Card className="gap-4">
            <CardHeader>
              <CardTitle className="text-lg">اینستاگرام</CardTitle>
              <CardAction>
                <span className="text-xs text-muted-foreground">۷ روز</span>
              </CardAction>
            </CardHeader>
            <CardContent className="flex flex-col gap-4">
              <div className="grid grid-cols-2 gap-3 xl:grid-cols-1">
                <div className="flex flex-col gap-1 rounded-lg bg-subtle p-3">
                  <span className="text-xs text-muted-foreground">دنبال‌کننده</span>
                  <span className="flex items-baseline gap-1.5">
                    <span className="text-lg font-black tabular-nums">{formatNumber(135_145)}</span>
                    <bdi dir="ltr" className="text-xs font-semibold text-success">
                      +{toFaDigits(16)}
                    </bdi>
                  </span>
                </div>
                <div className="flex items-center justify-between gap-2 rounded-lg bg-subtle p-3">
                  <span className="flex flex-col gap-1">
                    <span className="text-xs text-muted-foreground">تعامل</span>
                    <span className="text-lg font-black tabular-nums">{formatNumber(1_356)}</span>
                  </span>
                  <ProgressCircle value={68} size="md" aria-label="نرخِ تعامل ۶۸ درصد" className="[--progress:var(--dig-orange-500)]" />
                </div>
              </div>
              <dl className="grid grid-cols-2 gap-x-4 gap-y-2 text-sm">
                {[
                  ["پسند", 697],
                  ["نظر", 22],
                  ["ذخیره", 566],
                  ["اشتراک", 71],
                ].map(([label, value]) => (
                  <div key={label} className="flex justify-between gap-2">
                    <dt className="text-muted-foreground">{label}</dt>
                    <dd className="font-semibold tabular-nums">{formatNumber(value as number)}</dd>
                  </div>
                ))}
              </dl>
              <div className="flex flex-col gap-2">
                <span className="text-sm font-bold">بهترین پست‌ها</span>
                <ul className="grid grid-cols-4 gap-2">
                  {POSTS.map((post) => (
                    <li key={post.date} className="flex flex-col items-center gap-1">
                      {/* eslint-disable-next-line @next/next/no-img-element -- عکس از کتابخانهٔ تصویرِ دیگ */}
                      <img src={stockImages[post.image].src} alt="" loading="lazy" className="aspect-square w-full rounded-lg object-cover" />
                      <span className="text-[0.6875rem] text-muted-foreground">{post.date}</span>
                    </li>
                  ))}
                </ul>
              </div>
            </CardContent>
          </Card>
        </aside>
      </main>
    </ControlSizeProvider>
  )
}