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

516 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,
} from "date-fns";
import { faIR } from "date-fns/locale";
import {
ArrowDownRight,
ArrowUpRight,
CalendarIcon,
DollarSign,
Eye,
ShoppingCart,
Users,
} from "lucide-react";
import { useMemo, useState } from "react";
import type { DateRange } from "react-day-picker";
import {
Area,
AreaChart,
CartesianGrid,
Line,
LineChart,
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 { ScrollArea } from "@/components/ui/scroll-area";
import { cn } from "@/lib/utils";
interface ChartGroup13Props {
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 percentChangeFormatter = new Intl.NumberFormat("fa-IR", {
maximumFractionDigits: 1,
minimumFractionDigits: 1,
signDisplay: "exceptZero",
});
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) * 12000) + 5000,
users: Math.floor(seededRandom(seed + 1000) * 500) + 200,
orders: Math.floor(seededRandom(seed + 2000) * 100) + 40,
pageviews: Math.floor(seededRandom(seed + 3000) * 5000) + 2000,
});
}
return data;
};
const presets = [
{ label: "امروز", getValue: () => ({ from: new Date(), to: new Date() }) },
{
label: "دیروز",
getValue: () => ({
from: subDays(new Date(), 1),
to: subDays(new Date(), 1),
}),
},
{
label: "۷ روز اخیر",
getValue: () => ({ from: subDays(new Date(), 6), to: new Date() }),
},
{
label: "۱۴ روز اخیر",
getValue: () => ({ from: subDays(new Date(), 13), to: new Date() }),
},
{
label: "۳۰ روز اخیر",
getValue: () => ({ from: subDays(new Date(), 29), to: new Date() }),
},
{
label: "۹۰ روز اخیر",
getValue: () => ({ from: subDays(new Date(), 89), to: new Date() }),
},
];
const revenueConfig = {
revenue: { label: "درآمد", color: "var(--chart-1)" },
} satisfies ChartConfig;
const usersConfig = {
users: { label: "کاربران", color: "var(--chart-2)" },
} satisfies ChartConfig;
const ordersConfig = {
orders: { label: "سفارش‌ها", color: "var(--chart-3)" },
} satisfies ChartConfig;
const pageviewsConfig = {
pageviews: { label: "بازدید صفحه", color: "var(--chart-4)" },
} satisfies ChartConfig;
const ChartGroup13 = ({ className }: ChartGroup13Props) => {
const [dateRange, setDateRange] = useState<DateRange | undefined>({
from: subDays(new Date(), 29),
to: new Date(),
});
const [presetLabel, setPresetLabel] = useState("۳۰ روز اخیر");
const filteredData = useMemo(() => {
const from = dateRange?.from ?? subDays(new Date(), 29);
const to = dateRange?.to ?? new Date();
return generateDataForRange(from, to);
}, [dateRange]);
const stats = useMemo(() => {
const totalRevenue = filteredData.reduce(
(sum, item) => sum + item.revenue,
0,
);
const totalUsers = filteredData.reduce((sum, item) => sum + item.users, 0);
const totalOrders = filteredData.reduce(
(sum, item) => sum + item.orders,
0,
);
const totalPageviews = filteredData.reduce(
(sum, item) => sum + item.pageviews,
0,
);
return { totalRevenue, totalUsers, totalOrders, totalPageviews };
}, [filteredData]);
const handlePreset = (preset: (typeof presets)[0]) => {
setDateRange(preset.getValue());
setPresetLabel(preset.label);
};
const handleCustomSelect = (range: DateRange | undefined) => {
setDateRange(range);
setPresetLabel("بازه دلخواه");
};
const dateRangeSubtitle =
dateRange?.from && dateRange?.to
? `${formatDate(dateRange.from, "d MMM y", { locale: faIR })} – ${formatDate(dateRange.to, "d MMM y", { locale: faIR })}`
: "انتخاب بازه تاریخ";
const statCards = [
{
title: "درآمد",
value: currencyFormatter.format(stats.totalRevenue),
change: 12.5,
icon: DollarSign,
config: revenueConfig,
dataKey: "revenue" as const,
color: "var(--chart-1)",
},
{
title: "کاربران",
value: numberFormatter.format(stats.totalUsers),
change: 8.2,
icon: Users,
config: usersConfig,
dataKey: "users" as const,
color: "var(--chart-2)",
},
{
title: "سفارش‌ها",
value: numberFormatter.format(stats.totalOrders),
change: -3.1,
icon: ShoppingCart,
config: ordersConfig,
dataKey: "orders" as const,
color: "var(--chart-3)",
},
{
title: "بازدید صفحه",
value: numberFormatter.format(stats.totalPageviews),
change: 15.8,
icon: Eye,
config: pageviewsConfig,
dataKey: "pageviews" as const,
color: "var(--chart-4)",
},
];
return (
<section
dir="rtl"
lang="fa"
className={cn("py-32 text-start", className)}
>
<div className="container space-y-6">
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div>
<h2 className="text-xl font-bold tracking-tight">تحلیل‌ها</h2>
<p className="text-muted-foreground">{dateRangeSubtitle}</p>
</div>
<Popover>
<PopoverTrigger asChild>
<Button
variant="outline"
className="min-w-[280px] justify-start text-start font-normal"
>
<CalendarIcon className="ms-2 size-4" aria-hidden="true" />
{presetLabel}
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start">
<div className="flex">
<div className="border-e p-2">
<ScrollArea className="h-72">
<div className="space-y-1">
{presets.map((preset) => (
<Button
key={preset.label}
variant={
presetLabel === preset.label ? "secondary" : "ghost"
}
size="sm"
className="w-full justify-start text-start"
onClick={() => handlePreset(preset)}
>
{preset.label}
</Button>
))}
</div>
</ScrollArea>
</div>
<Calendar
mode="range"
numberOfMonths={2}
locale={faIR}
selected={dateRange}
onSelect={handleCustomSelect}
defaultMonth={subDays(new Date(), 30)}
/>
</div>
</PopoverContent>
</Popover>
</div>
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
{statCards.map((stat, index) => (
<Card key={index} className="text-start">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
{stat.title}
</CardTitle>
<stat.icon className="size-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-xl font-bold tabular-nums">
{stat.value}
</div>
<div className="flex flex-wrap items-center gap-1 text-xs">
{stat.change >= 0 ? (
<ArrowUpRight className="size-3 text-green-500" />
) : (
<ArrowDownRight className="size-3 text-red-500" />
)}
<span
className={cn(
stat.change >= 0 ? "text-green-500" : "text-red-500",
"tabular-nums",
)}
>
{percentChangeFormatter.format(stat.change)}٪
</span>
<span className="text-muted-foreground">
نسبت به دوره قبل
</span>
</div>
<ChartContainer
config={stat.config}
className="mt-3 h-12 w-full"
>
<AreaChart
data={filteredData.slice(-14)}
margin={{ top: 0, right: 0, left: 0, bottom: 0 }}
>
<defs>
<linearGradient
id={`grad-${stat.dataKey}`}
x1="0"
y1="0"
x2="0"
y2="1"
>
<stop
offset="0%"
stopColor={stat.color}
stopOpacity={0.3}
/>
<stop
offset="100%"
stopColor={stat.color}
stopOpacity={0}
/>
</linearGradient>
</defs>
<Area
type="monotone"
dataKey={stat.dataKey}
stroke={stat.color}
strokeWidth={1.5}
fill={`url(#grad-${stat.dataKey})`}
/>
</AreaChart>
</ChartContainer>
</CardContent>
</Card>
))}
</div>
<div className="grid gap-4 lg:grid-cols-7">
<Card className="text-start lg:col-span-4">
<CardHeader>
<CardTitle>درآمد در طول زمان</CardTitle>
<CardDescription>درآمد روزانه در بازه انتخاب‌شده</CardDescription>
</CardHeader>
<CardContent>
<ChartContainer config={revenueConfig} className="h-80 w-full">
<AreaChart
data={filteredData}
margin={{ top: 10, right: 10, left: 0, bottom: 0 }}
>
<defs>
<linearGradient
id="revGradMain"
x1="0"
y1="0"
x2="0"
y2="1"
>
<stop
offset="0%"
stopColor="var(--color-revenue)"
stopOpacity={0.4}
/>
<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(#revGradMain)"
/>
</AreaChart>
</ChartContainer>
</CardContent>
</Card>
<Card className="text-start lg:col-span-3">
<CardHeader>
<CardTitle>کاربران و سفارش‌ها</CardTitle>
<CardDescription>
همبستگی بین کاربران و سفارش‌ها
</CardDescription>
</CardHeader>
<CardContent>
<ChartContainer
config={{ ...usersConfig, ...ordersConfig }}
className="h-80 w-full"
>
<LineChart
data={filteredData}
margin={{ top: 10, right: 10, left: 0, bottom: 0 }}
>
<CartesianGrid strokeDasharray="3 3" vertical={false} />
<XAxis
dataKey="dateStr"
axisLine={false}
tickLine={false}
tickMargin={8}
fontSize={12}
interval="preserveStartEnd"
/>
<YAxis
yAxisId="left"
axisLine={false}
tickLine={false}
tickMargin={8}
fontSize={10}
width={40}
tickFormatter={(v) => numberFormatter.format(Number(v))}
/>
<YAxis
yAxisId="right"
orientation="right"
axisLine={false}
tickLine={false}
tickMargin={8}
fontSize={10}
width={40}
tickFormatter={(v) => numberFormatter.format(Number(v))}
/>
<ChartTooltip
content={
<ChartTooltipContent
formatter={(value) =>
numberFormatter.format(Number(value))
}
/>
}
cursor={{ stroke: "var(--border)", strokeDasharray: "4 4" }}
/>
<Line
yAxisId="left"
type="monotone"
dataKey="users"
stroke="var(--color-users)"
strokeWidth={2}
dot={false}
/>
<Line
yAxisId="right"
type="monotone"
dataKey="orders"
stroke="var(--color-orders)"
strokeWidth={2}
dot={false}
/>
</LineChart>
</ChartContainer>
</CardContent>
</Card>
</div>
</div>
</section>
);
};
export { ChartGroup13 };