"use client"; import { useMotionValueEvent, useSpring } from "framer-motion"; import { ArrowDownRight, ArrowUpRight, BarChart3, Bell, Box, ChevronRight, ChevronsUpDown, ClipboardList, Facebook, Globe, HelpCircle, 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 { Area, AreaChart, Bar, BarChart, CartesianGrid, Cell, 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 { Input } from "@/components/ui/input"; 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 { Tooltip as ShadTooltip, TooltipContent as ShadTooltipContent, TooltipProvider as ShadTooltipProvider, TooltipTrigger as ShadTooltipTrigger, } 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})`, }, }; const getRetentionColor = (percent: number): string => { const mix = Math.max(5, Math.round(percent * 0.85)); return `color-mix(in oklch, var(--primary) ${mix}%, ${mixBase})`; }; // ============================================================================ // Types // ============================================================================ type NavItem = { label: string; icon: React.ComponentType>; 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 Segment = { name: string; value: number; color: string; change: number; }; type Channel = { icon: React.ComponentType>; name: string; count: number; change: number; }; type CohortRow = { label: string; retentionByMonth: 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 oneDecimalFormatter = new Intl.NumberFormat("fa-IR", { maximumFractionDigits: 1, }); const monthFormatter = new Intl.DateTimeFormat("fa-IR", { month: "short" }); const monthLabel = (monthIndex: number) => monthFormatter.format(new Date(2025, monthIndex, 1)); const weekdayFormatter = new Intl.DateTimeFormat("fa-IR", { weekday: "short", }); 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}٪`; }; // ============================================================================ // Hooks // ============================================================================ function useHoverHighlight() { const [active, setActive] = React.useState(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-5.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" }, { title: "مشتریان جدید", value: 12, 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 segments: Segment[] = [ { name: "سازمانی", value: 1074, color: palette.primary, change: 3.2 }, { name: "کسب‌وکار متوسط", value: 836, color: palette.secondary.light, change: -1.5 }, { name: "شخصی", value: 478, color: palette.tertiary.light, change: 5.8, }, ]; const customerTrendData = [ { day: weekdayFormatter.format(new Date(2025, 0, 6)), customers: 1850 }, { day: weekdayFormatter.format(new Date(2025, 0, 7)), customers: 2150 }, { day: weekdayFormatter.format(new Date(2025, 0, 8)), customers: 1720 }, { day: weekdayFormatter.format(new Date(2025, 0, 9)), customers: 2480 }, { day: weekdayFormatter.format(new Date(2025, 0, 10)), customers: 1890 }, { day: weekdayFormatter.format(new Date(2025, 0, 11)), customers: 2320 }, { day: weekdayFormatter.format(new Date(2025, 0, 12)), customers: 2388 }, ]; 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 cohortRetentionData: CohortRow[] = [ { label: monthLabel(5), retentionByMonth: [100, 65, 51, 43, 37, 32, 28] }, { label: monthLabel(6), retentionByMonth: [100, 71, 59, 50, 43, 37] }, { label: monthLabel(7), retentionByMonth: [100, 73, 60, 51, 44] }, { label: monthLabel(8), retentionByMonth: [100, 69, 55, 46] }, { label: monthLabel(9), retentionByMonth: [100, 75, 62] }, { label: monthLabel(10), retentionByMonth: [100, 67] }, { label: monthLabel(11), retentionByMonth: [100] }, ]; const storeVisitsData = [ { date: monthLabel(0), visits: 245 }, { date: monthLabel(1), visits: 654 }, { date: monthLabel(2), visits: 387 }, { date: monthLabel(3), visits: 521 }, { date: monthLabel(4), visits: 412 }, { date: monthLabel(5), visits: 598 }, { date: monthLabel(6), visits: 312 }, { date: monthLabel(7), visits: 743 }, { date: monthLabel(8), visits: 489 }, { date: monthLabel(9), visits: 476 }, { date: monthLabel(10), visits: 687 }, { date: monthLabel(11), visits: 198 }, ]; // ============================================================================ // Chart Configs // ============================================================================ const revenueChartConfig = { revenue: { label: "درآمد", color: palette.secondary.light, }, } satisfies ChartConfig; const visitsChartConfig = { visits: { label: "بازدید", color: palette.secondary.light, }, } satisfies ChartConfig; const customerChartConfig = { customers: { label: "مشتریان", color: palette.secondary.light, }, } satisfies ChartConfig; // ============================================================================ // Sidebar Components // ============================================================================ const SidebarLogo = ({ logo }: { logo: SidebarData["logo"] }) => { return (
{logo.alt}
{logo.title} {logo.description}
); }; const NavMenuItem = ({ item }: { item: NavItem }) => { const Icon = item.icon; const hasChildren = item.children && item.children.length > 0; if (!hasChildren) { return ( ); } return ( {item.children!.map((child) => ( {child.label} ))} ); }; const NavUser = ({ user }: { user: UserData }) => { const initials = user.name .split(" ") .map((n) => n[0]) .join(""); const userAvatar = ( {initials} ); const userInfo = (
{user.name} {user.email}
); return ( {userAvatar} {userInfo}
{userAvatar} {userInfo}
); }; const AppSidebar = ({ ...props }: React.ComponentProps) => { return (
{sidebarData.navGroups.map((group) => ( {group.title} {group.items.map((item) => ( ))} ))} {sidebarData.user && }
); }; // ============================================================================ // Dashboard Header // ============================================================================ const DashboardHeader = () => { return (
); }; // ============================================================================ // Dashboard Content Components // ============================================================================ // Growth section component (vertical layout for SalesRevenueCard header) const GrowthSection = ({ label, change, }: { label: string; change: number; }) => { const isPositive = change >= 0; return (
{label} {isPositive ? (
); }; // 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 (
{stat.title}
{formatValue(stat.value, stat.format)}
{isPositive ? (
); }; // KPI Stats Row const KPIStatsRow = () => { return (
{kpiStats.map((stat) => ( ))}
); }; // ============================================================================ // Animated Reference Line Components // ============================================================================ interface CustomReferenceLabelProps { viewBox?: { x?: number; y?: number }; value: number; } const CustomReferenceLabel: React.FC = (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 ( <> {compactCurrencyFormatter.format(value)} ); }; // Revenue Bar Chart Tooltip function RevenueBarTooltip({ active, payload, label, }: any) { if (!active || !payload?.length) return null; const value = payload[0]?.value || 0; return (

{label}

درآمد: {currencyFormatter.format(Number(value))}
); } function CustomersAreaTooltip({ active, payload, label, }: any) { if (!active || !payload?.length) return null; const value = Number(payload[0]?.value ?? 0); const pointIndex = customerTrendData.findIndex( (entry) => entry.day === label, ); const previousValue = pointIndex > 0 ? customerTrendData[pointIndex - 1].customers : value; const delta = previousValue === 0 ? 0 : ((value - previousValue) / previousValue) * 100; const isPositive = delta >= 0; return (

{label}

مشتریان: {numberFormatter.format(value)}

{formatChangePercent(delta)}

); } // 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( 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 (
{/* Left: Title + Value + Change */}
درآمد فروش {currencyFormatter.format(maxValueData.value)}
{formatChangePercent(changePercent)} {isPositive ? (
{/* Right: Growth sections */}
setActiveIndex(undefined)} > } cursor={{ fillOpacity: 0.05 }} /> {monthlyRevenueData.map((_, index) => ( setActiveIndex(index)} /> ))} } />
); }; // Segmentation Card (Conversion Rate Style) const SegmentationCard = () => { const [activeCustomerIndex, setActiveCustomerIndex] = React.useState< number | undefined >(undefined); const trackedCustomerPoint = React.useMemo(() => { if ( activeCustomerIndex !== undefined && activeCustomerIndex >= 0 && activeCustomerIndex < customerTrendData.length ) { return { index: activeCustomerIndex, ...customerTrendData[activeCustomerIndex], }; } const fallbackIndex = customerTrendData.length - 1; return { index: fallbackIndex, ...customerTrendData[fallbackIndex] }; }, [activeCustomerIndex]); const previousCustomerPoint = React.useMemo(() => { const previousIndex = Math.max(0, trackedCustomerPoint.index - 1); return customerTrendData[previousIndex]; }, [trackedCustomerPoint.index]); const totalCustomers = trackedCustomerPoint.customers; const totalChange = previousCustomerPoint.customers === 0 ? 0 : ((totalCustomers - previousCustomerPoint.customers) / previousCustomerPoint.customers) * 100; const comparisonLabel = trackedCustomerPoint.index === 0 ? "نسبت به ابتدای هفته" : `نسبت به ${previousCustomerPoint.day}`; const isPositive = totalChange >= 0; return (
{/* Header - matches Sales Revenue layout */}
{/* Left: Label + Value + Change */}
کل مشتریان {numberFormatter.format(totalCustomers)} {trackedCustomerPoint.day}
{formatChangePercent(totalChange)} {isPositive ? (
{/* Right: Details button */}
{/* Segment metrics rows */}
{segments.map((segment) => { const isPositive = segment.change >= 0; return (
{segment.name}
{numberFormatter.format(segment.value)}
{isPositive ? (
); })}
{/* Area chart */}
{ if (typeof state.activeTooltipIndex === "number") { setActiveCustomerIndex(state.activeTooltipIndex); } }} onMouseLeave={() => setActiveCustomerIndex(undefined)} > compactNumberFormatter.format(v)} /> } cursor={{ stroke: "var(--color-customers)", strokeDasharray: "3 3", strokeOpacity: 0.35, }} />
); }; // User Retention Cohort Heatmap Card const UserRetentionCard = () => { const maxMonths = 7; return (
{/* Header */}
نگهداشت کاربر {percentFormatter.format(0.24)}
{formatChangePercent(2.0)}
{/* Heatmap */}
{cohortRetentionData.map((cohort) => (
{cohort.retentionByMonth.map((value, monthIndex) => (
{cohort.label} ماه{" "} {(monthIndex + 1).toLocaleString("fa-IR")}:{" "} {value.toLocaleString("fa-IR")}٪ ))}
))}
{/* X-axis labels */}
{Array.from({ length: maxMonths }, (_, i) => ( {(i + 1).toLocaleString("fa-IR")} ))}
); }; const StoreVisitsTargetLineChart = () => { const chartWrapperRef = React.useRef(null); const [chartWidth, setChartWidth] = React.useState(0); const [axis, setAxis] = React.useState(0); const lastVisitValue = storeVisitsData[storeVisitsData.length - 1]?.visits ?? 0; const axisSpring = useSpring(0, { stiffness: 100, damping: 30, }); const valueSpring = useSpring(lastVisitValue, { stiffness: 100, damping: 30, }); const [springyValue, setSpringyValue] = React.useState(lastVisitValue); useMotionValueEvent(axisSpring, "change", (latest) => { setAxis(Number(latest)); }); useMotionValueEvent(valueSpring, "change", (latest) => { setSpringyValue(Number(Number(latest).toFixed(0))); }); React.useEffect(() => { const el = chartWrapperRef.current; if (!el) return; const updateWidth = () => { const nextWidth = Math.max( 0, Math.floor(el.getBoundingClientRect().width), ); setChartWidth(nextWidth); setAxis((prev) => (prev === 0 ? nextWidth : prev)); }; updateWidth(); const ro = new ResizeObserver(updateWidth); ro.observe(el); return () => { ro.disconnect(); }; }, []); React.useEffect(() => { valueSpring.set(lastVisitValue); }, [lastVisitValue, valueSpring]); const safeAxis = chartWidth > 0 ? Math.min(Math.max(axis, 0), chartWidth) : axis; const rightInset = chartWidth > 0 ? Math.max(0, chartWidth - safeAxis) : 0; const clipPath = chartWidth > 0 ? `inset(0px ${rightInset}px 0px 0px)` : undefined; const tagWidth = 56; const tagHeight = 18; const tagHalfWidth = tagWidth / 2; const tagX = chartWidth > 0 ? Math.min(Math.max(safeAxis - tagWidth, 0), chartWidth - tagWidth) : 0; const textX = chartWidth > 0 ? Math.min( Math.max(safeAxis - tagHalfWidth, tagHalfWidth), chartWidth - tagHalfWidth, ) : 0; return (
{ const x = state.activeCoordinate?.x; const dataValue = state.activePayload?.[0]?.value; if (typeof x === "number" && dataValue !== undefined) { axisSpring.set(x); valueSpring.set(Number(dataValue)); } }} onMouseLeave={() => { if (chartWidth > 0) axisSpring.set(chartWidth); valueSpring.set(lastVisitValue); }} > {compactNumberFormatter.format(springyValue)}
); }; // Store Visits Card with Area Chart const StoreVisitsCard = () => { const totalVisits = 701340000; const changePercent = -12.5; return (
{/* Left: Title + Value + Change */}
بازدید فروشگاه {compactNumberFormatter.format(totalVisits)}
{formatChangePercent(changePercent, false)}
{/* Right: Growth sections */}
); }; // Main Dashboard Content const DashboardContent = () => { return (
{/* KPI Stats Row */} {/* Middle Row: Sales Revenue + Segmentation */}
{/* Bottom Row: User Retention + Store Visits */}
); }; // ============================================================================ // Main Dashboard Component // ============================================================================ const Dashboard11 = ({ className }: { className?: string }) => { return ( رفتن به محتوای اصلی ); }; export { Dashboard11 };