Files
reui-blocks/blocks/chart-group12.tsx
T

497 lines
16 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import {
addDays,
differenceInDays,
format as formatDate,
startOfDay,
subDays,
subMonths,
} from "date-fns";
import { faIR } from "date-fns/locale";
import { CalendarIcon, TrendingDown, TrendingUp } from "lucide-react";
import { useMemo, useState } from "react";
import type { DateRange } from "react-day-picker";
import {
Area,
AreaChart,
Bar,
BarChart,
CartesianGrid,
Pie,
PieChart,
XAxis,
YAxis,
} from "recharts";
import { Button } from "@/components/ui/button";
import { Calendar } from "@/components/ui/calendar";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
type ChartConfig,
ChartContainer,
ChartTooltip,
ChartTooltipContent,
} from "@/components/ui/chart";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { cn } from "@/lib/utils";
interface ChartGroup12Props {
className?: string;
}
const seededRandom = (seed: number) => {
const x = Math.sin(seed) * 10000;
return x - Math.floor(x);
};
const currencyFormatter = new Intl.NumberFormat("fa-IR", {
style: "currency",
currency: "IRR",
minimumFractionDigits: 0,
maximumFractionDigits: 0,
});
const compactCurrencyFormatter = new Intl.NumberFormat("fa-IR", {
style: "currency",
currency: "IRR",
notation: "compact",
minimumFractionDigits: 0,
maximumFractionDigits: 1,
});
const numberFormatter = new Intl.NumberFormat("fa-IR", {
maximumFractionDigits: 0,
});
const oneDecimalFormatter = new Intl.NumberFormat("fa-IR", {
maximumFractionDigits: 1,
minimumFractionDigits: 1,
});
const percentChangeFormatter = new Intl.NumberFormat("fa-IR", {
maximumFractionDigits: 1,
minimumFractionDigits: 1,
signDisplay: "exceptZero",
});
const formatPercent = (value: number) =>
`${oneDecimalFormatter.format(value)}٪`;
const generateDataForRange = (from: Date, to: Date) => {
const data = [];
const days = differenceInDays(to, from) + 1;
for (let i = 0; i < days; i++) {
const date = addDays(startOfDay(from), i);
const seed = date.getTime() / 86400000;
data.push({
date: date,
dateStr: formatDate(date, "d MMM", { locale: faIR }),
revenue: Math.floor(seededRandom(seed) * 8000) + 3000,
orders: Math.floor(seededRandom(seed + 1000) * 80) + 30,
visitors: Math.floor(seededRandom(seed + 2000) * 2000) + 500,
});
}
return data;
};
const categoryData = [
{ name: "الکترونیک", value: 4200, fill: "var(--chart-1)" },
{ name: "پوشاک", value: 3100, fill: "var(--chart-2)" },
{ name: "خانه", value: 2400, fill: "var(--chart-3)" },
{ name: "ورزش", value: 1800, fill: "var(--chart-4)" },
];
const categoryConfig = {
الکترونیک: { label: "الکترونیک", color: "var(--chart-1)" },
پوشاک: { label: "پوشاک", color: "var(--chart-2)" },
خانه: { label: "خانه", color: "var(--chart-3)" },
ورزش: { label: "ورزش", color: "var(--chart-4)" },
} satisfies ChartConfig;
const sourceData = [
{ name: "ارگانیک", value: 42 },
{ name: "پولی", value: 28 },
{ name: "شبکه‌های اجتماعی", value: 18 },
{ name: "ارجاع", value: 12 },
];
const presets = [
{ label: "۷ روز اخیر", days: 7 },
{ label: "۳۰ روز اخیر", days: 30 },
{ label: "۹۰ روز اخیر", days: 90 },
];
const revenueConfig = {
revenue: { label: "درآمد", color: "var(--chart-1)" },
} satisfies ChartConfig;
const sourceConfig = {
value: { label: "درصد", color: "var(--chart-2)" },
} satisfies ChartConfig;
const ChartGroup12 = ({ className }: ChartGroup12Props) => {
const [dateRange, setDateRange] = useState<DateRange | undefined>({
from: subDays(new Date(), 30),
to: new Date(),
});
const filteredData = useMemo(() => {
const from = dateRange?.from ?? subDays(new Date(), 30);
const to = dateRange?.to ?? new Date();
return generateDataForRange(from, to);
}, [dateRange]);
const totalRevenue = filteredData.reduce(
(sum, item) => sum + item.revenue,
0,
);
const totalOrders = filteredData.reduce((sum, item) => sum + item.orders, 0);
const totalVisitors = filteredData.reduce(
(sum, item) => sum + item.visitors,
0,
);
const conversionRate =
totalVisitors > 0 ? (totalOrders / totalVisitors) * 100 : 0;
const handlePreset = (days: number) => {
setDateRange({
from: subDays(new Date(), days),
to: new Date(),
});
};
const stats = [
{
title: "مجموع درآمد",
value: currencyFormatter.format(totalRevenue),
change: 12.5,
},
{
title: "مجموع سفارش‌ها",
value: numberFormatter.format(totalOrders),
change: 8.2,
},
{
title: "بازدیدکنندگان",
value: numberFormatter.format(totalVisitors),
change: -2.4,
},
{
title: "نرخ تبدیل",
value: formatPercent(conversionRate),
change: 5.1,
},
];
const avgOrderValue =
totalOrders > 0 ? Math.round(totalRevenue / totalOrders) : 0;
const ordersPerDay =
filteredData.length > 0
? Math.round(totalOrders / filteredData.length)
: 0;
const revenuePerDay =
filteredData.length > 0
? Math.round(totalRevenue / filteredData.length)
: 0;
const visitorsPerDay =
filteredData.length > 0
? Math.round(totalVisitors / filteredData.length)
: 0;
return (
<section
dir="rtl"
lang="fa"
className={cn("py-32 text-start", className)}
>
<div className="container space-y-4">
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div>
<h2 className="text-xl font-bold">مرور داشبورد</h2>
<p className="text-muted-foreground">
پیگیری عملکرد کسب‌وکار شما
</p>
</div>
<div className="flex flex-wrap items-center gap-2">
{presets.map((preset) => (
<Button
key={preset.days}
variant="outline"
size="sm"
onClick={() => handlePreset(preset.days)}
className={cn(
dateRange?.from &&
formatDate(dateRange.from, "yyyy-MM-dd") ===
formatDate(
subDays(new Date(), preset.days),
"yyyy-MM-dd",
) &&
"bg-accent",
)}
>
{preset.label}
</Button>
))}
<Popover>
<PopoverTrigger asChild>
<Button variant="outline" size="sm">
<CalendarIcon className="ms-2 size-4" aria-hidden="true" />
بازه دلخواه
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start">
<Calendar
mode="range"
numberOfMonths={2}
locale={faIR}
selected={dateRange}
onSelect={setDateRange}
defaultMonth={subMonths(new Date(), 1)}
/>
</PopoverContent>
</Popover>
</div>
</div>
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
{stats.map((stat, index) => (
<Card key={index} className="text-start">
<CardContent className="pt-6">
<p className="text-sm text-muted-foreground">{stat.title}</p>
<p className="text-xl font-bold tabular-nums">{stat.value}</p>
<div className="mt-1 flex items-center gap-1 text-sm">
{stat.change >= 0 ? (
<TrendingUp className="size-4 text-green-500" />
) : (
<TrendingDown className="size-4 text-red-500" />
)}
<span
className={cn(
stat.change >= 0 ? "text-green-500" : "text-red-500",
"tabular-nums",
)}
>
{percentChangeFormatter.format(stat.change)}٪
</span>
</div>
</CardContent>
</Card>
))}
</div>
<div className="grid gap-4 lg:grid-cols-3">
<Card className="text-start lg:col-span-2">
<CardHeader>
<CardTitle>روند درآمد</CardTitle>
<CardDescription>درآمد روزانه در بازه انتخاب‌شده</CardDescription>
</CardHeader>
<CardContent>
<ChartContainer config={revenueConfig} className="h-64 w-full">
<AreaChart
data={filteredData}
margin={{ top: 10, right: 10, left: 0, bottom: 0 }}
>
<defs>
<linearGradient id="revGrad12" x1="0" y1="0" x2="0" y2="1">
<stop
offset="0%"
stopColor="var(--color-revenue)"
stopOpacity={0.3}
/>
<stop
offset="100%"
stopColor="var(--color-revenue)"
stopOpacity={0}
/>
</linearGradient>
</defs>
<CartesianGrid strokeDasharray="3 3" vertical={false} />
<XAxis
dataKey="dateStr"
axisLine={false}
tickLine={false}
tickMargin={8}
fontSize={12}
interval="preserveStartEnd"
/>
<YAxis
axisLine={false}
tickLine={false}
tickMargin={8}
fontSize={10}
width={52}
tickFormatter={(v) =>
compactCurrencyFormatter.format(Number(v))
}
/>
<ChartTooltip
content={
<ChartTooltipContent
formatter={(value) =>
currencyFormatter.format(Number(value))
}
/>
}
cursor={{ stroke: "var(--border)", strokeDasharray: "4 4" }}
/>
<Area
type="monotone"
dataKey="revenue"
stroke="var(--color-revenue)"
strokeWidth={2}
fill="url(#revGrad12)"
/>
</AreaChart>
</ChartContainer>
</CardContent>
</Card>
<Card className="text-start">
<CardHeader>
<CardTitle>فروش به تفکیک دسته</CardTitle>
<CardDescription>تفکیک درآمد بر اساس دسته</CardDescription>
</CardHeader>
<CardContent>
<div className="flex flex-col items-center gap-4">
<ChartContainer config={categoryConfig} className="size-36">
<PieChart>
<ChartTooltip
content={
<ChartTooltipContent
nameKey="name"
hideLabel
formatter={(value) =>
currencyFormatter.format(Number(value))
}
/>
}
cursor={false}
/>
<Pie
data={categoryData}
cx="50%"
cy="50%"
innerRadius={45}
outerRadius={65}
paddingAngle={2}
dataKey="value"
nameKey="name"
strokeWidth={0}
/>
</PieChart>
</ChartContainer>
<div className="grid w-full grid-cols-2 gap-2 text-sm">
{categoryData.map((item, index) => (
<div key={index} className="flex items-center gap-2">
<div
className="size-2 shrink-0 rounded-full"
style={{ backgroundColor: item.fill }}
/>
<span className="truncate text-muted-foreground">
{item.name}
</span>
</div>
))}
</div>
</div>
</CardContent>
</Card>
<Card className="text-start lg:col-span-2">
<CardHeader>
<CardTitle>منابع ترافیک</CardTitle>
<CardDescription>منشأ بازدیدکنندگان شما</CardDescription>
</CardHeader>
<CardContent>
<ChartContainer config={sourceConfig} className="h-40 w-full">
<BarChart
data={sourceData}
layout="vertical"
margin={{ top: 0, right: 10, left: 0, bottom: 0 }}
>
<XAxis
type="number"
axisLine={false}
tickLine={false}
fontSize={12}
tickFormatter={(v) => formatPercent(Number(v))}
/>
<YAxis
type="category"
dataKey="name"
axisLine={false}
tickLine={false}
fontSize={12}
width={100}
/>
<ChartTooltip
content={
<ChartTooltipContent
formatter={(value) => formatPercent(Number(value))}
/>
}
cursor={{ fillOpacity: 0.05 }}
/>
<Bar
dataKey="value"
fill="var(--color-value)"
radius={[0, 4, 4, 0]}
/>
</BarChart>
</ChartContainer>
</CardContent>
</Card>
<Card className="text-start">
<CardHeader>
<CardTitle>آمار سریع</CardTitle>
<CardDescription>خلاصه شاخص‌های کلیدی</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex justify-between gap-4">
<span className="text-muted-foreground">میانگین ارزش سفارش</span>
<span className="font-semibold tabular-nums">
{currencyFormatter.format(avgOrderValue)}
</span>
</div>
<div className="flex justify-between gap-4">
<span className="text-muted-foreground">سفارش در روز</span>
<span className="font-semibold tabular-nums">
{numberFormatter.format(ordersPerDay)}
</span>
</div>
<div className="flex justify-between gap-4">
<span className="text-muted-foreground">درآمد در روز</span>
<span className="font-semibold tabular-nums">
{currencyFormatter.format(revenuePerDay)}
</span>
</div>
<div className="flex justify-between gap-4">
<span className="text-muted-foreground">بازدید در روز</span>
<span className="font-semibold tabular-nums">
{numberFormatter.format(visitorsPerDay)}
</span>
</div>
</CardContent>
</Card>
</div>
</div>
</section>
);
};
export { ChartGroup12 };