Files
reui-blocks/blocks/dashboard12.tsx
T

1807 lines
90 KiB
TypeScript
Raw Normal View History

"use client";
import DottedMap from "dotted-map/without-countries";
import { useMotionValueEvent, useSpring } from "framer-motion";
import {
ArrowDownRight,
ArrowUpRight,
BarChart3,
Box,
ChevronRight,
ChevronsUpDown,
ClipboardList,
Facebook,
Globe,
Instagram,
LayoutDashboard,
LogOut,
MessageSquare,
Package,
RotateCcw,
Search,
Settings,
Truck,
Twitter,
User,
Users,
Wallet,
} from "lucide-react";
import * as React from "react";
import type { TooltipProps } from "recharts";
import { Bar, BarChart, ReferenceLine, Tooltip, XAxis, YAxis } from "recharts";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Button } from "@/components/ui/button";
import { type ChartConfig, ChartContainer } from "@/components/ui/chart";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { ScrollArea } from "@/components/ui/scroll-area";
import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupContent,
SidebarGroupLabel,
SidebarHeader,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
SidebarMenuSub,
SidebarMenuSubButton,
SidebarMenuSubItem,
SidebarInset,
SidebarProvider,
SidebarRail,
SidebarTrigger,
} from "@/components/ui/sidebar";
import { TooltipProvider } from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
// ============================================================================
// Color Palette
// ============================================================================
const mixBase = "var(--background)";
const palette = {
primary: "var(--primary)",
secondary: {
light: `color-mix(in oklch, var(--primary) 75%, ${mixBase})`,
dark: `color-mix(in oklch, var(--primary) 85%, ${mixBase})`,
},
tertiary: {
light: `color-mix(in oklch, var(--primary) 55%, ${mixBase})`,
dark: `color-mix(in oklch, var(--primary) 65%, ${mixBase})`,
},
quaternary: {
light: `color-mix(in oklch, var(--primary) 40%, ${mixBase})`,
dark: `color-mix(in oklch, var(--primary) 45%, ${mixBase})`,
},
quinary: {
light: `color-mix(in oklch, var(--primary) 25%, ${mixBase})`,
dark: `color-mix(in oklch, var(--primary) 30%, ${mixBase})`,
},
};
// ============================================================================
// Types
// ============================================================================
type NavItem = {
label: string;
icon: React.ComponentType<React.SVGProps<SVGSVGElement>>;
href: string;
isActive?: boolean;
children?: NavItem[];
};
type NavGroup = {
title: string;
items: NavItem[];
defaultOpen?: boolean;
};
type UserData = {
name: string;
email: string;
avatar: string;
};
type SidebarData = {
logo: {
src: string;
alt: string;
title: string;
description: string;
};
navGroups: NavGroup[];
user?: UserData;
};
type KPIStat = {
title: string;
value: number;
change: number;
format: "currency" | "percent" | "number";
};
type CustomerSegment = {
name: string;
percentage: number;
customers: number;
change: number;
color: string;
};
type Channel = {
icon: React.ComponentType<React.SVGProps<SVGSVGElement>>;
name: string;
count: number;
change: number;
};
type GeographyCountry = {
name: string;
flag: string;
lat: number;
lng: number;
users: number;
demographics: { men: number; women: number; other: number };
};
// ============================================================================
// Formatters
// ============================================================================
const currencyFormatter = new Intl.NumberFormat("fa-IR", {
style: "currency",
currency: "IRR",
maximumFractionDigits: 0,
});
const compactCurrencyFormatter = new Intl.NumberFormat("fa-IR", {
style: "currency",
currency: "IRR",
notation: "compact",
maximumFractionDigits: 1,
});
const numberFormatter = new Intl.NumberFormat("fa-IR");
const compactNumberFormatter = new Intl.NumberFormat("fa-IR", {
notation: "compact",
maximumFractionDigits: 2,
});
const percentFormatter = new Intl.NumberFormat("fa-IR", {
style: "percent",
maximumFractionDigits: 1,
});
const monthFormatter = new Intl.DateTimeFormat("fa-IR", { month: "short" });
const monthLabel = (monthIndex: number) =>
monthFormatter.format(new Date(2025, monthIndex, 1));
const formatChangePercent = (change: number, signed = true) => {
const formatted = Math.abs(change).toLocaleString("fa-IR", {
minimumFractionDigits: change % 1 !== 0 ? 1 : 0,
maximumFractionDigits: 1,
});
if (!signed) return `${formatted}٪`;
return `${change >= 0 ? "+" : "-"}${formatted}٪`;
};
type OrderStatus = "Delivered" | "Processing" | "Cancelled";
const orderStatusLabels: Record<OrderStatus, string> = {
Delivered: "تحویل‌شده",
Processing: "در حال پردازش",
Cancelled: "لغوشده",
};
// ============================================================================
// Hooks
// ============================================================================
function useHoverHighlight<T extends string | number>() {
const [active, setActive] = React.useState<T | null>(null);
const handleHover = React.useCallback((value: T | null) => {
setActive(value);
}, []);
return { active, handleHover };
}
// ============================================================================
// Mock Data
// ============================================================================
const sidebarData: SidebarData = {
logo: {
src: "/block-media/shadcnblocks/block/logos/NovaSales-logo.svg",
alt: "فروشگاه آکمه",
title: "ری یو آی Store",
description: "داشبورد",
},
navGroups: [
{
title: "اصلی",
defaultOpen: true,
items: [
{
label: "داشبورد",
icon: LayoutDashboard,
href: "#",
isActive: true,
},
{ label: "سفارش‌ها", icon: ClipboardList, href: "#" },
{ label: "مرجوعی‌ها", icon: RotateCcw, href: "#" },
],
},
{
title: "کاتالوگ",
defaultOpen: true,
items: [
{
label: "محصولات",
icon: Box,
href: "#",
children: [
{ label: "همه محصولات", icon: Package, href: "#" },
{ label: "دسته‌بندی‌ها", icon: Package, href: "#" },
{ label: "موجودی", icon: Package, href: "#" },
],
},
{ label: "ارسال", icon: Truck, href: "#" },
],
},
{
title: "مشتریان",
defaultOpen: false,
items: [
{ label: "همه مشتریان", icon: Users, href: "#" },
{ label: "پیام‌ها", icon: MessageSquare, href: "#" },
],
},
{
title: "تحلیل فروش",
defaultOpen: false,
items: [
{ label: "نمای کلی", icon: Globe, href: "#" },
{ label: "گزارش‌ها", icon: BarChart3, href: "#" },
{ label: "مالی", icon: Wallet, href: "#" },
],
},
{
title: "تنظیمات",
defaultOpen: false,
items: [{ label: "تنظیمات", icon: Settings, href: "#" }],
},
],
user: {
name: "علی رضایی",
email: "john@acmestore.com",
avatar:
"/block-media/shadcnblocks/block/avatar-6.webp",
},
};
const kpiStats: KPIStat[] = [
{ title: "درآمد کل", value: 485000, change: -12.5, format: "currency" },
{ title: "نرخ تبدیل", value: 18.5, change: -12.5, format: "percent" },
{ title: "کل سفارش‌ها", value: 92, change: 25, format: "number" },
{ title: "مشتریان فعال", value: 38, change: 25, format: "number" },
];
const monthlyRevenueData = [
{ month: monthLabel(0), revenue: 72000 },
{ month: monthLabel(1), revenue: 128000 },
{ month: monthLabel(2), revenue: 103000 },
{ month: monthLabel(3), revenue: 176000 },
{ month: monthLabel(4), revenue: 230000 },
{ month: monthLabel(5), revenue: 142000 },
{ month: monthLabel(6), revenue: 310000 },
{ month: monthLabel(7), revenue: 640000 },
{ month: monthLabel(8), revenue: 410000 },
{ month: monthLabel(9), revenue: 210000 },
{ month: monthLabel(10), revenue: 98000 },
{ month: monthLabel(11), revenue: 260000 },
];
const customerSegmentsData: CustomerSegment[] = [
{
name: "سازمانی",
percentage: 45,
customers: 1074,
color: palette.primary,
change: 3.2,
},
{
name: "کسب‌وکار متوسط",
percentage: 35,
customers: 836,
color: palette.secondary.light,
change: -1.5,
},
{
name: "شخصی",
percentage: 20,
customers: 478,
color: palette.tertiary.light,
change: 5.8,
},
];
const totalCustomers = 2388;
const totalCustomersChange = 4.2;
const channelsData: Channel[] = [
{ icon: Facebook, name: "فیس‌بوک", count: 2341, change: 25 },
{ icon: Twitter, name: "توییتر", count: 1231, change: -25 },
{ icon: Globe, name: "گوگل", count: 1123, change: -25 },
{ icon: Instagram, name: "اینستاگرام", count: 125, change: 25 },
];
const geographyCountries: GeographyCountry[] = [
{
name: "ایالات متحده",
flag: "\u{1F1FA}\u{1F1F8}",
lat: 38.9,
lng: -77.0,
users: 12400,
demographics: { men: 38, women: 54, other: 8 },
},
{
name: "بریتانیا",
flag: "\u{1F1EC}\u{1F1E7}",
lat: 51.5,
lng: -0.1,
users: 8200,
demographics: { men: 35, women: 57, other: 8 },
},
{
name: "هند",
flag: "\u{1F1EE}\u{1F1F3}",
lat: 20.6,
lng: 79.0,
users: 6500,
demographics: { men: 42, women: 50, other: 8 },
},
{
name: "برزیل",
flag: "\u{1F1E7}\u{1F1F7}",
lat: -14.2,
lng: -51.9,
users: 4200,
demographics: { men: 30, women: 62, other: 8 },
},
{
name: "ژاپن",
flag: "\u{1F1EF}\u{1F1F5}",
lat: 36.2,
lng: 139.7,
users: 3400,
demographics: { men: 32, women: 60, other: 8 },
},
];
const totalGeographyUsers = geographyCountries.reduce(
(sum, c) => sum + c.users,
0,
);
// Inline pre-computed dotted map grid (from dotted-map/without-countries)
// prettier-ignore
const mapJsonData = { "points": { "11;0": { "x": 11.5, "y": 0 }, "12;0": { "x": 12.5, "y": 0 }, "16;0": { "x": 16.5, "y": 0 }, "17;0": { "x": 17.5, "y": 0 }, "18;0": { "x": 18.5, "y": 0 }, "22;0": { "x": 22.5, "y": 0 }, "23;0": { "x": 23.5, "y": 0 }, "24;0": { "x": 24.5, "y": 0 }, "25;0": { "x": 25.5, "y": 0 }, "26;0": { "x": 26.5, "y": 0 }, "27;0": { "x": 27.5, "y": 0 }, "36;0": { "x": 36.5, "y": 0 }, "41;0": { "x": 41.5, "y": 0 }, "43;0": { "x": 43.5, "y": 0 }, "44;0": { "x": 44.5, "y": 0 }, "45;0": { "x": 45.5, "y": 0 }, "46;0": { "x": 46.5, "y": 0 }, "47;0": { "x": 47.5, "y": 0 }, "48;0": { "x": 48.5, "y": 0 }, "49;0": { "x": 49.5, "y": 0 }, "50;0": { "x": 50.5, "y": 0 }, "51;0": { "x": 51.5, "y": 0 }, "52;0": { "x": 52.5, "y": 0 }, "53;0": { "x": 53.5, "y": 0 }, "55;0": { "x": 55.5, "y": 0 }, "56;0": { "x": 56.5, "y": 0 }, "57;0": { "x": 57.5, "y": 0 }, "3;1": { "x": 3, "y": 0.8660254037844386 }, "4;1": { "x": 4, "y": 0.8660254037844386 }, "5;1": { "x": 5, "y": 0.8660254037844386 }, "6;1": { "x": 6, "y": 0.8660254037844386 }, "7;1": { "x": 7, "y": 0.8660254037844386 }, "8;1": { "x": 8, "y": 0.8660254037844386 }, "9;1": { "x": 9, "y": 0.8660254037844386 }, "10;1": { "x": 10, "y": 0.8660254037844386 }, "12;1": { "x": 12, "y": 0.8660254037844386 }, "13;1": { "x": 13, "y": 0.8660254037844386 }, "14;1": { "x": 14, "y": 0.8660254037844386 }, "15;1": { "x": 15, "y": 0.8660254037844386 }, "17;1": { "x": 17, "y": 0.8660254037844386 }, "19;1": { "x": 19, "y": 0.8660254037844386 }, "23;1": { "x": 23, "y": 0.8660254037844386 }, "24;1": { "x": 24, "y": 0.8660254037844386 }, "25;1": { "x": 25, "y": 0.8660254037844386 }, "26;1": { "x": 26, "y": 0.8660254037844386 }, "27;1": { "x": 27, "y": 0.8660254037844386 }, "35;1": { "x": 35, "y": 0.8660254037844386 }, "36;1": { "x": 36, "y": 0.8660254037844386 }, "37;1": { "x": 37, "y": 0.8660254037844386 }, "44;1": { "x": 44, "y": 0.8660254037844386 }, "45;1": { "x": 45, "y": 0.8660254037844386 }, "46;1": { "x": 46, "y": 0.8660254037844386 }, "47;1": { "x": 47, "y": 0.8660254037844386 }, "48;1": { "x": 48, "y": 0.8660254037844386 }, "49;1": { "x": 49, "y": 0.8660254037844386 }, "50;1": { "x": 50, "y": 0.8660254037844386 }, "51;1": { "x": 51, "y": 0.8660254037844386 }, "52;1": { "x": 52, "y": 0.8660254037844386 }, "53;1": { "x": 53, "y": 0.8660254037844386 }, "54;1": { "x": 54, "y": 0.8660254037844386 }, "55;1": { "x": 55, "y": 0.8660254037844386 }, "56;1": { "x": 56, "y": 0.8660254037844386 }, "57;1": { "x": 57, "y": 0.8660254037844386 }, "58;1": { "x": 58, "y": 0.8660254037844386 }, "59;1": { "x": 59, "y": 0.8660254037844386 }, "60;1": { "x": 60, "y": 0.8660254037844386 }, "61;1": { "x": 61, "y": 0.8660254037844386 }, "62;1": { "x": 62, "y": 0.8660254037844386 }, "0;2": { "x": 0.5, "y": 1.7320508075688772 }, "3;2": { "x": 3.5, "y": 1.7320508075688772 }, "4;2": { "x": 4.5, "y": 1.7320508075688772 }, "5;2": { "x": 5.5, "y": 1.7320508075688772 }, "6;2": { "x": 6.5, "y": 1.7320508075688772 }, "7;2": { "x": 7.5, "y": 1.7320508075688772 }, "8;2": { "x": 8.5, "y": 1.7320508075688772 }, "9;2": { "x": 9.5, "y": 1.7320508075688772 }, "10;2": { "x": 10.5, "y": 1.7320508075688772 }, "11;2": { "x": 11.5, "y": 1.7320508075688772 }, "12;2": { "x": 12.5, "y": 1.7320508075688772 }, "13;2": { "x": 13.5, "y": 1.7320508075688772 }, "14;2": { "x": 14.5, "y": 1.7320508075688772 }, "15;2": { "x": 15.5, "y": 1.7320508075688772 }, "16;2": { "x": 16.5, "y": 1.7320508075688772 }, "19;2": { "x": 19.5, "y": 1.7320508075688772 }, "22;2": { "x": 22.5, "y": 1.7320508075688772 }, "23;2": { "x": 23.5, "y": 1.7320508075688772 }, "24;2": { "x": 24.5, "y": 1.7320508075688772 }, "25;2": { "x": 25.5, "y": 1.7320508075688772 }, "34;2": { "x": 34.5, "y": 1.7320508075688772 }, "35;2": { "x": 35.5, "y": 1.7320508075688772 }, "36;2": { "x": 36.5, "y": 1.7320508075688772 }, "37;2": { "x": 37.5, "y": 1.7320508075688772 }, "38;2": { "x": 38.5, "y": 1.7320508075688772 }, "39;2": { "x": 39.5, "y": 1.7320508075688772 }, "40;2": { "x": 40.5, "y": 1.7320508075688772 }, "41;2": { "x": 41.5, "y": 1.7320508075688772 }, "42;2": { "x": 42.5, "y"
// Pre-compute dotted map data at module scope
const dottedMap = new DottedMap({ map: mapJsonData as any });
const geographyPinPositions = geographyCountries.map((country) =>
dottedMap.addPin({
lat: country.lat,
lng: country.lng,
svgOptions: { color: "transparent", radius: 0 },
}),
);
const mapBasePoints = dottedMap.getPoints();
const SPOTLIGHT_RADIUS = 4;
const spotlightMaps: Map<number, number>[] = geographyPinPositions.map(
(pin) => {
const dotMap = new Map<number, number>();
mapBasePoints.forEach((dot, dotIndex) => {
const dx = dot.x - pin.x;
const dy = dot.y - pin.y;
const distSq = dx * dx + dy * dy;
const radiusSq = SPOTLIGHT_RADIUS * SPOTLIGHT_RADIUS;
if (distSq <= radiusSq) {
const dist = Math.sqrt(distSq);
const t = dist / SPOTLIGHT_RADIUS;
dotMap.set(dotIndex, 0.35 - t * (0.35 - 0.12));
}
});
return dotMap;
},
);
const orderStatuses = {
Delivered: "text-emerald-600 bg-emerald-600/10",
Processing: "text-amber-500 bg-amber-500/10",
Cancelled: "text-red-600 bg-red-600/10",
};
const recentOrders = [
{
customer: { name: "سارا احمدی", avatar: "سا" },
orderId: "#ORD-7821",
product: "هدفون بی‌سیم",
status: "Delivered" as const,
amount: 129.99,
date: "۲۳ دقیقه پیش",
},
{
customer: { name: "محمد کریمی", avatar: "مک" },
orderId: "#ORD-7820",
product: "کفش دویدن",
status: "Processing" as const,
amount: 89.95,
date: "۱ ساعت پیش",
},
{
customer: { name: "نازنین موسوی", avatar: "نم" },
orderId: "#ORD-7819",
product: "کرم صورت ارگانیک",
status: "Delivered" as const,
amount: 34.5,
date: "۲ ساعت پیش",
},
{
customer: { name: "رضا نوری", avatar: "رن" },
orderId: "#ORD-7818",
product: "اسپیکر بلوتوث",
status: "Cancelled" as const,
amount: 199.0,
date: "۳ ساعت پیش",
},
{
customer: { name: "فاطمه رحیمی", avatar: "فر" },
orderId: "#ORD-7817",
product: "مت یوگا حرفه‌ای",
status: "Delivered" as const,
amount: 45.0,
date: "۵ ساعت پیش",
},
];
const secondaryNavigation = [
{ name: "نمای کلی", href: "#", current: true },
{ name: "سفارش‌ها", href: "#", current: false },
{ name: "محصولات", href: "#", current: false },
{ name: "مشتریان", href: "#", current: false },
{ name: "تحلیل فروش", href: "#", current: false },
];
// ============================================================================
// Chart Configs
// ============================================================================
const revenueChartConfig = {
revenue: {
label: "درآمد",
color: palette.secondary.light,
},
} satisfies ChartConfig;
// ============================================================================
// Sidebar Components
// ============================================================================
const SidebarLogo = ({ logo }: { logo: SidebarData["logo"] }) => {
return (
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton size="lg" tooltip={logo.title}>
<div className="flex aspect-square size-8 items-center justify-center bg-primary">
<img
src={logo.src}
alt={logo.alt}
width={24}
height={24}
className="size-6 text-primary-foreground invert dark:invert-0"
/>
</div>
<div className="flex flex-col gap-0.5 leading-none">
<span className="font-medium">{logo.title}</span>
<span className="text-xs text-muted-foreground">
{logo.description}
</span>
</div>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
);
};
const NavMenuItem = ({ item }: { item: NavItem }) => {
const Icon = item.icon;
const hasChildren = item.children && item.children.length > 0;
if (!hasChildren) {
return (
<SidebarMenuItem>
<SidebarMenuButton
asChild
isActive={item.isActive}
tooltip={item.label}
>
<a href={item.href}>
<Icon className="size-4" aria-hidden="true" />
<span>{item.label}</span>
</a>
</SidebarMenuButton>
</SidebarMenuItem>
);
}
return (
<Collapsible asChild defaultOpen className="group/collapsible">
<SidebarMenuItem>
<CollapsibleTrigger asChild>
<SidebarMenuButton isActive={item.isActive} tooltip={item.label}>
<Icon className="size-4" aria-hidden="true" />
<span>{item.label}</span>
<ChevronRight
className="ms-auto size-4 transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90 rtl:rotate-180"
aria-hidden="true"
/>
</SidebarMenuButton>
</CollapsibleTrigger>
<CollapsibleContent>
<SidebarMenuSub>
{item.children!.map((child) => (
<SidebarMenuSubItem key={child.label}>
<SidebarMenuSubButton
asChild
isActive={child.isActive}
className={
child.isActive
? "relative overflow-visible before:absolute before:top-0 before:-start-2.5 before:z-10 before:h-full before:w-px before:bg-primary before:content-['']"
: ""
}
>
<a href={child.href}>{child.label}</a>
</SidebarMenuSubButton>
</SidebarMenuSubItem>
))}
</SidebarMenuSub>
</CollapsibleContent>
</SidebarMenuItem>
</Collapsible>
);
};
const NavUser = ({ user }: { user: UserData }) => {
const initials = user.name
.split(" ")
.map((n) => n[0])
.join("");
const userAvatar = (
<Avatar className="size-8 rounded-none">
<AvatarImage src={user.avatar} alt={user.name} />
<AvatarFallback className="rounded-none">{initials}</AvatarFallback>
</Avatar>
);
const userInfo = (
<div className="grid flex-1 text-start text-sm leading-tight">
<span className="truncate font-medium">{user.name}</span>
<span className="truncate text-xs text-muted-foreground">
{user.email}
</span>
</div>
);
return (
<SidebarMenu>
<SidebarMenuItem>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<SidebarMenuButton
size="lg"
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
>
{userAvatar}
{userInfo}
<ChevronsUpDown className="ms-auto size-4" aria-hidden="true" />
</SidebarMenuButton>
</DropdownMenuTrigger>
<DropdownMenuContent
className="w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-none"
side="bottom"
align="end"
sideOffset={4}
>
<DropdownMenuLabel className="p-0 font-normal">
<div className="flex items-center gap-2 px-1 py-1.5 text-start text-sm">
{userAvatar}
{userInfo}
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem>
<User className="me-2 size-4" aria-hidden="true" />
حساب کاربری
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem>
<LogOut className="me-2 size-4" aria-hidden="true" />
خروج
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuItem>
</SidebarMenu>
);
};
const AppSidebar = ({ ...props }: React.ComponentProps<typeof Sidebar>) => {
return (
<Sidebar side="right" variant="inset" collapsible="icon" {...props}>
<SidebarHeader>
<div className="flex items-center gap-2 group-data-[collapsible=icon]:flex-col">
<SidebarLogo logo={sidebarData.logo} />
<SidebarTrigger className="ms-auto group-data-[collapsible=icon]:ms-0" />
</div>
</SidebarHeader>
<SidebarContent>
<ScrollArea className="h-full">
{sidebarData.navGroups.map((group) => (
<SidebarGroup key={group.title}>
<SidebarGroupLabel>{group.title}</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>
{group.items.map((item) => (
<NavMenuItem key={item.label} item={item} />
))}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
))}
</ScrollArea>
</SidebarContent>
<SidebarFooter>
{sidebarData.user && <NavUser user={sidebarData.user} />}
</SidebarFooter>
<SidebarRail />
</Sidebar>
);
};
// ============================================================================
// Dashboard Header
// ============================================================================
const DashboardHeader = () => {
return (
<header className="sticky top-0 z-40 flex h-16 shrink-0 items-center gap-4 border-b bg-background px-4 sm:px-6">
<div className="flex flex-1 gap-4 self-stretch">
<form className="grid flex-1 grid-cols-1">
<input
name="search"
type="search"
placeholder="جستجو…"
aria-label="جستجوی داشبورد"
autoComplete="off"
className="col-start-1 row-start-1 block size-full bg-transparent ps-9 text-sm text-foreground outline-hidden placeholder:text-muted-foreground"
/>
<Search
aria-hidden="true"
className="pointer-events-none col-start-1 row-start-1 size-4 self-center justify-self-start ms-3 text-muted-foreground"
/>
</form>
</div>
</header>
);
};
// ============================================================================
// Secondary Navigation
// ============================================================================
const SecondaryNav = () => {
return (
<nav className="flex overflow-x-auto border-b bg-background py-4">
<ul
role="list"
className="flex min-w-full flex-none gap-x-6 px-4 text-sm/6 font-semibold text-muted-foreground sm:px-6"
>
{secondaryNavigation.map((item) => (
<li key={item.name}>
<a href={item.href} className={item.current ? "text-primary" : ""}>
{item.name}
</a>
</li>
))}
</ul>
</nav>
);
};
// ============================================================================
// Dashboard Heading
// ============================================================================
const DashboardHeading = () => {
return (
<div className="border-b px-4 py-4 sm:px-6">
<h1 className="flex gap-x-3 text-base/7">
<span className="font-semibold text-foreground">ری یو آی Store</span>
<span className="text-muted-foreground/60">/</span>
<span className="font-semibold text-foreground">نمای کلی</span>
</h1>
<p className="mt-2 text-xs/6 text-muted-foreground">
تحلیل فروشگاه آنلاین و شاخص‌های عملکرد
</p>
</div>
);
};
// ============================================================================
// Dashboard Content Components
// ============================================================================
// Growth section component (vertical layout for SalesRevenueCard header)
const GrowthSection = ({
label,
change,
}: {
label: string;
change: number;
}) => {
const isPositive = change >= 0;
return (
<div className="flex items-center gap-1.5 border bg-muted/30 px-2 py-1">
<span className="text-xs text-muted-foreground">{label}</span>
<span
className={cn(
"font-mono",
"flex items-center gap-0.5 text-xs font-medium",
isPositive ? "text-emerald-600" : "text-red-600",
)}
>
{isPositive ? (
<ArrowUpRight className="size-3" aria-hidden="true" />
) : (
<ArrowDownRight className="size-3" aria-hidden="true" />
)}
{formatChangePercent(change)}
</span>
</div>
);
};
// KPI Stat Card
const KPIStatCard = ({ stat }: { stat: KPIStat }) => {
const isPositive = stat.change >= 0;
const formatValue = (value: number, format: KPIStat["format"]) => {
switch (format) {
case "currency":
return currencyFormatter.format(value);
case "percent":
return percentFormatter.format(value / 100);
case "number":
return numberFormatter.format(value);
}
};
return (
<div className="flex h-full flex-col gap-2 border-b bg-card px-4 py-6 sm:px-6">
<span className="text-xs text-muted-foreground">{stat.title}</span>
<div className="flex items-baseline gap-2">
<span className={cn("font-mono", "text-xl font-semibold")}>
{formatValue(stat.value, stat.format)}
</span>
</div>
<div className="flex items-center gap-1 text-xs">
{isPositive ? (
<ArrowUpRight
className="size-3.5 text-emerald-600"
aria-hidden="true"
/>
) : (
<ArrowDownRight
className="size-3.5 text-red-600"
aria-hidden="true"
/>
)}
<span
className={cn(
"font-mono",
isPositive ? "text-emerald-600" : "text-red-600",
)}
>
{formatChangePercent(stat.change)}
</span>
<span className="text-muted-foreground">نسبت به ماه قبل</span>
</div>
</div>
);
};
// KPI Stats Row
const KPIStatsRow = () => {
return (
<div className="grid grid-cols-2 sm:grid-cols-4 [&>*]:border-e [&>*:last-child]:border-e-0">
{kpiStats.map((stat) => (
<KPIStatCard key={stat.title} stat={stat} />
))}
</div>
);
};
// ============================================================================
// Animated Reference Line Components
// ============================================================================
interface CustomReferenceLabelProps {
viewBox?: { x?: number; y?: number };
value: number;
}
const CustomReferenceLabel: React.FC<CustomReferenceLabelProps> = (props) => {
const { viewBox, value } = props;
const y = viewBox?.y ?? 0;
const width = React.useMemo(() => {
const characterWidth = 8;
const padding = 10;
return value.toString().length * characterWidth + padding;
}, [value]);
return (
<>
<rect
x={0}
y={y - 9}
width={width}
height={18}
fill="var(--secondary-foreground)"
rx={0}
/>
<text
className="font-mono"
fontWeight={600}
x={6}
y={y + 4}
fill="var(--primary-foreground)"
>
{compactCurrencyFormatter.format(value)}
</text>
</>
);
};
// Revenue Bar Chart Tooltip
function RevenueBarTooltip({
active,
payload,
label,
}: any) {
if (!active || !payload?.length) return null;
const value = payload[0]?.value || 0;
return (
<div className="border border-border bg-popover p-2 shadow-lg sm:p-3">
<p className="mb-1.5 text-xs font-medium text-foreground sm:mb-2 sm:text-sm">
{label}
</p>
<div className="flex items-center gap-1.5 sm:gap-2">
<div className="size-2 rounded-full bg-primary sm:size-2.5" />
<span className="text-[10px] text-muted-foreground sm:text-sm">
درآمد:
</span>
<span
className={cn(
"font-mono",
"text-[10px] font-medium text-foreground sm:text-sm",
)}
>
{currencyFormatter.format(Number(value))}
</span>
</div>
</div>
);
}
// Custom gradient bar shape for Sales Revenue chart
const CustomGradientBar = (
props: React.SVGProps<SVGRectElement> & {
index?: number;
activeIndex: number | undefined;
setActiveIndex: (index: number | undefined) => void;
maxValueIndex: number;
},
) => {
const {
fill,
x,
y,
width,
height,
index,
activeIndex,
setActiveIndex,
maxValueIndex,
} = props;
const highlightedIndex =
activeIndex !== undefined ? activeIndex : maxValueIndex;
const isActive = index === highlightedIndex;
const gradientId = `revenue-gradient-${index}`;
const barFill = isActive ? palette.secondary.light : palette.tertiary.light;
return (
<g onMouseEnter={() => setActiveIndex(index)}>
<defs>
<linearGradient id={gradientId} x1="0" y1="0" x2="0" y2="1">
<stop
offset="0%"
stopColor={barFill}
stopOpacity={isActive ? 0.6 : 0.15}
/>
<stop offset="100%" stopColor={barFill} stopOpacity={0} />
</linearGradient>
</defs>
{/* Gradient body */}
<rect
x={x}
y={y}
width={width}
height={height}
fill={`url(#${gradientId})`}
stroke="none"
/>
{/* 2px solid cap at top */}
<rect x={x} y={y} width={width} height={2} fill={barFill} stroke="none" />
</g>
);
};
// Sales Revenue Card with Animated Value Line Bar Chart
const SalesRevenueCard = () => {
const changePercent = 8.2;
// Track which bar is being hovered
const [activeIndex, setActiveIndex] = React.useState<number | undefined>(
undefined,
);
// Calculate the currently tracked value (hovered bar or max bar)
const maxValueData = React.useMemo(() => {
if (activeIndex !== undefined) {
return {
index: activeIndex,
value: monthlyRevenueData[activeIndex].revenue,
};
}
return monthlyRevenueData.reduce(
(max, data, index) =>
data.revenue > max.value ? { index, value: data.revenue } : max,
{ index: 0, value: 0 },
);
}, [activeIndex]);
const isPositive = changePercent >= 0;
// Spring animation for the reference line value
const valueSpring = useSpring(maxValueData.value, {
stiffness: 100,
damping: 20,
});
const [springyValue, setSpringyValue] = React.useState(maxValueData.value);
useMotionValueEvent(valueSpring, "change", (latest) => {
setSpringyValue(Number(latest.toFixed(0)));
});
React.useEffect(() => {
valueSpring.set(maxValueData.value);
}, [maxValueData.value, valueSpring]);
return (
<div className="flex min-w-0 flex-1 flex-col gap-4 bg-card p-4 sm:gap-5 sm:p-5">
<div className="flex items-start justify-between gap-4">
{/* Left: Title + Value + Change */}
<div className="flex flex-col gap-1">
<span className="text-xs text-muted-foreground">درآمد فروش</span>
<span
className={cn(
"font-mono",
"text-xl font-semibold tabular-nums sm:text-3xl",
)}
>
{currencyFormatter.format(maxValueData.value)}
</span>
<div className="flex items-center gap-1">
<span
className={cn(
"font-mono",
"text-sm font-medium",
isPositive ? "text-emerald-600" : "text-red-600",
)}
>
{formatChangePercent(changePercent)}
</span>
{isPositive ? (
<ArrowUpRight
className="size-3.5 text-emerald-600"
aria-hidden="true"
/>
) : (
<ArrowDownRight
className="size-3.5 text-red-600"
aria-hidden="true"
/>
)}
<span className="text-sm text-muted-foreground">نسبت به ماه قبل</span>
</div>
</div>
{/* Right: Growth sections */}
<div className="flex flex-wrap gap-2">
<GrowthSection label="۴ هفته" change={11.6} />
<GrowthSection label="۱۳ هفته" change={7.9} />
<GrowthSection label="۱۲ ماه" change={-3.4} />
</div>
</div>
<div className="h-[180px] w-full min-w-0 sm:h-[220px]">
<ChartContainer
config={revenueChartConfig}
className="aspect-auto h-full w-full"
>
<BarChart
data={monthlyRevenueData}
onMouseLeave={() => setActiveIndex(undefined)}
>
<XAxis
dataKey="month"
axisLine={false}
tickLine={false}
tick={{ fontSize: 10 }}
dy={8}
/>
<YAxis hide />
<Tooltip content={<RevenueBarTooltip />} cursor={false} />
<Bar
dataKey="revenue"
radius={0}
shape={
<CustomGradientBar
activeIndex={activeIndex}
setActiveIndex={setActiveIndex}
maxValueIndex={maxValueData.index}
/>
}
/>
<ReferenceLine
opacity={0.4}
y={springyValue}
stroke="var(--secondary-foreground)"
strokeWidth={1}
strokeDasharray="3 3"
label={<CustomReferenceLabel value={maxValueData.value} />}
/>
</BarChart>
</ChartContainer>
</div>
</div>
);
};
// Proportional Bubble Chart (Pure SVG)
const ProportionalBubbleChart = ({
activeIndex,
onHover,
}: {
activeIndex: number | null;
onHover: (index: number | null) => void;
}) => {
const maxPct = Math.max(...customerSegmentsData.map((r) => r.percentage));
const maxRadius = 70;
const positions = [
{ cx: 180, cy: 105 },
{ cx: 75, cy: 130 },
{ cx: 100, cy: 55 },
];
return (
<svg
viewBox="0 0 280 200"
preserveAspectRatio="xMidYMid meet"
className="h-full w-full font-mono"
onMouseLeave={() => onHover(null)}
>
{customerSegmentsData.map((segment, i) => {
const baseR = Math.sqrt(segment.percentage / maxPct) * maxRadius;
const isActive = activeIndex === i;
const isDimmed = activeIndex !== null && activeIndex !== i;
const r = isActive ? baseR * 1.08 : baseR;
const { cx, cy } = positions[i];
return (
<g
key={segment.name}
onMouseEnter={() => onHover(i)}
style={{ transition: "opacity 150ms ease-out" }}
>
<circle
cx={cx}
cy={cy}
r={r}
fill={segment.color}
fillOpacity={isActive ? 0.4 : isDimmed ? 0.12 : 0.25}
stroke={segment.color}
strokeWidth={1}
strokeOpacity={isActive ? 0.7 : isDimmed ? 0.2 : 0.4}
style={{ transition: "all 150ms ease-out" }}
/>
<text
x={cx}
y={isActive ? cy - 8 : cy}
textAnchor="middle"
dominantBaseline="central"
fill="currentColor"
fontSize={14}
fontWeight={600}
style={{
opacity: isDimmed ? 0.4 : 1,
transition: "all 150ms ease-out",
}}
>
{segment.percentage.toLocaleString("fa-IR")}٪
</text>
{isActive && (
<text
x={cx}
y={cy + 10}
textAnchor="middle"
dominantBaseline="central"
fill="currentColor"
fontSize={10}
style={{
opacity: 0.6,
transition: "opacity 150ms ease-out",
}}
>
{segment.name}
</text>
)}
</g>
);
})}
</svg>
);
};
// Customer Segments Card
const CustomerSegmentsCard = () => {
const isPositive = totalCustomersChange >= 0;
const [activeIndex, setActiveIndex] = React.useState<number | null>(null);
return (
<div
className="flex min-w-0 flex-col gap-4 bg-card p-4 sm:gap-5 sm:p-5"
onMouseLeave={() => setActiveIndex(null)}
>
{/* Header - matches SalesRevenueCard pattern */}
<div className="flex items-start justify-between gap-4">
{/* Left: Title + Value + Change */}
<div className="flex flex-col gap-1">
<span className="text-xs text-muted-foreground">کل مشتریان</span>
<span
className={cn("font-mono", "text-xl font-semibold tabular-nums")}
>
{numberFormatter.format(totalCustomers)}
</span>
<div className="flex items-center gap-1">
<span
className={cn(
"font-mono",
"text-sm font-medium",
isPositive ? "text-emerald-600" : "text-red-600",
)}
>
{formatChangePercent(totalCustomersChange)}
</span>
{isPositive ? (
<ArrowUpRight
className="size-3.5 text-emerald-600"
aria-hidden="true"
/>
) : (
<ArrowDownRight
className="size-3.5 text-red-600"
aria-hidden="true"
/>
)}
</div>
</div>
{/* Right: Details button */}
<Button variant="outline" size="sm" className="h-7 px-2 text-xs">
جزئیات
</Button>
</div>
{/* Bubble chart */}
<div className="h-[160px]">
<ProportionalBubbleChart
activeIndex={activeIndex}
onHover={setActiveIndex}
/>
</div>
{/* Legend rows */}
<div className="flex flex-col gap-3">
{customerSegmentsData.map((segment, i) => {
const segmentPositive = segment.change >= 0;
const isActive = activeIndex === i;
const isDimmed = activeIndex !== null && activeIndex !== i;
return (
<div
key={segment.name}
className="flex items-center justify-between"
style={{
opacity: isDimmed ? 0.4 : 1,
transition: "opacity 150ms ease-out",
}}
onMouseEnter={() => setActiveIndex(i)}
onMouseLeave={() => setActiveIndex(null)}
>
<div className="flex items-center gap-2">
<div
className="h-2 w-2 rounded-full"
style={{ backgroundColor: segment.color }}
/>
<span
className={cn(
"text-sm text-muted-foreground",
isActive && "font-medium text-foreground",
)}
style={{ transition: "all 150ms ease-out" }}
>
{segment.name}
</span>
</div>
<div className="flex items-center gap-4">
<span
className={cn(
"font-mono",
"text-sm font-medium tabular-nums",
)}
>
{numberFormatter.format(segment.customers)}
</span>
<div
className={cn(
"font-mono",
"flex items-center gap-0.5 text-xs tabular-nums",
segmentPositive ? "text-emerald-600" : "text-red-600",
)}
>
{segmentPositive ? (
<ArrowUpRight className="size-3" aria-hidden="true" />
) : (
<ArrowDownRight className="size-3" aria-hidden="true" />
)}
{formatChangePercent(segment.change)}
</div>
</div>
</div>
);
})}
</div>
</div>
);
};
// Dotted World Map (Pure SVG)
const DottedWorldMap = ({
activeIndex,
onSelect,
hoveredIndex,
onHover,
}: {
activeIndex: number | null;
onSelect: (index: number | null) => void;
hoveredIndex: number | null;
onHover: (index: number | null) => void;
}) => {
const maxUsers = Math.max(...geographyCountries.map((c) => c.users));
const minRadius = 1.2;
const maxRadius = 2.8;
return (
<svg
viewBox="0 0 63 30"
preserveAspectRatio="xMidYMid meet"
className="h-full w-full"
onMouseLeave={() => onHover(null)}
>
<defs>
<style>{`
@keyframes mapMarkerPulse {
0% { opacity: 1.0; }
50% { opacity: 0.65; }
100% { opacity: 1.0; }
}
`}</style>
</defs>
{mapBasePoints.map((point, i) => {
let dotOpacity = 0.12;
if (hoveredIndex !== null) {
const boosted = spotlightMaps[hoveredIndex].get(i);
dotOpacity = boosted !== undefined ? boosted : 0.12;
}
return (
<circle
key={i}
cx={point.x}
cy={point.y}
r={0.35}
fill="currentColor"
opacity={dotOpacity}
style={{ transition: "opacity 300ms ease-out" }}
/>
);
})}
{geographyPinPositions.map((pin, i) => {
const isActive = activeIndex === i;
const isHovered = hoveredIndex === i;
const isDimmed =
(activeIndex !== null || hoveredIndex !== null) &&
!isActive &&
!isHovered;
const ratio = geographyCountries[i].users / maxUsers;
const baseR = minRadius + ratio * (maxRadius - minRadius);
const r = isActive ? baseR * 1.15 : isHovered ? baseR * 1.08 : baseR;
const markerFill = isActive
? palette.primary
: isHovered
? palette.secondary.light
: palette.tertiary.light;
const markerOpacity = isActive
? 0.7
: isHovered
? 0.6
: isDimmed
? 0.2
: 0.5;
const isIdle = !isActive && !isHovered && !isDimmed;
return (
<g
key={geographyCountries[i].name}
onClick={() => onSelect(isActive ? null : i)}
onMouseEnter={() => onHover(i)}
style={{
cursor: "pointer",
animation: isIdle
? `mapMarkerPulse 3s ease-in-out ${i * 0.6}s infinite`
: "none",
}}
>
{isActive && (
<circle
cx={pin.x}
cy={pin.y}
r={r * 2.5}
fill={palette.primary}
fillOpacity={0.08}
stroke={palette.primary}
strokeWidth={0.3}
strokeOpacity={0.15}
style={{ transition: "all 150ms ease-out" }}
/>
)}
{isHovered && !isActive && (
<circle
cx={pin.x}
cy={pin.y}
r={r * 2.0}
fill={palette.tertiary.light}
fillOpacity={0.06}
stroke={palette.tertiary.light}
strokeWidth={0.2}
strokeOpacity={0.12}
style={{ transition: "all 150ms ease-out" }}
/>
)}
<circle
cx={pin.x}
cy={pin.y}
r={r}
fill={markerFill}
fillOpacity={markerOpacity}
stroke={markerFill}
strokeWidth={0.4}
strokeOpacity={
isActive ? 0.9 : isHovered ? 0.6 : isDimmed ? 0.1 : 0.4
}
style={{ transition: "all 150ms ease-out" }}
/>
</g>
);
})}
</svg>
);
};
// Geography Card
const GeographyCard = () => {
const [activeIndex, setActiveIndex] = React.useState<number | null>(null);
const [hoveredIndex, setHoveredIndex] = React.useState<number | null>(null);
const selected =
activeIndex !== null ? geographyCountries[activeIndex] : null;
const hovered =
hoveredIndex !== null ? geographyCountries[hoveredIndex] : null;
const previewCountry = hovered ?? selected;
const globalDemographics = {
men: Math.round(
geographyCountries.reduce((s, c) => s + c.demographics.men * c.users, 0) /
totalGeographyUsers,
),
women: Math.round(
geographyCountries.reduce(
(s, c) => s + c.demographics.women * c.users,
0,
) / totalGeographyUsers,
),
other: Math.round(
geographyCountries.reduce(
(s, c) => s + c.demographics.other * c.users,
0,
) / totalGeographyUsers,
),
};
const displayUsers = previewCountry
? previewCountry.users
: totalGeographyUsers;
const displayDemographics = previewCountry
? previewCountry.demographics
: globalDemographics;
const displayLabel = previewCountry
? { flag: previewCountry.flag, name: previewCountry.name }
: null;
const tooltipCountry = hovered;
const tooltipPin =
hoveredIndex !== null ? geographyPinPositions[hoveredIndex] : null;
const viewBoxW = 63;
const viewBoxH = 30;
let tooltipLeft = 0;
let tooltipTop = 0;
let tooltipAbove = false;
if (tooltipPin) {
tooltipLeft = (tooltipPin.x / viewBoxW) * 100;
tooltipTop = (tooltipPin.y / viewBoxH) * 100;
tooltipAbove = tooltipPin.y > viewBoxH / 2;
}
const tooltipTranslateX =
tooltipLeft < 20 ? "0%" : tooltipLeft > 80 ? "-100%" : "-50%";
return (
<div className="flex min-w-0 flex-col gap-4 bg-card p-4 sm:gap-5 sm:p-5">
<div className="flex items-start justify-between gap-4">
<div className="flex flex-col gap-1">
<span className="text-xs text-muted-foreground">جغرافیا</span>
<div className="flex items-baseline gap-2">
<span
className={cn("font-mono", "text-xl font-semibold tabular-nums")}
>
{numberFormatter.format(displayUsers)}
</span>
{displayLabel && (
<span className="flex items-center gap-1 border bg-muted/30 px-1.5 py-0.5 text-xs text-muted-foreground">
{displayLabel.flag} {displayLabel.name}
</span>
)}
</div>
</div>
<Button variant="outline" size="sm" className="h-7 px-2 text-xs">
جزئیات
</Button>
</div>
<div className="flex items-center gap-3">
<div className="flex h-1.5 flex-1 overflow-hidden">
<div
style={{
width: `${displayDemographics.men}%`,
backgroundColor: palette.primary,
transition: "width 150ms ease-out",
}}
className="h-full"
/>
<div
style={{
width: `${displayDemographics.women}%`,
backgroundColor: palette.tertiary.light,
transition: "width 150ms ease-out",
}}
className="h-full"
/>
<div
style={{
width: `${displayDemographics.other}%`,
backgroundColor: palette.quinary.light,
transition: "width 150ms ease-out",
}}
className="h-full"
/>
</div>
</div>
<div className="flex items-center gap-3 text-xs text-muted-foreground">
<span className="flex items-center gap-1">
<span
className="inline-block h-1.5 w-1.5 rounded-full"
style={{ backgroundColor: palette.primary }}
/>
مرد {displayDemographics.men.toLocaleString("fa-IR")}٪
</span>
<span className="flex items-center gap-1">
<span
className="inline-block h-1.5 w-1.5 rounded-full"
style={{ backgroundColor: palette.tertiary.light }}
/>
زن {displayDemographics.women.toLocaleString("fa-IR")}٪
</span>
<span className="flex items-center gap-1">
<span
className="inline-block h-1.5 w-1.5 rounded-full"
style={{ backgroundColor: palette.quinary.light }}
/>
سایر {displayDemographics.other.toLocaleString("fa-IR")}٪
</span>
</div>
<div className="relative h-[200px]">
<DottedWorldMap
activeIndex={activeIndex}
onSelect={setActiveIndex}
hoveredIndex={hoveredIndex}
onHover={setHoveredIndex}
/>
{tooltipCountry && tooltipPin && (
<div
className="pointer-events-none absolute z-10 flex flex-col gap-1.5 border border-border bg-popover px-3 py-2 shadow-lg"
style={{
left: `${tooltipLeft}%`,
top: tooltipAbove ? `${tooltipTop}%` : `${tooltipTop}%`,
transform: `${tooltipTranslateX !== "-50%" ? `translateX(${tooltipTranslateX})` : "translateX(-50%)"} ${tooltipAbove ? "translateY(calc(-100% - 12px))" : "translateY(12px)"}`,
opacity: 1,
transition: "opacity 150ms ease-out, transform 150ms ease-out",
}}
>
<div className="flex items-center gap-1.5 text-xs font-medium text-foreground">
<span>{tooltipCountry.flag}</span>
<span>{tooltipCountry.name}</span>
</div>
<span
className={cn(
"font-mono",
"text-sm font-semibold text-foreground tabular-nums",
)}
>
{numberFormatter.format(tooltipCountry.users)}
</span>
<div className="flex h-1 w-full overflow-hidden">
<div
style={{
width: `${tooltipCountry.demographics.men}%`,
backgroundColor: palette.primary,
}}
className="h-full"
/>
<div
style={{
width: `${tooltipCountry.demographics.women}%`,
backgroundColor: palette.tertiary.light,
}}
className="h-full"
/>
<div
style={{
width: `${tooltipCountry.demographics.other}%`,
backgroundColor: palette.quinary.light,
}}
className="h-full"
/>
</div>
<div className="flex items-center gap-2 text-[10px] text-muted-foreground">
<span>م {tooltipCountry.demographics.men.toLocaleString("fa-IR")}٪</span>
<span>ز {tooltipCountry.demographics.women.toLocaleString("fa-IR")}٪</span>
<span>س {tooltipCountry.demographics.other.toLocaleString("fa-IR")}٪</span>
</div>
</div>
)}
</div>
</div>
);
};
// Recent Orders Card (activity table)
function RecentOrdersCard() {
return (
<div className="flex flex-col bg-card">
<div className="px-4 pt-5 sm:px-6">
<h2 className="text-sm font-medium text-foreground">سفارش‌های اخیر</h2>
</div>
<div className="mt-4">
<table className="w-full text-start">
<colgroup>
<col className="w-full sm:w-4/12" />
<col className="sm:w-2/12" />
<col className="sm:w-2/12" />
<col className="sm:w-2/12" />
<col className="sm:w-2/12" />
</colgroup>
<thead className="border-b border-border text-xs text-muted-foreground">
<tr>
<th scope="col" className="py-2 pe-3 ps-4 font-medium sm:ps-6">
مشتری
</th>
<th
scope="col"
className="hidden px-3 py-2 font-medium sm:table-cell"
>
سفارش
</th>
<th scope="col" className="px-3 py-2 font-medium">
وضعیت
</th>
<th scope="col" className="px-3 py-2 text-end font-medium">
مبلغ
</th>
<th
scope="col"
className="hidden py-2 pe-4 ps-3 text-end font-medium sm:table-cell sm:pe-6"
>
زمان
</th>
</tr>
</thead>
<tbody className="divide-y divide-border">
{recentOrders.map((order) => (
<tr key={order.orderId}>
<td className="py-3 pe-3 ps-4 sm:ps-6">
<div className="flex items-center gap-x-3">
<div className="flex size-8 items-center justify-center rounded-full bg-muted text-xs font-medium text-muted-foreground">
{order.customer.avatar}
</div>
<span className="text-sm font-medium text-foreground">
{order.customer.name}
</span>
</div>
</td>
<td className="hidden px-3 py-3 sm:table-cell">
<span className="font-mono text-xs text-muted-foreground">
{order.orderId}
</span>
</td>
<td className="px-3 py-3">
<span
className={`inline-flex items-center gap-x-1.5 rounded-full px-2 py-0.5 text-xs font-medium ${orderStatuses[order.status]}`}
>
<span className="size-1.5 rounded-full bg-current" />
{orderStatusLabels[order.status]}
</span>
</td>
<td className="px-3 py-3 text-end">
<span className="font-mono text-sm text-foreground">
{currencyFormatter.format(order.amount)}
</span>
</td>
<td className="hidden py-3 pe-4 ps-3 text-end sm:table-cell sm:pe-6">
<span className="text-xs text-muted-foreground">
{order.date}
</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
);
}
// Main Dashboard Content
const DashboardContent = () => {
return (
<div id="dashboard-main" className="flex w-full flex-1 flex-col bg-card">
{/* Heading */}
<DashboardHeading />
{/* KPI Stats Row */}
<KPIStatsRow />
{/* Middle Row: Sales Revenue + Segmentation */}
<div className="grid lg:grid-cols-3">
<div className="border-b lg:col-span-2 lg:border-e">
<SalesRevenueCard />
</div>
<div className="border-b">
<CustomerSegmentsCard />
</div>
</div>
{/* Bottom Row: Geography + Recent Orders */}
<div className="grid lg:grid-cols-3">
<div className="border-b lg:border-e">
<GeographyCard />
</div>
<div className="border-b lg:col-span-2">
<RecentOrdersCard />
</div>
</div>
</div>
);
};
// ============================================================================
// Main Dashboard Component
// ============================================================================
const Dashboard12 = ({ className }: { className?: string }) => {
return (
<TooltipProvider>
<SidebarProvider
dir="rtl"
lang="fa"
className={cn("min-h-svh w-full overflow-hidden bg-sidebar", className)}
style={{ "--sidebar-width": "18rem" } as React.CSSProperties}
>
<a
href="#dashboard-main"
className="sr-only focus:not-sr-only focus:absolute focus:top-4 focus:start-4 focus:z-50 focus:rounded-md focus:bg-background focus:px-3 focus:py-2 focus:text-sm focus:text-foreground focus:ring-2 focus:ring-ring"
>
رفتن به محتوای اصلی
</a>
<AppSidebar />
<SidebarInset className="min-h-svh min-w-0 w-full overflow-auto md:peer-data-[variant=inset]:m-0 md:peer-data-[variant=inset]:rounded-none md:peer-data-[variant=inset]:shadow-none md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ms-0 md:peer-data-[variant=inset]:peer-data-[state=collapsed]:me-0">
<DashboardHeader />
<SecondaryNav />
<DashboardContent />
</SidebarInset>
</SidebarProvider>
</TooltipProvider>
);
};
export { Dashboard12 };