From eadb58da60150b0b22ed07dd07dc7eb36d30ee2d Mon Sep 17 00:00:00 2001 From: Saeed Abadiyan Date: Thu, 28 Aug 2025 13:02:11 +0330 Subject: [PATCH] fix styles in tabs and add d3js component for main section in dashboard-home ,also add chartBar --- app/components/dashboard/d3-image-info.tsx | 241 ++++++++++++++++++ .../dashboard/dashboard-custom-bar-chart.tsx | 2 +- app/components/dashboard/dashboard-home.tsx | 223 +++++++--------- app/components/dashboard/dashboard-layout.tsx | 137 ---------- app/components/dashboard/header.tsx | 6 +- .../dashboard/interactive-bar-chart.tsx | 125 +++++++++ app/components/dashboard/sidebar.tsx | 49 ++-- app/components/ui/tabs.tsx | 45 +++- 8 files changed, 535 insertions(+), 293 deletions(-) create mode 100644 app/components/dashboard/d3-image-info.tsx create mode 100644 app/components/dashboard/interactive-bar-chart.tsx diff --git a/app/components/dashboard/d3-image-info.tsx b/app/components/dashboard/d3-image-info.tsx new file mode 100644 index 0000000..becf542 --- /dev/null +++ b/app/components/dashboard/d3-image-info.tsx @@ -0,0 +1,241 @@ +"use client"; + +import React, { useEffect, useRef, useState } from "react"; +import * as d3 from "d3"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from "~/components/ui/dialog"; + +export type D3ImageInfoProps = { + imageUrl?: string; + title?: string; + description?: string; + width?: number; // fallback width if container size not measured yet + height?: number; // fallback height +}; + +/** + * D3ImageInfo + * - Renders an image and an information box beside it using D3 within an SVG. + * - Includes a clickable "show" chip that opens a popup dialog with more details. + */ +export function D3ImageInfo({ + imageUrl = "/placeholder.svg", + title = "عنوان آیتم", + description = "توضیحات تکمیلی در مورد این آیتم در این قسمت نمایش داده می‌شود.", + width = 800, + height = 360, +}: D3ImageInfoProps) { + const containerRef = useRef(null); + const svgRef = useRef(null); + const [open, setOpen] = useState(false); + + // Redraw helper + const draw = () => { + if (!containerRef.current || !svgRef.current) return; + + const container = containerRef.current; + const svg = d3.select(svgRef.current); + + const W = Math.max(480, container.clientWidth || width); + const H = Math.max(260, height); + + svg.attr("width", W).attr("height", H); + + // Clear previous content + svg.selectAll("*").remove(); + + // Layout + const padding = 16; + const imageAreaWidth = Math.min(300, Math.max(220, W * 0.35)); + const infoAreaX = padding + imageAreaWidth + padding; + const infoAreaWidth = W - infoAreaX - padding; + + // Image area (with rounded border) + const imgGroup = svg + .append("g") + .attr("transform", `translate(${padding}, ${padding})`); + + const imgW = imageAreaWidth; + const imgH = H - 2 * padding; + + // Frame + imgGroup + .append("rect") + .attr("width", imgW) + .attr("height", imgH) + .attr("rx", 10) + .attr("ry", 10) + .attr("fill", "#1F2937") // gray-800 + .attr("stroke", "#4B5563") // gray-600 + .attr("stroke-width", 1.5); + + // Image + imgGroup + .append("image") + .attr("href", imageUrl) + .attr("x", 4) + .attr("y", 4) + .attr("width", imgW - 8) + .attr("height", imgH - 8) + .attr("preserveAspectRatio", "xMidYMid slice") + .attr("clip-path", null); + + // Info area + const infoGroup = svg + .append("g") + .attr("transform", `translate(${infoAreaX}, ${padding})`); + + // Info container + infoGroup + .append("rect") + .attr("width", Math.max(220, infoAreaWidth)) + .attr("height", imgH) + .attr("rx", 12) + .attr("ry", 12) + .attr("fill", "url(#infoGradient)") + .attr("stroke", "#6B7280") // gray-500 + .attr("stroke-width", 1); + + // Background gradient + const defs = svg.append("defs"); + const gradient = defs + .append("linearGradient") + .attr("id", "infoGradient") + .attr("x1", "0%") + .attr("y1", "0%") + .attr("x2", "0%") + .attr("y2", "100%"); + + gradient.append("stop").attr("offset", "0%").attr("stop-color", "#111827"); // gray-900 + gradient + .append("stop") + .attr("offset", "100%") + .attr("stop-color", "#374151"); // gray-700 + + // Title + infoGroup + .append("text") + .attr("x", 16) + .attr("y", 36) + .attr("fill", "#F9FAFB") // gray-50 + .attr("font-weight", 700) + .attr("font-size", 18) + .text(title); + + // Description (wrapped) + const wrapText = (text: string, maxWidth: number) => { + const words = text.split(/\s+/).reverse(); + const lines: string[] = []; + let line: string[] = []; + let t = ""; + while (words.length) { + const word = words.pop()!; + const test = (t + " " + word).trim(); + // Approximate measure using character count + const tooLong = test.length * 8 > maxWidth; // 8px avg char width + if (tooLong && t.length) { + lines.push(t); + t = word; + } else { + t = test; + } + } + if (t) lines.push(t); + return lines; + }; + + const descMaxWidth = Math.max(200, infoAreaWidth - 32); + const descLines = wrapText(description, descMaxWidth); + + descLines.forEach((line, i) => { + infoGroup + .append("text") + .attr("x", 16) + .attr("y", 70 + i * 22) + .attr("fill", "#E5E7EB") // gray-200 + .attr("font-size", 14) + .text(line); + }); + + // Show button-like chip + const chipY = Math.min(imgH - 48, 70 + descLines.length * 22 + 16); + const chip = infoGroup + .append("g") + .attr("class", "show-chip") + .style("cursor", "pointer"); + + const chipW = 120; + const chipH = 36; + + chip + .append("rect") + .attr("x", 16) + .attr("y", chipY) + .attr("width", chipW) + .attr("height", chipH) + .attr("rx", 8) + .attr("ry", 8) + .attr("fill", "#3B82F6") // blue-500 + .attr("stroke", "#60A5FA") + .attr("stroke-width", 1.5) + .attr("opacity", 0.95); + + chip + .append("text") + .attr("x", 16 + chipW / 2) + .attr("y", chipY + chipH / 2 + 5) + .attr("text-anchor", "middle") + .attr("fill", "#FFFFFF") + .attr("font-weight", 700) + .text("نمایش"); + + // Hover & click + chip + .on("mouseenter", function () { + d3.select(this).select("rect").attr("fill", "#2563EB"); // blue-600 + }) + .on("mouseleave", function () { + d3.select(this).select("rect").attr("fill", "#3B82F6"); // blue-500 + }) + .on("click", () => setOpen(true)); + }; + + useEffect(() => { + const ro = new ResizeObserver(() => draw()); + if (containerRef.current) ro.observe(containerRef.current); + draw(); + return () => ro.disconnect(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [imageUrl, title, description]); + + return ( +
+
+ +
+ + + + + {title} + + {description} + + +
+ {title} +
+
+
+
+ ); +} diff --git a/app/components/dashboard/dashboard-custom-bar-chart.tsx b/app/components/dashboard/dashboard-custom-bar-chart.tsx index 516cffb..fff9e2a 100644 --- a/app/components/dashboard/dashboard-custom-bar-chart.tsx +++ b/app/components/dashboard/dashboard-custom-bar-chart.tsx @@ -54,7 +54,7 @@ export function DashboardCustomBarChart({
{/* Animated bar */}
diff --git a/app/components/dashboard/dashboard-home.tsx b/app/components/dashboard/dashboard-home.tsx index 4af7528..bcf8b47 100644 --- a/app/components/dashboard/dashboard-home.tsx +++ b/app/components/dashboard/dashboard-home.tsx @@ -31,6 +31,8 @@ import { import { Tabs, TabsList, TabsTrigger, TabsContent } from "~/components/ui/tabs"; import { CustomBarChart } from "~/components/ui/custom-bar-chart"; import { DashboardCustomBarChart } from "./dashboard-custom-bar-chart"; +import { InteractiveBarChart } from "./interactive-bar-chart"; +import { D3ImageInfo } from "./d3-image-info"; import { Label, PolarGrid, @@ -130,12 +132,81 @@ export function DashboardHome() { }, }; + // Skeleton component for cards + const SkeletonCard = ({ className = "" }) => ( +
+
+
+
+
+
+
+
+ ); + + // Skeleton for the chart + const SkeletonChart = () => ( +
+
+
+
+
+
+
+
+
+
+ {[...Array(12)].map((_, i) => ( +
+
+
+
+
+ ))} +
+
+ {[...Array(6)].map((_, i) => ( +
+ ))} +
+
+ ); + if (loading) { return ( -
-
-
+
+ {/* Top Cards Row */} +
+ + + + +
+ + {/* Middle Section */} +
+ {/* Chart Section */} + +
+ + {/* Right Sidebar */} +
+ + + +
@@ -546,129 +617,33 @@ export function DashboardHome() { {/* Main Content with Tabs */} - - - مقایسه ای - - - شماتیک - - +
+

+ تحقق ارزش ها +

+ + + شماتیک + + + مقایسه ای + + +
- -
- {/* Right Section - Charts */} -
- {/* Main Chart */} - - - - تحلیل ارزش‌ها - -

- نمودار مقایسه‌ای عملکرد ماهانه -

-
- -
- - - - - - - - - - - -
-
-
-
+ + + + + +
+
diff --git a/app/components/dashboard/dashboard-layout.tsx b/app/components/dashboard/dashboard-layout.tsx index a320bda..8406aa6 100644 --- a/app/components/dashboard/dashboard-layout.tsx +++ b/app/components/dashboard/dashboard-layout.tsx @@ -144,145 +144,8 @@ export function DashboardHome() { - -
24
-

- +2 از ماه گذشته -

-
- - - - - - پروژه‌های فعال - - - - - - - -
12
-

- +1 از هفته گذشته -

-
-
- - - - - پروژه‌های تکمیل شده - - - - - - -
8
-

- +3 از ماه گذشته -

-
-
- - - - - درصد موفقیت - - - - - - -
85%
-

- +5% از ماه گذشته -

-
- - {/* Recent Projects */} - - - پروژه‌های اخیر - - -
- {[ - { - name: "سیستم مدیریت محتوا", - status: "در حال انجام", - progress: 75, - }, - { name: "اپلیکیشن موبایل", status: "تکمیل شده", progress: 100 }, - { - name: "پلتفرم تجارت الکترونیک", - status: "شروع شده", - progress: 25, - }, - { - name: "سیستم مدیریت مالی", - status: "در حال بررسی", - progress: 10, - }, - ].map((project, index) => ( -
-
-
-

- {project.name} -

-

- {project.status} -

-
-
-
-
-
- - {project.progress}% - -
-
- ))} -
-
-
); diff --git a/app/components/dashboard/header.tsx b/app/components/dashboard/header.tsx index f160a85..32c7fb0 100644 --- a/app/components/dashboard/header.tsx +++ b/app/components/dashboard/header.tsx @@ -4,7 +4,7 @@ import { Link } from "react-router"; import { cn } from "~/lib/utils"; import { Button } from "~/components/ui/button"; import { -PanelLeft, + PanelLeft, Search, Bell, Settings, @@ -54,7 +54,9 @@ export function Header({ )} {/* Page Title */} -

{title}

+

+ {title} +

{/* Right Section */} diff --git a/app/components/dashboard/interactive-bar-chart.tsx b/app/components/dashboard/interactive-bar-chart.tsx new file mode 100644 index 0000000..703e448 --- /dev/null +++ b/app/components/dashboard/interactive-bar-chart.tsx @@ -0,0 +1,125 @@ +import { Bar, BarChart, CartesianGrid, XAxis, YAxis, LabelList } from "recharts"; +import React, { useState, useEffect } from "react"; + +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "~/components/ui/card"; +import { + type ChartConfig, + ChartContainer, + ChartTooltip, + ChartTooltipContent, +} from "~/components/ui/chart"; + +export const description = "An interactive bar chart"; + +const chartData = [ + { category: "کیمیا", ideas: 12, revenue: 850, cost: 320 }, + { category: "ُفرآروزش", ideas: 19, revenue: 1200, cost: 450 }, + { category: "خوارزمی", ideas: 15, revenue: 1400, cost: 520 }, +]; + +const chartConfig = { + ideas: { + label: "ایده‌ها", + color: "#60A5FA", // Blue-400 + }, + revenue: { + label: "درآمد (میلیون)", + color: "#4ADE80", // Green-400 + }, + cost: { + label: "کاهش هزینه (میلیون)", + color: "#F87171", // Red-400 + }, +} satisfies ChartConfig; + +export function InteractiveBarChart() { + const [activeChart, setActiveChart] = + React.useState("ideas"); + + const total = React.useMemo( + () => ({ + ideas: chartData.reduce((acc, curr) => acc + curr.ideas, 0), + revenue: chartData.reduce((acc, curr) => acc + curr.revenue, 0), + cost: chartData.reduce((acc, curr) => acc + curr.cost, 0), + }), + [], + ); + + return ( + + + + + + + `${value}%`} + /> + + + + + + + + + + + + + + ); +} diff --git a/app/components/dashboard/sidebar.tsx b/app/components/dashboard/sidebar.tsx index 1803dd0..717f204 100644 --- a/app/components/dashboard/sidebar.tsx +++ b/app/components/dashboard/sidebar.tsx @@ -153,7 +153,7 @@ export function Sidebar({ menuItems.forEach((item) => { if (item.children) { const hasActiveChild = item.children.some( - (child) => child.href && location.pathname === child.href + (child) => child.href && location.pathname === child.href, ); if (hasActiveChild) { newExpandedItems.push(item.id); @@ -171,10 +171,10 @@ export function Sidebar({ setExpandedItems((prev) => { // If trying to collapse, check if any child is active if (prev.includes(itemId)) { - const item = menuItems.find(menuItem => menuItem.id === itemId); + const item = menuItems.find((menuItem) => menuItem.id === itemId); if (item?.children) { const hasActiveChild = item.children.some( - (child) => child.href && location.pathname === child.href + (child) => child.href && location.pathname === child.href, ); // Don't collapse if a child is active if (hasActiveChild) { @@ -200,10 +200,12 @@ export function Sidebar({ const renderMenuItem = (item: MenuItem, level = 0) => { const isActive = isActiveRoute(item.href, item.children); - const isExpanded = expandedItems.includes(item.id) || - (item.children && item.children.some(child => - child.href && location.pathname === child.href - )); + const isExpanded = + expandedItems.includes(item.id) || + (item.children && + item.children.some( + (child) => child.href && location.pathname === child.href, + )); const hasChildren = item.children && item.children.length > 0; const ItemIcon = item.icon; @@ -228,7 +230,8 @@ export function Sidebar({ ? " text-emerald-400 border-r-2 border-emerald-400" : "text-gray-300 hover:bg-gradient-to-r hover:from-emerald-500/10 hover:to-teal-500/10 hover:text-emerald-300", isCollapsed && level === 0 && "justify-center px-2", - item.id === "logout" && "hover:bg-red-500/10 hover:text-red-400", + item.id === "logout" && + "hover:bg-red-500/10 hover:text-red-400", )} >
@@ -269,9 +272,11 @@ export function Sidebar({ className={cn( "w-full text-right", // Disable pointer cursor when child is active (cannot collapse) - item.children && item.children.some(child => - child.href && location.pathname === child.href - ) && "cursor-not-allowed" + item.children && + item.children.some( + (child) => child.href && location.pathname === child.href, + ) && + "cursor-not-allowed", )} onClick={handleClick} > @@ -283,7 +288,8 @@ export function Sidebar({ ? " text-emerald-400 border-r-2 border-emerald-400" : "text-gray-300 hover:bg-gradient-to-r hover:from-emerald-500/10 hover:to-teal-500/10 hover:text-emerald-300", isCollapsed && level === 0 && "justify-center px-2", - item.id === "logout" && "hover:bg-red-500/10 hover:text-red-400", + item.id === "logout" && + "hover:bg-red-500/10 hover:text-red-400", )} >
@@ -313,9 +319,13 @@ export function Sidebar({ "w-4 h-4 transition-transform duration-200", isExpanded ? "rotate-180" : "rotate-0", // Show different color when child is active (cannot collapse) - item.children && item.children.some(child => - child.href && location.pathname === child.href - ) ? "text-emerald-400" : "text-current" + item.children && + item.children.some( + (child) => + child.href && location.pathname === child.href, + ) + ? "text-emerald-400" + : "text-current", )} /> )} @@ -324,14 +334,12 @@ export function Sidebar({
)} - {/* Submenu */} {hasChildren && isExpanded && !isCollapsed && (
{item.children?.map((child) => renderMenuItem(child, level + 1))}
)} - {/* Tooltip for collapsed state */} {isCollapsed && level === 0 && (
@@ -375,7 +383,12 @@ export function Sidebar({
) : (
- +
)}
diff --git a/app/components/ui/tabs.tsx b/app/components/ui/tabs.tsx index 50fcb46..e9354c2 100644 --- a/app/components/ui/tabs.tsx +++ b/app/components/ui/tabs.tsx @@ -16,17 +16,23 @@ interface TabsProps { children: React.ReactNode; } -export function Tabs({ defaultValue, value, onValueChange, className, children }: TabsProps) { +export function Tabs({ + defaultValue, + value, + onValueChange, + className, + children, +}: TabsProps) { const [internalValue, setInternalValue] = useState(defaultValue || ""); - + const currentValue = value ?? internalValue; const handleValueChange = onValueChange ?? setInternalValue; return ( - -
- {children} -
+ +
{children}
); } @@ -38,7 +44,12 @@ interface TabsListProps { export function TabsList({ className, children }: TabsListProps) { return ( -
+
{children}
); @@ -51,7 +62,12 @@ interface TabsTriggerProps { children: React.ReactNode; } -export function TabsTrigger({ value, className, disabled, children }: TabsTriggerProps) { +export function TabsTrigger({ + value, + className, + disabled, + children, +}: TabsTriggerProps) { const context = useContext(TabsContext); if (!context) throw new Error("TabsTrigger must be used within Tabs"); @@ -64,8 +80,10 @@ export function TabsTrigger({ value, className, disabled, children }: TabsTrigge onClick={() => !disabled && context.onValueChange(value)} className={cn( "inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", - isActive ? "bg-background text-foreground shadow-sm" : "hover:bg-muted/50", - className + isActive + ? "bg-gray-700 text-foreground shadow-sm" + : "hover:bg-muted/50", + className, )} > {children} @@ -86,7 +104,12 @@ export function TabsContent({ value, className, children }: TabsContentProps) { if (context.value !== value) return null; return ( -
+
{children}
);