1807 lines
90 KiB
TypeScript
1807 lines
90 KiB
TypeScript
"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": 1.7320508075688772 }, "43;2": { "x": 43.5, "y": 1.7320508075688772 }, "45;2": { "x": 45.5, "y": 1.7320508075688772 }, "46;2": { "x": 46.5, "y": 1.7320508075688772 }, "47;2": { "x": 47.5, "y": 1.7320508075688772 }, "48;2": { "x": 48.5, "y": 1.7320508075688772 }, "49;2": { "x": 49.5, "y": 1.7320508075688772 }, "50;2": { "x": 50.5, "y": 1.7320508075688772 }, "51;2": { "x": 51.5, "y": 1.7320508075688772 }, "52;2": { "x": 52.5, "y": 1.7320508075688772 }, "53;2": { "x": 53.5, "y": 1.7320508075688772 }, "54;2": { "x": 54.5, "y": 1.7320508075688772 }, "55;2": { "x": 55.5, "y": 1.7320508075688772 }, "56;2": { "x": 56.5, "y": 1.7320508075688772 }, "57;2": { "x": 57.5, "y": 1.7320508075688772 }, "58;2": { "x": 58.5, "y": 1.7320508075688772 }, "59;2": { "x": 59.5, "y": 1.7320508075688772 }, "60;2": { "x": 60.5, "y": 1.7320508075688772 }, "61;2": { "x": 61.5, "y": 1.7320508075688772 }, "62;2": { "x": 62.5, "y": 1.7320508075688772 }, "1;3": { "x": 1, "y": 2.598076211353316 }, "2;3": { "x": 2, "y": 2.598076211353316 }, "3;3": { "x": 3, "y": 2.598076211353316 }, "4;3": { "x": 4, "y": 2.598076211353316 }, "5;3": { "x": 5, "y": 2.598076211353316 }, "6;3": { "x": 6, "y": 2.598076211353316 }, "7;3": { "x": 7, "y": 2.598076211353316 }, "8;3": { "x": 8, "y": 2.598076211353316 }, "9;3": { "x": 9, "y": 2.598076211353316 }, "10;3": { "x": 10, "y": 2.598076211353316 }, "11;3": { "x": 11, "y": 2.598076211353316 }, "12;3": { "x": 12, "y": 2.598076211353316 }, "13;3": { "x": 13, "y": 2.598076211353316 }, "14;3": { "x": 14, "y": 2.598076211353316 }, "15;3": { "x": 15, "y": 2.598076211353316 }, "16;3": { "x": 16, "y": 2.598076211353316 }, "19;3": { "x": 19, "y": 2.598076211353316 }, "20;3": { "x": 20, "y": 2.598076211353316 }, "23;3": { "x": 23, "y": 2.598076211353316 }, "24;3": { "x": 24, "y": 2.598076211353316 }, "28;3": { "x": 28, "y": 2.598076211353316 }, "34;3": { "x": 34, "y": 2.598076211353316 }, "35;3": { "x": 35, "y": 2.598076211353316 }, "36;3": { "x": 36, "y": 2.598076211353316 }, "37;3": { "x": 37, "y": 2.598076211353316 }, "39;3": { "x": 39, "y": 2.598076211353316 }, "40;3": { "x": 40, "y": 2.598076211353316 }, "41;3": { "x": 41, "y": 2.598076211353316 }, "42;3": { "x": 42, "y": 2.598076211353316 }, "43;3": { "x": 43, "y": 2.598076211353316 }, "44;3": { "x": 44, "y": 2.598076211353316 }, "45;3": { "x": 45, "y": 2.598076211353316 }, "46;3": { "x": 46, "y": 2.598076211353316 }, "47;3": { "x": 47, "y": 2.598076211353316 }, "48;3": { "x": 48, "y": 2.598076211353316 }, "49;3": { "x": 49, "y": 2.598076211353316 }, "50;3": { "x": 50, "y": 2.598076211353316 }, "51;3": { "x": 51, "y": 2.598076211353316 }, "52;3": { "x": 52, "y": 2.598076211353316 }, "53;3": { "x": 53, "y": 2.598076211353316 }, "54;3": { "x": 54, "y": 2.598076211353316 }, "55;3": { "x": 55, "y": 2.598076211353316 }, "56;3": { "x": 56, "y": 2.598076211353316 }, "57;3": { "x": 57, "y": 2.598076211353316 }, "58;3": { "x": 58, "y": 2.598076211353316 }, "59;3": { "x": 59, "y": 2.598076211353316 }, "60;3": { "x": 60, "y": 2.598076211353316 }, "61;3": { "x": 61, "y": 2.598076211353316 }, "62;3": { "x": 62, "y": 2.598076211353316 }, "1;4": { "x": 1.5, "y": 3.4641016151377544 }, "3;4": { "x": 3.5, "y": 3.4641016151377544 }, "4;4": { "x": 4.5, "y": 3.4641016151377544 }, "5;4": { "x": 5.5, "y": 3.4641016151377544 }, "6;4": { "x": 6.5, "y": 3.4641016151377544 }, "7;4": { "x": 7.5, "y": 3.4641016151377544 }, "8;4": { "x": 8.5, "y": 3.4641016151377544 }, "9;4": { "x": 9.5, "y": 3.4641016151377544 }, "10;4": { "x": 10.5, "y": 3.4641016151377544 }, "11;4": { "x": 11.5, "y": 3.4641016151377544 }, "12;4": { "x": 12.5, "y": 3.4641016151377544 }, "13;4": { "x": 13.5, "y": 3.4641016151377544 }, "14;4": { "x": 14.5, "y": 3.4641016151377544 }, "15;4": { "x": 15.5, "y": 3.4641016151377544 }, "16;4": { "x": 16.5, "y": 3.4641016151377544 }, "22;4": { "x": 22.5, "y": 3.4641016151377544 }, "23;4": { "x": 23.5, "y": 3.4641016151377544 }, "33;4": { "x": 33.5, "y": 3.4641016151377544 }, "34;4": { "x": 34.5, "y": 3.4641016151377544 }, "35;4": { "x": 35.5, "y": 3.4641016151377544 }, "36;4": { "x": 36.5, "y": 3.4641016151377544 }, "37;4": { "x": 37.5, "y": 3.4641016151377544 }, "38;4": { "x": 38.5, "y": 3.4641016151377544 }, "39;4": { "x": 39.5, "y": 3.4641016151377544 }, "40;4": { "x": 40.5, "y": 3.4641016151377544 }, "41;4": { "x": 41.5, "y": 3.4641016151377544 }, "42;4": { "x": 42.5, "y": 3.4641016151377544 }, "43;4": { "x": 43.5, "y": 3.4641016151377544 }, "44;4": { "x": 44.5, "y": 3.4641016151377544 }, "45;4": { "x": 45.5, "y": 3.4641016151377544 }, "46;4": { "x": 46.5, "y": 3.4641016151377544 }, "47;4": { "x": 47.5, "y": 3.4641016151377544 }, "48;4": { "x": 48.5, "y": 3.4641016151377544 }, "49;4": { "x": 49.5, "y": 3.4641016151377544 }, "50;4": { "x": 50.5, "y": 3.4641016151377544 }, "51;4": { "x": 51.5, "y": 3.4641016151377544 }, "52;4": { "x": 52.5, "y": 3.4641016151377544 }, "53;4": { "x": 53.5, "y": 3.4641016151377544 }, "54;4": { "x": 54.5, "y": 3.4641016151377544 }, "55;4": { "x": 55.5, "y": 3.4641016151377544 }, "56;4": { "x": 56.5, "y": 3.4641016151377544 }, "57;4": { "x": 57.5, "y": 3.4641016151377544 }, "58;4": { "x": 58.5, "y": 3.4641016151377544 }, "59;4": { "x": 59.5, "y": 3.4641016151377544 }, "60;4": { "x": 60.5, "y": 3.4641016151377544 }, "61;4": { "x": 61.5, "y": 3.4641016151377544 }, "62;4": { "x": 62.5, "y": 3.4641016151377544 }, "3;5": { "x": 3, "y": 4.330127018922193 }, "4;5": { "x": 4, "y": 4.330127018922193 }, "6;5": { "x": 6, "y": 4.330127018922193 }, "7;5": { "x": 7, "y": 4.330127018922193 }, "8;5": { "x": 8, "y": 4.330127018922193 }, "9;5": { "x": 9, "y": 4.330127018922193 }, "10;5": { "x": 10, "y": 4.330127018922193 }, "11;5": { "x": 11, "y": 4.330127018922193 }, "12;5": { "x": 12, "y": 4.330127018922193 }, "13;5": { "x": 13, "y": 4.330127018922193 }, "14;5": { "x": 14, "y": 4.330127018922193 }, "18;5": { "x": 18, "y": 4.330127018922193 }, "23;5": { "x": 23, "y": 4.330127018922193 }, "33;5": { "x": 33, "y": 4.330127018922193 }, "34;5": { "x": 34, "y": 4.330127018922193 }, "36;5": { "x": 36, "y": 4.330127018922193 }, "37;5": { "x": 37, "y": 4.330127018922193 }, "38;5": { "x": 38, "y": 4.330127018922193 }, "39;5": { "x": 39, "y": 4.330127018922193 }, "40;5": { "x": 40, "y": 4.330127018922193 }, "41;5": { "x": 41, "y": 4.330127018922193 }, "42;5": { "x": 42, "y": 4.330127018922193 }, "43;5": { "x": 43, "y": 4.330127018922193 }, "44;5": { "x": 44, "y": 4.330127018922193 }, "45;5": { "x": 45, "y": 4.330127018922193 }, "46;5": { "x": 46, "y": 4.330127018922193 }, "47;5": { "x": 47, "y": 4.330127018922193 }, "48;5": { "x": 48, "y": 4.330127018922193 }, "49;5": { "x": 49, "y": 4.330127018922193 }, "50;5": { "x": 50, "y": 4.330127018922193 }, "51;5": { "x": 51, "y": 4.330127018922193 }, "52;5": { "x": 52, "y": 4.330127018922193 }, "53;5": { "x": 53, "y": 4.330127018922193 }, "54;5": { "x": 54, "y": 4.330127018922193 }, "55;5": { "x": 55, "y": 4.330127018922193 }, "56;5": { "x": 56, "y": 4.330127018922193 }, "57;5": { "x": 57, "y": 4.330127018922193 }, "58;5": { "x": 58, "y": 4.330127018922193 }, "59;5": { "x": 59, "y": 4.330127018922193 }, "61;5": { "x": 61, "y": 4.330127018922193 }, "3;6": { "x": 3.5, "y": 5.196152422706632 }, "7;6": { "x": 7.5, "y": 5.196152422706632 }, "8;6": { "x": 8.5, "y": 5.196152422706632 }, "9;6": { "x": 9.5, "y": 5.196152422706632 }, "10;6": { "x": 10.5, "y": 5.196152422706632 }, "11;6": { "x": 11.5, "y": 5.196152422706632 }, "12;6": { "x": 12.5, "y": 5.196152422706632 }, "13;6": { "x": 13.5, "y": 5.196152422706632 }, "14;6": { "x": 14.5, "y": 5.196152422706632 }, "18;6": { "x": 18.5, "y": 5.196152422706632 }, "32;6": { "x": 32.5, "y": 5.196152422706632 }, "33;6": { "x": 33.5, "y": 5.196152422706632 }, "36;6": { "x": 36.5, "y": 5.196152422706632 }, "37;6": { "x": 37.5, "y": 5.196152422706632 }, "38;6": { "x": 38.5, "y": 5.196152422706632 }, "39;6": { "x": 39.5, "y": 5.196152422706632 }, "40;6": { "x": 40.5, "y": 5.196152422706632 }, "41;6": { "x": 41.5, "y": 5.196152422706632 }, "42;6": { "x": 42.5, "y": 5.196152422706632 }, "43;6": { "x": 43.5, "y": 5.196152422706632 }, "44;6": { "x": 44.5, "y": 5.196152422706632 }, "45;6": { "x": 45.5, "y": 5.196152422706632 }, "46;6": { "x": 46.5, "y": 5.196152422706632 }, "47;6": { "x": 47.5, "y": 5.196152422706632 }, "48;6": { "x": 48.5, "y": 5.196152422706632 }, "49;6": { "x": 49.5, "y": 5.196152422706632 }, "50;6": { "x": 50.5, "y": 5.196152422706632 }, "51;6": { "x": 51.5, "y": 5.196152422706632 }, "52;6": { "x": 52.5, "y": 5.196152422706632 }, "53;6": { "x": 53.5, "y": 5.196152422706632 }, "54;6": { "x": 54.5, "y": 5.196152422706632 }, "55;6": { "x": 55.5, "y": 5.196152422706632 }, "9;7": { "x": 9, "y": 6.06217782649107 }, "10;7": { "x": 10, "y": 6.06217782649107 }, "11;7": { "x": 11, "y": 6.06217782649107 }, "12;7": { "x": 12, "y": 6.06217782649107 }, "13;7": { "x": 13, "y": 6.06217782649107 }, "14;7": { "x": 14, "y": 6.06217782649107 }, "15;7": { "x": 15, "y": 6.06217782649107 }, "16;7": { "x": 16, "y": 6.06217782649107 }, "18;7": { "x": 18, "y": 6.06217782649107 }, "19;7": { "x": 19, "y": 6.06217782649107 }, "20;7": { "x": 20, "y": 6.06217782649107 }, "33;7": { "x": 33, "y": 6.06217782649107 }, "34;7": { "x": 34, "y": 6.06217782649107 }, "36;7": { "x": 36, "y": 6.06217782649107 }, "37;7": { "x": 37, "y": 6.06217782649107 }, "38;7": { "x": 38, "y": 6.06217782649107 }, "39;7": { "x": 39, "y": 6.06217782649107 }, "40;7": { "x": 40, "y": 6.06217782649107 }, "41;7": { "x": 41, "y": 6.06217782649107 }, "42;7": { "x": 42, "y": 6.06217782649107 }, "43;7": { "x": 43, "y": 6.06217782649107 }, "44;7": { "x": 44, "y": 6.06217782649107 }, "45;7": { "x": 45, "y": 6.06217782649107 }, "46;7": { "x": 46, "y": 6.06217782649107 }, "47;7": { "x": 47, "y": 6.06217782649107 }, "48;7": { "x": 48, "y": 6.06217782649107 }, "49;7": { "x": 49, "y": 6.06217782649107 }, "50;7": { "x": 50, "y": 6.06217782649107 }, "51;7": { "x": 51, "y": 6.06217782649107 }, "52;7": { "x": 52, "y": 6.06217782649107 }, "53;7": { "x": 53, "y": 6.06217782649107 }, "54;7": { "x": 54, "y": 6.06217782649107 }, "55;7": { "x": 55, "y": 6.06217782649107 }, "59;7": { "x": 59, "y": 6.06217782649107 }, "60;7": { "x": 60, "y": 6.06217782649107 }, "9;8": { "x": 9.5, "y": 6.928203230275509 }, "10;8": { "x": 10.5, "y": 6.928203230275509 }, "11;8": { "x": 11.5, "y": 6.928203230275509 }, "12;8": { "x": 12.5, "y": 6.928203230275509 }, "13;8": { "x": 13.5, "y": 6.928203230275509 }, "14;8": { "x": 14.5, "y": 6.928203230275509 }, "15;8": { "x": 15.5, "y": 6.928203230275509 }, "16;8": { "x": 16.5, "y": 6.928203230275509 }, "18;8": { "x": 18.5, "y": 6.928203230275509 }, "19;8": { "x": 19.5, "y": 6.928203230275509 }, "20;8": { "x": 20.5, "y": 6.928203230275509 }, "21;8": { "x": 21.5, "y": 6.928203230275509 }, "31;8": { "x": 31.5, "y": 6.928203230275509 }, "32;8": { "x": 32.5, "y": 6.928203230275509 }, "33;8": { "x": 33.5, "y": 6.928203230275509 }, "34;8": { "x": 34.5, "y": 6.928203230275509 }, "35;8": { "x": 35.5, "y": 6.928203230275509 }, "36;8": { "x": 36.5, "y": 6.928203230275509 }, "37;8": { "x": 37.5, "y": 6.928203230275509 }, "38;8": { "x": 38.5, "y": 6.928203230275509 }, "39;8": { "x": 39.5, "y": 6.928203230275509 }, "40;8": { "x": 40.5, "y": 6.928203230275509 }, "41;8": { "x": 41.5, "y": 6.928203230275509 }, "42;8": { "x": 42.5, "y": 6.928203230275509 }, "43;8": { "x": 43.5, "y": 6.928203230275509 }, "44;8": { "x": 44.5, "y": 6.928203230275509 }, "45;8": { "x": 45.5, "y": 6.928203230275509 }, "46;8": { "x": 46.5, "y": 6.928203230275509 }, "47;8": { "x": 47.5, "y": 6.928203230275509 }, "48;8": { "x": 48.5, "y": 6.928203230275509 }, "49;8": { "x": 49.5, "y": 6.928203230275509 }, "50;8": { "x": 50.5, "y": 6.928203230275509 }, "51;8": { "x": 51.5, "y": 6.928203230275509 }, "52;8": { "x": 52.5, "y": 6.928203230275509 }, "53;8": { "x": 53.5, "y": 6.928203230275509 }, "54;8": { "x": 54.5, "y": 6.928203230275509 }, "55;8": { "x": 55.5, "y": 6.928203230275509 }, "56;8": { "x": 56.5, "y": 6.928203230275509 }, "59;8": { "x": 59.5, "y": 6.928203230275509 }, "9;9": { "x": 9, "y": 7.794228634059947 }, "10;9": { "x": 10, "y": 7.794228634059947 }, "11;9": { "x": 11, "y": 7.794228634059947 }, "12;9": { "x": 12, "y": 7.794228634059947 }, "13;9": { "x": 13, "y": 7.794228634059947 }, "14;9": { "x": 14, "y": 7.794228634059947 }, "15;9": { "x": 15, "y": 7.794228634059947 }, "16;9": { "x": 16, "y": 7.794228634059947 }, "17;9": { "x": 17, "y": 7.794228634059947 }, "18;9": { "x": 18, "y": 7.794228634059947 }, "19;9": { "x": 19, "y": 7.794228634059947 }, "32;9": { "x": 32, "y": 7.794228634059947 }, "33;9": { "x": 33, "y": 7.794228634059947 }, "34;9": { "x": 34, "y": 7.794228634059947 }, "35;9": { "x": 35, "y": 7.794228634059947 }, "36;9": { "x": 36, "y": 7.794228634059947 }, "37;9": { "x": 37, "y": 7.794228634059947 }, "38;9": { "x": 38, "y": 7.794228634059947 }, "39;9": { "x": 39, "y": 7.794228634059947 }, "40;9": { "x": 40, "y": 7.794228634059947 }, "41;9": { "x": 41, "y": 7.794228634059947 }, "42;9": { "x": 42, "y": 7.794228634059947 }, "43;9": { "x": 43, "y": 7.794228634059947 }, "44;9": { "x": 44, "y": 7.794228634059947 }, "45;9": { "x": 45, "y": 7.794228634059947 }, "46;9": { "x": 46, "y": 7.794228634059947 }, "47;9": { "x": 47, "y": 7.794228634059947 }, "48;9": { "x": 48, "y": 7.794228634059947 }, "49;9": { "x": 49, "y": 7.794228634059947 }, "50;9": { "x": 50, "y": 7.794228634059947 }, "51;9": { "x": 51, "y": 7.794228634059947 }, "52;9": { "x": 52, "y": 7.794228634059947 }, "53;9": { "x": 53, "y": 7.794228634059947 }, "54;9": { "x": 54, "y": 7.794228634059947 }, "55;9": { "x": 55, "y": 7.794228634059947 }, "56;9": { "x": 56, "y": 7.794228634059947 }, "10;10": { "x": 10.5, "y": 8.660254037844386 }, "11;10": { "x": 11.5, "y": 8.660254037844386 }, "12;10": { "x": 12.5, "y": 8.660254037844386 }, "13;10": { "x": 13.5, "y": 8.660254037844386 }, "14;10": { "x": 14.5, "y": 8.660254037844386 }, "15;10": { "x": 15.5, "y": 8.660254037844386 }, "16;10": { "x": 16.5, "y": 8.660254037844386 }, "17;10": { "x": 17.5, "y": 8.660254037844386 }, "18;10": { "x": 18.5, "y": 8.660254037844386 }, "19;10": { "x": 19.5, "y": 8.660254037844386 }, "31;10": { "x": 31.5, "y": 8.660254037844386 }, "32;10": { "x": 32.5, "y": 8.660254037844386 }, "33;10": { "x": 33.5, "y": 8.660254037844386 }, "34;10": { "x": 34.5, "y": 8.660254037844386 }, "35;10": { "x": 35.5, "y": 8.660254037844386 }, "36;10": { "x": 36.5, "y": 8.660254037844386 }, "37;10": { "x": 37.5, "y": 8.660254037844386 }, "38;10": { "x": 38.5, "y": 8.660254037844386 }, "39;10": { "x": 39.5, "y": 8.660254037844386 }, "41;10": { "x": 41.5, "y": 8.660254037844386 }, "42;10": { "x": 42.5, "y": 8.660254037844386 }, "43;10": { "x": 43.5, "y": 8.660254037844386 }, "44;10": { "x": 44.5, "y": 8.660254037844386 }, "45;10": { "x": 45.5, "y": 8.660254037844386 }, "46;10": { "x": 46.5, "y": 8.660254037844386 }, "47;10": { "x": 47.5, "y": 8.660254037844386 }, "48;10": { "x": 48.5, "y": 8.660254037844386 }, "49;10": { "x": 49.5, "y": 8.660254037844386 }, "50;10": { "x": 50.5, "y": 8.660254037844386 }, "51;10": { "x": 51.5, "y": 8.660254037844386 }, "52;10": { "x": 52.5, "y": 8.660254037844386 }, "53;10": { "x": 53.5, "y": 8.660254037844386 }, "54;10": { "x": 54.5, "y": 8.660254037844386 }, "55;10": { "x": 55.5, "y": 8.660254037844386 }, "56;10": { "x": 56.5, "y": 8.660254037844386 }, "10;11": { "x": 10, "y": 9.526279441628825 }, "11;11": { "x": 11, "y": 9.526279441628825 }, "12;11": { "x": 12, "y": 9.526279441628825 }, "13;11": { "x": 13, "y": 9.526279441628825 }, "14;11": { "x": 14, "y": 9.526279441628825 }, "15;11": { "x": 15, "y": 9.526279441628825 }, "16;11": { "x": 16, "y": 9.526279441628825 }, "17;11": { "x": 17, "y": 9.526279441628825 }, "18;11": { "x": 18, "y": 9.526279441628825 }, "19;11": { "x": 19, "y": 9.526279441628825 }, "30;11": { "x": 30, "y": 9.526279441628825 }, "32;11": { "x": 32, "y": 9.526279441628825 }, "35;11": { "x": 35, "y": 9.526279441628825 }, "36;11": { "x": 36, "y": 9.526279441628825 }, "39;11": { "x": 39, "y": 9.526279441628825 }, "41;11": { "x": 41, "y": 9.526279441628825 }, "42;11": { "x": 42, "y": 9.526279441628825 }, "43;11": { "x": 43, "y": 9.526279441628825 }, "44;11": { "x": 44, "y": 9.526279441628825 }, "45;11": { "x": 45, "y": 9.526279441628825 }, "46;11": { "x": 46, "y": 9.526279441628825 }, "47;11": { "x": 47, "y": 9.526279441628825 }, "48;11": { "x": 48, "y": 9.526279441628825 }, "49;11": { "x": 49, "y": 9.526279441628825 }, "50;11": { "x": 50, "y": 9.526279441628825 }, "51;11": { "x": 51, "y": 9.526279441628825 }, "52;11": { "x": 52, "y": 9.526279441628825 }, "53;11": { "x": 53, "y": 9.526279441628825 }, "54;11": { "x": 54, "y": 9.526279441628825 }, "55;11": { "x": 55, "y": 9.526279441628825 }, "57;11": { "x": 57, "y": 9.526279441628825 }, "10;12": { "x": 10.5, "y": 10.392304845413264 }, "11;12": { "x": 11.5, "y": 10.392304845413264 }, "12;12": { "x": 12.5, "y": 10.392304845413264 }, "13;12": { "x": 13.5, "y": 10.392304845413264 }, "14;12": { "x": 14.5, "y": 10.392304845413264 }, "15;12": { "x": 15.5, "y": 10.392304845413264 }, "16;12": { "x": 16.5, "y": 10.392304845413264 }, "17;12": { "x": 17.5, "y": 10.392304845413264 }, "30;12": { "x": 30.5, "y": 10.392304845413264 }, "35;12": { "x": 35.5, "y": 10.392304845413264 }, "36;12": { "x": 36.5, "y": 10.392304845413264 }, "37;12": { "x": 37.5, "y": 10.392304845413264 }, "38;12": { "x": 38.5, "y": 10.392304845413264 }, "39;12": { "x": 39.5, "y": 10.392304845413264 }, "41;12": { "x": 41.5, "y": 10.392304845413264 }, "42;12": { "x": 42.5, "y": 10.392304845413264 }, "43;12": { "x": 43.5, "y": 10.392304845413264 }, "44;12": { "x": 44.5, "y": 10.392304845413264 }, "45;12": { "x": 45.5, "y": 10.392304845413264 }, "46;12": { "x": 46.5, "y": 10.392304845413264 }, "47;12": { "x": 47.5, "y": 10.392304845413264 }, "48;12": { "x": 48.5, "y": 10.392304845413264 }, "49;12": { "x": 49.5, "y": 10.392304845413264 }, "50;12": { "x": 50.5, "y": 10.392304845413264 }, "51;12": { "x": 51.5, "y": 10.392304845413264 }, "52;12": { "x": 52.5, "y": 10.392304845413264 }, "53;12": { "x": 53.5, "y": 10.392304845413264 }, "11;13": { "x": 11, "y": 11.258330249197702 }, "12;13": { "x": 12, "y": 11.258330249197702 }, "13;13": { "x": 13, "y": 11.258330249197702 }, "14;13": { "x": 14, "y": 11.258330249197702 }, "15;13": { "x": 15, "y": 11.258330249197702 }, "16;13": { "x": 16, "y": 11.258330249197702 }, "17;13": { "x": 17, "y": 11.258330249197702 }, "18;13": { "x": 18, "y": 11.258330249197702 }, "32;13": { "x": 32, "y": 11.258330249197702 }, "33;13": { "x": 33, "y": 11.258330249197702 }, "38;13": { "x": 38, "y": 11.258330249197702 }, "39;13": { "x": 39, "y": 11.258330249197702 }, "40;13": { "x": 40, "y": 11.258330249197702 }, "41;13": { "x": 41, "y": 11.258330249197702 }, "42;13": { "x": 42, "y": 11.258330249197702 }, "43;13": { "x": 43, "y": 11.258330249197702 }, "44;13": { "x": 44, "y": 11.258330249197702 }, "45;13": { "x": 45, "y": 11.258330249197702 }, "46;13": { "x": 46, "y": 11.258330249197702 }, "47;13": { "x": 47, "y": 11.258330249197702 }, "48;13": { "x": 48, "y": 11.258330249197702 }, "49;13": { "x": 49, "y": 11.258330249197702 }, "50;13": { "x": 50, "y": 11.258330249197702 }, "51;13": { "x": 51, "y": 11.258330249197702 }, "52;13": { "x": 52, "y": 11.258330249197702 }, "54;13": { "x": 54, "y": 11.258330249197702 }, "56;13": { "x": 56, "y": 11.258330249197702 }, "11;14": { "x": 11.5, "y": 12.12435565298214 }, "12;14": { "x": 12.5, "y": 12.12435565298214 }, "13;14": { "x": 13.5, "y": 12.12435565298214 }, "14;14": { "x": 14.5, "y": 12.12435565298214 }, "15;14": { "x": 15.5, "y": 12.12435565298214 }, "16;14": { "x": 16.5, "y": 12.12435565298214 }, "30;14": { "x": 30.5, "y": 12.12435565298214 }, "31;14": { "x": 31.5, "y": 12.12435565298214 }, "32;14": { "x": 32.5, "y": 12.12435565298214 }, "33;14": { "x": 33.5, "y": 12.12435565298214 }, "35;14": { "x": 35.5, "y": 12.12435565298214 }, "38;14": { "x": 38.5, "y": 12.12435565298214 }, "39;14": { "x": 39.5, "y": 12.12435565298214 }, "40;14": { "x": 40.5, "y": 12.12435565298214 }, "41;14": { "x": 41.5, "y": 12.12435565298214 }, "42;14": { "x": 42.5, "y": 12.12435565298214 }, "43;14": { "x": 43.5, "y": 12.12435565298214 }, "44;14": { "x": 44.5, "y": 12.12435565298214 }, "45;14": { "x": 45.5, "y": 12.12435565298214 }, "46;14": { "x": 46.5, "y": 12.12435565298214 }, "47;14": { "x": 47.5, "y": 12.12435565298214 }, "48;14": { "x": 48.5, "y": 12.12435565298214 }, "49;14": { "x": 49.5, "y": 12.12435565298214 }, "50;14": { "x": 50.5, "y": 12.12435565298214 }, "51;14": { "x": 51.5, "y": 12.12435565298214 }, "52;14": { "x": 52.5, "y": 12.12435565298214 }, "54;14": { "x": 54.5, "y": 12.12435565298214 }, "13;15": { "x": 13, "y": 12.990381056766578 }, "14;15": { "x": 14, "y": 12.990381056766578 }, "17;15": { "x": 17, "y": 12.990381056766578 }, "30;15": { "x": 30, "y": 12.990381056766578 }, "31;15": { "x": 31, "y": 12.990381056766578 }, "32;15": { "x": 32, "y": 12.990381056766578 }, "33;15": { "x": 33, "y": 12.990381056766578 }, "34;15": { "x": 34, "y": 12.990381056766578 }, "35;15": { "x": 35, "y": 12.990381056766578 }, "36;15": { "x": 36, "y": 12.990381056766578 }, "37;15": { "x": 37, "y": 12.990381056766578 }, "38;15": { "x": 38, "y": 12.990381056766578 }, "39;15": { "x": 39, "y": 12.990381056766578 }, "40;15": { "x": 40, "y": 12.990381056766578 }, "41;15": { "x": 41, "y": 12.990381056766578 }, "42;15": { "x": 42, "y": 12.990381056766578 }, "43;15": { "x": 43, "y": 12.990381056766578 }, "44;15": { "x": 44, "y": 12.990381056766578 }, "45;15": { "x": 45, "y": 12.990381056766578 }, "46;15": { "x": 46, "y": 12.990381056766578 }, "47;15": { "x": 47, "y": 12.990381056766578 }, "48;15": { "x": 48, "y": 12.990381056766578 }, "49;15": { "x": 49, "y": 12.990381056766578 }, "50;15": { "x": 50, "y": 12.990381056766578 }, "51;15": { "x": 51, "y": 12.990381056766578 }, "52;15": { "x": 52, "y": 12.990381056766578 }, "13;16": { "x": 13.5, "y": 13.856406460551018 }, "29;16": { "x": 29.5, "y": 13.856406460551018 }, "30;16": { "x": 30.5, "y": 13.856406460551018 }, "31;16": { "x": 31.5, "y": 13.856406460551018 }, "32;16": { "x": 32.5, "y": 13.856406460551018 }, "33;16": { "x": 33.5, "y": 13.856406460551018 }, "34;16": { "x": 34.5, "y": 13.856406460551018 }, "35;16": { "x": 35.5, "y": 13.856406460551018 }, "36;16": { "x": 36.5, "y": 13.856406460551018 }, "37;16": { "x": 37.5, "y": 13.856406460551018 }, "38;16": { "x": 38.5, "y": 13.856406460551018 }, "39;16": { "x": 39.5, "y": 13.856406460551018 }, "40;16": { "x": 40.5, "y": 13.856406460551018 }, "41;16": { "x": 41.5, "y": 13.856406460551018 }, "44;16": { "x": 44.5, "y": 13.856406460551018 }, "45;16": { "x": 45.5, "y": 13.856406460551018 }, "46;16": { "x": 46.5, "y": 13.856406460551018 }, "47;16": { "x": 47.5, "y": 13.856406460551018 }, "48;16": { "x": 48.5, "y": 13.856406460551018 }, "49;16": { "x": 49.5, "y": 13.856406460551018 }, "50;16": { "x": 50.5, "y": 13.856406460551018 }, "51;16": { "x": 51.5, "y": 13.856406460551018 }, "14;17": { "x": 14, "y": 14.722431864335457 }, "16;17": { "x": 16, "y": 14.722431864335457 }, "19;17": { "x": 19, "y": 14.722431864335457 }, "29;17": { "x": 29, "y": 14.722431864335457 }, "30;17": { "x": 30, "y": 14.722431864335457 }, "31;17": { "x": 31, "y": 14.722431864335457 }, "32;17": { "x": 32, "y": 14.722431864335457 }, "33;17": { "x": 33, "y": 14.722431864335457 }, "34;17": { "x": 34, "y": 14.722431864335457 }, "35;17": { "x": 35, "y": 14.722431864335457 }, "36;17": { "x": 36, "y": 14.722431864335457 }, "37;17": { "x": 37, "y": 14.722431864335457 }, "38;17": { "x": 38, "y": 14.722431864335457 }, "39;17": { "x": 39, "y": 14.722431864335457 }, "40;17": { "x": 40, "y": 14.722431864335457 }, "41;17": { "x": 41, "y": 14.722431864335457 }, "45;17": { "x": 45, "y": 14.722431864335457 }, "46;17": { "x": 46, "y": 14.722431864335457 }, "49;17": { "x": 49, "y": 14.722431864335457 }, "50;17": { "x": 50, "y": 14.722431864335457 }, "16;18": { "x": 16.5, "y": 15.588457268119894 }, "29;18": { "x": 29.5, "y": 15.588457268119894 }, "30;18": { "x": 30.5, "y": 15.588457268119894 }, "31;18": { "x": 31.5, "y": 15.588457268119894 }, "32;18": { "x": 32.5, "y": 15.588457268119894 }, "33;18": { "x": 33.5, "y": 15.588457268119894 }, "34;18": { "x": 34.5, "y": 15.588457268119894 }, "35;18": { "x": 35.5, "y": 15.588457268119894 }, "36;18": { "x": 36.5, "y": 15.588457268119894 }, "37;18": { "x": 37.5, "y": 15.588457268119894 }, "38;18": { "x": 38.5, "y": 15.588457268119894 }, "39;18": { "x": 39.5, "y": 15.588457268119894 }, "45;18": { "x": 45.5, "y": 15.588457268119894 }, "49;18": { "x": 49.5, "y": 15.588457268119894 }, "50;18": { "x": 50.5, "y": 15.588457268119894 }, "17;19": { "x": 17, "y": 16.454482671904334 }, "19;19": { "x": 19, "y": 16.454482671904334 }, "20;19": { "x": 20, "y": 16.454482671904334 }, "30;19": { "x": 30, "y": 16.454482671904334 }, "31;19": { "x": 31, "y": 16.454482671904334 }, "32;19": { "x": 32, "y": 16.454482671904334 }, "33;19": { "x": 33, "y": 16.454482671904334 }, "34;19": { "x": 34, "y": 16.454482671904334 }, "35;19": { "x": 35, "y": 16.454482671904334 }, "36;19": { "x": 36, "y": 16.454482671904334 }, "37;19": { "x": 37, "y": 16.454482671904334 }, "38;19": { "x": 38, "y": 16.454482671904334 }, "39;19": { "x": 39, "y": 16.454482671904334 }, "40;19": { "x": 40, "y": 16.454482671904334 }, "45;19": { "x": 45, "y": 16.454482671904334 }, "49;19": { "x": 49, "y": 16.454482671904334 }, "50;19": { "x": 50, "y": 16.454482671904334 }, "18;20": { "x": 18.5, "y": 17.32050807568877 }, "19;20": { "x": 19.5, "y": 17.32050807568877 }, "20;20": { "x": 20.5, "y": 17.32050807568877 }, "21;20": { "x": 21.5, "y": 17.32050807568877 }, "33;20": { "x": 33.5, "y": 17.32050807568877 }, "34;20": { "x": 34.5, "y": 17.32050807568877 }, "35;20": { "x": 35.5, "y": 17.32050807568877 }, "36;20": { "x": 36.5, "y": 17.32050807568877 }, "37;20": { "x": 37.5, "y": 17.32050807568877 }, "38;20": { "x": 38.5, "y": 17.32050807568877 }, "39;20": { "x": 39.5, "y": 17.32050807568877 }, "48;20": { "x": 48.5, "y": 17.32050807568877 }, "49;20": { "x": 49.5, "y": 17.32050807568877 }, "18;21": { "x": 18, "y": 18.186533479473212 }, "19;21": { "x": 19, "y": 18.186533479473212 }, "20;21": { "x": 20, "y": 18.186533479473212 }, "21;21": { "x": 21, "y": 18.186533479473212 }, "22;21": { "x": 22, "y": 18.186533479473212 }, "34;21": { "x": 34, "y": 18.186533479473212 }, "35;21": { "x": 35, "y": 18.186533479473212 }, "36;21": { "x": 36, "y": 18.186533479473212 }, "37;21": { "x": 37, "y": 18.186533479473212 }, "38;21": { "x": 38, "y": 18.186533479473212 }, "51;21": { "x": 51, "y": 18.186533479473212 }, "52;21": { "x": 52, "y": 18.186533479473212 }, "55;21": { "x": 55, "y": 18.186533479473212 }, "17;22": { "x": 17.5, "y": 19.05255888325765 }, "18;22": { "x": 18.5, "y": 19.05255888325765 }, "19;22": { "x": 19.5, "y": 19.05255888325765 }, "20;22": { "x": 20.5, "y": 19.05255888325765 }, "21;22": { "x": 21.5, "y": 19.05255888325765 }, "22;22": { "x": 22.5, "y": 19.05255888325765 }, "23;22": { "x": 23.5, "y": 19.05255888325765 }, "24;22": { "x": 24.5, "y": 19.05255888325765 }, "34;22": { "x": 34.5, "y": 19.05255888325765 }, "35;22": { "x": 35.5, "y": 19.05255888325765 }, "36;22": { "x": 36.5, "y": 19.05255888325765 }, "37;22": { "x": 37.5, "y": 19.05255888325765 }, "56;22": { "x": 56.5, "y": 19.05255888325765 }, "18;23": { "x": 18, "y": 19.918584287042087 }, "19;23": { "x": 19, "y": 19.918584287042087 }, "20;23": { "x": 20, "y": 19.918584287042087 }, "21;23": { "x": 21, "y": 19.918584287042087 }, "22;23": { "x": 22, "y": 19.918584287042087 }, "23;23": { "x": 23, "y": 19.918584287042087 }, "24;23": { "x": 24, "y": 19.918584287042087 }, "25;23": { "x": 25, "y": 19.918584287042087 }, "34;23": { "x": 34, "y": 19.918584287042087 }, "35;23": { "x": 35, "y": 19.918584287042087 }, "36;23": { "x": 36, "y": 19.918584287042087 }, "37;23": { "x": 37, "y": 19.918584287042087 }, "38;23": { "x": 38, "y": 19.918584287042087 }, "60;23": { "x": 60, "y": 19.918584287042087 }, "18;24": { "x": 18.5, "y": 20.784609690826528 }, "19;24": { "x": 19.5, "y": 20.784609690826528 }, "20;24": { "x": 20.5, "y": 20.784609690826528 }, "21;24": { "x": 21.5, "y": 20.784609690826528 }, "22;24": { "x": 22.5, "y": 20.784609690826528 }, "23;24": { "x": 23.5, "y": 20.784609690826528 }, "24;24": { "x": 24.5, "y": 20.784609690826528 }, "34;24": { "x": 34.5, "y": 20.784609690826528 }, "35;24": { "x": 35.5, "y": 20.784609690826528 }, "36;24": { "x": 36.5, "y": 20.784609690826528 }, "37;24": { "x": 37.5, "y": 20.784609690826528 }, "38;24": { "x": 38.5, "y": 20.784609690826528 }, "53;24": { "x": 53.5, "y": 20.784609690826528 }, "54;24": { "x": 54.5, "y": 20.784609690826528 }, "56;24": { "x": 56.5, "y": 20.784609690826528 }, "20;25": { "x": 20, "y": 21.650635094610966 }, "21;25": { "x": 21, "y": 21.650635094610966 }, "22;25": { "x": 22, "y": 21.650635094610966 }, "23;25": { "x": 23, "y": 21.650635094610966 }, "24;25": { "x": 24, "y": 21.650635094610966 }, "34;25": { "x": 34, "y": 21.650635094610966 }, "35;25": { "x": 35, "y": 21.650635094610966 }, "36;25": { "x": 36, "y": 21.650635094610966 }, "37;25": { "x": 37, "y": 21.650635094610966 }, "40;25": { "x": 40, "y": 21.650635094610966 }, "53;25": { "x": 53, "y": 21.650635094610966 }, "54;25": { "x": 54, "y": 21.650635094610966 }, "55;25": { "x": 55, "y": 21.650635094610966 }, "56;25": { "x": 56, "y": 21.650635094610966 }, "57;25": { "x": 57, "y": 21.650635094610966 }, "19;26": { "x": 19.5, "y": 22.516660498395403 }, "20;26": { "x": 20.5, "y": 22.516660498395403 }, "21;26": { "x": 21.5, "y": 22.516660498395403 }, "22;26": { "x": 22.5, "y": 22.516660498395403 }, "34;26": { "x": 34.5, "y": 22.516660498395403 }, "35;26": { "x": 35.5, "y": 22.516660498395403 }, "36;26": { "x": 36.5, "y": 22.516660498395403 }, "37;26": { "x": 37.5, "y": 22.516660498395403 }, "39;26": { "x": 39.5, "y": 22.516660498395403 }, "51;26": { "x": 51.5, "y": 22.516660498395403 }, "52;26": { "x": 52.5, "y": 22.516660498395403 }, "53;26": { "x": 53.5, "y": 22.516660498395403 }, "54;26": { "x": 54.5, "y": 22.516660498395403 }, "55;26": { "x": 55.5, "y": 22.516660498395403 }, "56;26": { "x": 56.5, "y": 22.516660498395403 }, "57;26": { "x": 57.5, "y": 22.516660498395403 }, "19;27": { "x": 19, "y": 23.38268590217984 }, "20;27": { "x": 20, "y": 23.38268590217984 }, "21;27": { "x": 21, "y": 23.38268590217984 }, "22;27": { "x": 22, "y": 23.38268590217984 }, "35;27": { "x": 35, "y": 23.38268590217984 }, "36;27": { "x": 36, "y": 23.38268590217984 }, "37;27": { "x": 37, "y": 23.38268590217984 }, "52;27": { "x": 52, "y": 23.38268590217984 }, "53;27": { "x": 53, "y": 23.38268590217984 }, "54;27": { "x": 54, "y": 23.38268590217984 }, "55;27": { "x": 55, "y": 23.38268590217984 }, "56;27": { "x": 56, "y": 23.38268590217984 }, "57;27": { "x": 57, "y": 23.38268590217984 }, "58;27": { "x": 58, "y": 23.38268590217984 }, "19;28": { "x": 19.5, "y": 24.24871130596428 }, "20;28": { "x": 20.5, "y": 24.24871130596428 }, "21;28": { "x": 21.5, "y": 24.24871130596428 }, "35;28": { "x": 35.5, "y": 24.24871130596428 }, "52;28": { "x": 52.5, "y": 24.24871130596428 }, "55;28": { "x": 55.5, "y": 24.24871130596428 }, "56;28": { "x": 56.5, "y": 24.24871130596428 }, "57;28": { "x": 57.5, "y": 24.24871130596428 }, "19;29": { "x": 19, "y": 25.11473670974872 }, "20;29": { "x": 20, "y": 25.11473670974872 }, "21;29": { "x": 21, "y": 25.11473670974872 }, "57;29": { "x": 57, "y": 25.11473670974872 }, "18;30": { "x": 18.5, "y": 25.980762113533157 }, "19;30": { "x": 19.5, "y": 25.980762113533157 }, "57;30": { "x": 57.5, "y": 25.980762113533157 }, "62;30": { "x": 62.5, "y": 25.980762113533157 }, "19;31": { "x": 19, "y": 26.846787517317598 }, "18;32": { "x": 18.5, "y": 27.712812921102035 }, "19;32": { "x": 19.5, "y": 27.712812921102035 }, "19;33": { "x": 19, "y": 28.578838324886473 }, "19;34": { "x": 19.5, "y": 29.444863728670914 } }, "X_MIN": -19926188.85199597, "Y_MIN": -7558415.656081783, "X_MAX": 19926188.85199597, "Y_MAX": 11402272.159297233, "X_RANGE": 39852377.70399194, "Y_RANGE": 18960687.815379016, "region": { "lat": { "min": -56, "max": 71 }, "lng": { "min": -179, "max": 179 } }, "grid": "diagonal", "height": 30, "width": 63, "ystep": 0.8660254037844386 };
|
||
|
||
// 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 };
|