add the popup and fix the style fomr dasbhard
This commit is contained in:
parent
1dd5ea70f4
commit
4fe9871266
74
app/components/common/popup-bar-chart.tsx
Normal file
74
app/components/common/popup-bar-chart.tsx
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
import {
|
||||||
|
Bar,
|
||||||
|
BarChart,
|
||||||
|
CartesianGrid,
|
||||||
|
XAxis,
|
||||||
|
YAxis,
|
||||||
|
LabelList,
|
||||||
|
ResponsiveContainer,
|
||||||
|
} from "recharts";
|
||||||
|
import { Card, CardContent } from "~/components/ui/card";
|
||||||
|
import {
|
||||||
|
type ChartConfig,
|
||||||
|
ChartContainer,
|
||||||
|
} from "~/components/ui/chart";
|
||||||
|
import { formatNumber } from "~/lib/utils";
|
||||||
|
|
||||||
|
export type PopupChartDatum = {
|
||||||
|
name: string;
|
||||||
|
value: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const chartConfig = {
|
||||||
|
value: {
|
||||||
|
label: "Operational Fee",
|
||||||
|
color: "#4ADE80", // Green-400
|
||||||
|
},
|
||||||
|
} satisfies ChartConfig;
|
||||||
|
|
||||||
|
|
||||||
|
export function PopupBarChart({
|
||||||
|
data,
|
||||||
|
}: {
|
||||||
|
data: PopupChartDatum[];
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Card className="py-0 bg-transparent mt-4 border-none h-full">
|
||||||
|
<CardContent className="px-2 sm:p-6 bg-transparent">
|
||||||
|
<ChartContainer config={chartConfig} className="aspect-auto h-80 w-full">
|
||||||
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
|
<BarChart
|
||||||
|
accessibilityLayer
|
||||||
|
data={data}
|
||||||
|
margin={{ top: 20, right: 20, left: 20, bottom: 5 }}
|
||||||
|
>
|
||||||
|
<CartesianGrid vertical={false} stroke="#475569" />
|
||||||
|
<XAxis
|
||||||
|
dataKey="name"
|
||||||
|
tickLine={false}
|
||||||
|
axisLine={false}
|
||||||
|
tickMargin={8}
|
||||||
|
tick={{ fill: "#94a3b8", fontSize: 12 }}
|
||||||
|
/>
|
||||||
|
<YAxis
|
||||||
|
tickLine={false}
|
||||||
|
axisLine={false}
|
||||||
|
tickMargin={8}
|
||||||
|
tick={{ fill: "#94a3b8", fontSize: 12 }}
|
||||||
|
tickFormatter={(value) => `${formatNumber(Math.round(value))}`}
|
||||||
|
/>
|
||||||
|
<Bar dataKey="value" fill={chartConfig.value.color} radius={[8, 8, 0, 0]}>
|
||||||
|
<LabelList
|
||||||
|
dataKey="value"
|
||||||
|
position="top"
|
||||||
|
style={{ fill: "#ffffff", fontSize: "12px", fontWeight: "bold" }}
|
||||||
|
formatter={(v: number) => `${formatNumber(Math.round(v))}`}
|
||||||
|
/>
|
||||||
|
</Bar>
|
||||||
|
</BarChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</ChartContainer>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -5,13 +5,17 @@ export type CompanyInfo = {
|
||||||
id: string;
|
id: string;
|
||||||
imageUrl: string;
|
imageUrl: string;
|
||||||
name: string;
|
name: string;
|
||||||
costReduction: number; // absolute value
|
costReduction: number;
|
||||||
revenue?: number;
|
revenue?: number;
|
||||||
capacity?: number;
|
capacity?: number;
|
||||||
|
costI : number,
|
||||||
|
capacityI : number,
|
||||||
|
revenueI : number,
|
||||||
|
cost : number | string,
|
||||||
};
|
};
|
||||||
|
|
||||||
export type D3ImageInfoProps = {
|
export type D3ImageInfoProps = {
|
||||||
companies: CompanyInfo[]; // exactly 6 items
|
companies: CompanyInfo[];
|
||||||
width?: number;
|
width?: number;
|
||||||
height?: number;
|
height?: number;
|
||||||
};
|
};
|
||||||
|
|
@ -59,7 +63,7 @@ export function D3ImageInfo({ companies }: D3ImageInfoProps) {
|
||||||
<div className="w-full h-[500px] rounded-xl p-4">
|
<div className="w-full h-[500px] rounded-xl p-4">
|
||||||
<div dir="ltr" className="company-grid-container">
|
<div dir="ltr" className="company-grid-container">
|
||||||
{displayCompanies.map((company, index) => {
|
{displayCompanies.map((company, index) => {
|
||||||
const gp = gridPositions.find(v => v.name === company.name) || { col: (index % 5) + 1, row: Math.floor(index / 5) + 1 };
|
const gp = gridPositions.find(v => v.name === company.name) ;
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
|
|
@ -77,7 +81,7 @@ export function D3ImageInfo({ companies }: D3ImageInfoProps) {
|
||||||
|
|
||||||
{company.name}
|
{company.name}
|
||||||
</div>
|
</div>
|
||||||
<InfoBox company={company} key={index +10} style={{ gridColumn: gp.colI , gridRow: gp.rowI }} />
|
<InfoBox company={company} key={index +10} style={{ gridColumn: gp?.colI , gridRow: gp?.rowI }} />
|
||||||
</>);
|
</>);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,9 @@ export function DashboardHome() {
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
// Chart and schematic data from select API
|
// Chart and schematic data from select API
|
||||||
const [companyChartData, setCompanyChartData] = useState<
|
const [companyChartData, setCompanyChartData] = useState<
|
||||||
{ category: string; capacity: number; revenue: number; cost: number }[]
|
{ category: string; capacity: number; revenue: number; cost: number , costI : number,
|
||||||
|
capacityI : number,
|
||||||
|
revenueI : number }[]
|
||||||
>([]);
|
>([]);
|
||||||
const [totalIncreasedCapacity, setTotalIncreasedCapacity] = useState<number>(0);
|
const [totalIncreasedCapacity, setTotalIncreasedCapacity] = useState<number>(0);
|
||||||
|
|
||||||
|
|
@ -138,7 +140,6 @@ export function DashboardHome() {
|
||||||
const capacityPct = preCap > 0 ? (incCap / preCap) * 100 : 0;
|
const capacityPct = preCap > 0 ? (incCap / preCap) * 100 : 0;
|
||||||
const revenuePct = preInc > 0 ? (incInc / preInc) * 100 : 0;
|
const revenuePct = preInc > 0 ? (incInc / preInc) * 100 : 0;
|
||||||
const costPct = preFee > 0 ? (costRed / preFee) * 100 : 0;
|
const costPct = preFee > 0 ? (costRed / preFee) * 100 : 0;
|
||||||
console.log(costRed)
|
|
||||||
return {
|
return {
|
||||||
category: rel,
|
category: rel,
|
||||||
capacity: isFinite(capacityPct) ? capacityPct : 0,
|
capacity: isFinite(capacityPct) ? capacityPct : 0,
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import { useState } from "react";
|
||||||
import { cn } from "~/lib/utils";
|
import { cn } from "~/lib/utils";
|
||||||
import { Sidebar } from "./sidebar";
|
import { Sidebar } from "./sidebar";
|
||||||
import { Header } from "./header";
|
import { Header } from "./header";
|
||||||
|
import { StrategicAlignmentPopup } from "./strategic-alignment-popup";
|
||||||
|
|
||||||
interface DashboardLayoutProps {
|
interface DashboardLayoutProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
|
|
@ -16,6 +17,7 @@ export function DashboardLayout({
|
||||||
}: DashboardLayoutProps) {
|
}: DashboardLayoutProps) {
|
||||||
const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false);
|
const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false);
|
||||||
const [isMobileSidebarOpen, setIsMobileSidebarOpen] = useState(false);
|
const [isMobileSidebarOpen, setIsMobileSidebarOpen] = useState(false);
|
||||||
|
const [isStrategicAlignmentPopupOpen, setIsStrategicAlignmentPopupOpen] = useState(false);
|
||||||
|
|
||||||
const toggleSidebarCollapse = () => {
|
const toggleSidebarCollapse = () => {
|
||||||
setIsSidebarCollapsed(!isSidebarCollapsed);
|
setIsSidebarCollapsed(!isSidebarCollapsed);
|
||||||
|
|
@ -55,6 +57,7 @@ export function DashboardLayout({
|
||||||
isCollapsed={isSidebarCollapsed}
|
isCollapsed={isSidebarCollapsed}
|
||||||
onToggleCollapse={toggleSidebarCollapse}
|
onToggleCollapse={toggleSidebarCollapse}
|
||||||
className="h-full flex-shrink-0 relative z-10"
|
className="h-full flex-shrink-0 relative z-10"
|
||||||
|
onStrategicAlignmentClick={() => setIsStrategicAlignmentPopupOpen(true)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -79,6 +82,7 @@ export function DashboardLayout({
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
<StrategicAlignmentPopup open={isStrategicAlignmentPopupOpen} onOpenChange={setIsStrategicAlignmentPopupOpen} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,7 @@ interface SidebarProps {
|
||||||
isCollapsed?: boolean;
|
isCollapsed?: boolean;
|
||||||
onToggleCollapse?: () => void;
|
onToggleCollapse?: () => void;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
onStrategicAlignmentClick?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface MenuItem {
|
interface MenuItem {
|
||||||
|
|
@ -52,6 +53,7 @@ interface MenuItem {
|
||||||
}
|
}
|
||||||
|
|
||||||
const menuItems: MenuItem[] = [
|
const menuItems: MenuItem[] = [
|
||||||
|
|
||||||
{
|
{
|
||||||
id: "dashboard",
|
id: "dashboard",
|
||||||
label: "صفحه اصلی",
|
label: "صفحه اصلی",
|
||||||
|
|
@ -119,6 +121,12 @@ const menuItems: MenuItem[] = [
|
||||||
icon: Star,
|
icon: Star,
|
||||||
href: "/dashboard/top-innovations",
|
href: "/dashboard/top-innovations",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: "strategic-alignment",
|
||||||
|
label: "میزان انطباق راهبردی",
|
||||||
|
icon: BarChart3,
|
||||||
|
href: "#", // This is not a route, it opens a popup
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const bottomMenuItems: MenuItem[] = [
|
const bottomMenuItems: MenuItem[] = [
|
||||||
|
|
@ -140,6 +148,7 @@ export function Sidebar({
|
||||||
isCollapsed = false,
|
isCollapsed = false,
|
||||||
onToggleCollapse,
|
onToggleCollapse,
|
||||||
className,
|
className,
|
||||||
|
onStrategicAlignmentClick,
|
||||||
}: SidebarProps) {
|
}: SidebarProps) {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const [expandedItems, setExpandedItems] = useState<string[]>([]);
|
const [expandedItems, setExpandedItems] = useState<string[]>([]);
|
||||||
|
|
@ -211,16 +220,42 @@ export function Sidebar({
|
||||||
const ItemIcon = item.icon;
|
const ItemIcon = item.icon;
|
||||||
|
|
||||||
const handleClick = () => {
|
const handleClick = () => {
|
||||||
if (item.id === "logout") {
|
if (item.id === "strategic-alignment") {
|
||||||
|
onStrategicAlignmentClick?.();
|
||||||
|
} else if (item.id === "logout") {
|
||||||
logout();
|
logout();
|
||||||
} else if (hasChildren) {
|
} else if (hasChildren) {
|
||||||
toggleExpanded(item.id);
|
toggleExpanded(item.id);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (item.id === "strategic-alignment") {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
className={cn(
|
||||||
|
"w-full text-right",
|
||||||
|
)}
|
||||||
|
onClick={handleClick}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex items-center justify-center w-full px-2 rounded-lg mt-4 transition-all duration-200 group",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="flex justify-center rounded-xl border-gray-500/20 border-2 cursor-pointer transition-all hover:bg-[#3F415A]/50 bg-[#3F415A] py-2 text-center items-center gap-3 min-w-0 flex-1">
|
||||||
|
<span className="font-persian text-sm font-medium truncate">
|
||||||
|
{item.label}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={item.id} className="relative">
|
<div key={item.id} className="relative">
|
||||||
{item.href && item.id !== "logout" ? (
|
{item.href && item.href !== "#" ? (
|
||||||
<Link to={item.href} className="block">
|
<Link to={item.href} className="block">
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
|
|
|
||||||
203
app/components/dashboard/strategic-alignment-popup.tsx
Normal file
203
app/components/dashboard/strategic-alignment-popup.tsx
Normal file
|
|
@ -0,0 +1,203 @@
|
||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "~/components/ui/dialog";
|
||||||
|
import {
|
||||||
|
BarChart,
|
||||||
|
Bar,
|
||||||
|
XAxis,
|
||||||
|
YAxis,
|
||||||
|
CartesianGrid,
|
||||||
|
Tooltip,
|
||||||
|
ResponsiveContainer,
|
||||||
|
LabelList,
|
||||||
|
Cell,
|
||||||
|
} from "recharts";
|
||||||
|
import apiService from "~/lib/api";
|
||||||
|
import { Skeleton } from "~/components/ui/skeleton";
|
||||||
|
import { formatNumber } from "~/lib/utils";
|
||||||
|
import { ChartContainer } from "../ui/chart";
|
||||||
|
|
||||||
|
interface StrategicAlignmentData {
|
||||||
|
strategic_theme: string;
|
||||||
|
operational_fee_sum: number;
|
||||||
|
percentage?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface StrategicAlignmentPopupProps {
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ Chart config for shadcn/ui
|
||||||
|
const chartConfig = {
|
||||||
|
percentage: {
|
||||||
|
label: "",
|
||||||
|
color: "#3AEA83",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const maxHeight = 150;
|
||||||
|
const barHeights = () => Math.floor(Math.random() * maxHeight);
|
||||||
|
|
||||||
|
const ChartSkeleton = () => (
|
||||||
|
|
||||||
|
<div className="flex justify-center h-96 w-full p-4">
|
||||||
|
{/* Chart bars */}
|
||||||
|
<div className=" w-full flex items-end gap-10">
|
||||||
|
{[...Array(9)].map((_, i) => (
|
||||||
|
<div key={i} className="flex flex-col items-center gap-1">
|
||||||
|
<Skeleton
|
||||||
|
className="w-10 bg-gray-700 rounded-md"
|
||||||
|
style={{ height: `${barHeights()}px` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{/* Left space for Y-axis label */}
|
||||||
|
<div className="flex flex-col justify-between mr-2">
|
||||||
|
<Skeleton className="h-6 w-15 bg-gray-700 rounded" />
|
||||||
|
<Skeleton className="h-6 w-15 bg-gray-700 rounded" />
|
||||||
|
<Skeleton className="h-6 w-15 bg-gray-700 rounded" />
|
||||||
|
<Skeleton className="h-6 w-15 bg-gray-700 rounded" />
|
||||||
|
<Skeleton className="h-6 w-15 bg-gray-700 rounded" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
export function StrategicAlignmentPopup({
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
}: StrategicAlignmentPopupProps) {
|
||||||
|
const [data, setData] = useState<StrategicAlignmentData[]>([]);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (open) {
|
||||||
|
fetchData();
|
||||||
|
}
|
||||||
|
}, [open]);
|
||||||
|
|
||||||
|
const fetchData = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const response = await apiService.select({
|
||||||
|
ProcessName: "project",
|
||||||
|
OutputFields: [
|
||||||
|
"strategic_theme",
|
||||||
|
"sum(operational_fee) as operational_fee_sum",
|
||||||
|
],
|
||||||
|
GroupBy: ["strategic_theme"],
|
||||||
|
});
|
||||||
|
|
||||||
|
const responseData =
|
||||||
|
typeof response.data === "string"
|
||||||
|
? JSON.parse(response.data)
|
||||||
|
: response.data;
|
||||||
|
|
||||||
|
const processedData = responseData
|
||||||
|
.map((item: any) => ({
|
||||||
|
strategic_theme: item.strategic_theme || "N/A",
|
||||||
|
operational_fee_sum: Math.max(0, Number(item.operational_fee_sum)),
|
||||||
|
}))
|
||||||
|
.filter((item: StrategicAlignmentData) => item.strategic_theme !== "");
|
||||||
|
|
||||||
|
const total = processedData.reduce(
|
||||||
|
(acc: number, item: StrategicAlignmentData) =>
|
||||||
|
acc + item.operational_fee_sum,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
|
||||||
|
const dataWithPercentage = processedData.map(
|
||||||
|
(item: StrategicAlignmentData) => ({
|
||||||
|
...item,
|
||||||
|
percentage:
|
||||||
|
total > 0
|
||||||
|
? Math.round((item.operational_fee_sum / total) * 100)
|
||||||
|
: 0,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
setData(dataWithPercentage || []);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching strategic alignment data:", error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
|
<DialogContent className="w-full max-w-4xl bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] text-white border-none">
|
||||||
|
<DialogHeader className="border-b-3 mb-10 py-2 w-full pb-4 border-b-2 border-gray-500/20">
|
||||||
|
<DialogTitle className="ml-auto ">میزان انطباق راهبردی</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
{loading ? (
|
||||||
|
<ChartSkeleton />
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<ResponsiveContainer width="100%" height={400}>
|
||||||
|
<ChartContainer config={chartConfig} className="aspect-auto h-96 w-full">
|
||||||
|
<BarChart
|
||||||
|
data={data}
|
||||||
|
margin={{ left: 12, right: 12 }}
|
||||||
|
barGap={15}
|
||||||
|
barSize={30}
|
||||||
|
accessibilityLayer
|
||||||
|
>
|
||||||
|
<CartesianGrid vertical={false} stroke="#475569" />
|
||||||
|
<XAxis
|
||||||
|
dataKey="strategic_theme"
|
||||||
|
tickLine={false}
|
||||||
|
axisLine={false}
|
||||||
|
tickMargin={10}
|
||||||
|
tick={{ fill: "#94a3b8", fontSize: 12 }}
|
||||||
|
/>
|
||||||
|
<YAxis
|
||||||
|
domain={[0, 100]}
|
||||||
|
tickLine={false}
|
||||||
|
axisLine={false}
|
||||||
|
tickMargin={8}
|
||||||
|
tick={{ fill: "#94a3b8", fontSize: 12 }}
|
||||||
|
tickFormatter={(value) =>
|
||||||
|
`${formatNumber(Math.round(value))}%`
|
||||||
|
}
|
||||||
|
label={{
|
||||||
|
value: "تعداد برنامه ها" ,
|
||||||
|
angle: -90,
|
||||||
|
position: "insideLeft",
|
||||||
|
fill: "#94a3b8",
|
||||||
|
fontSize: 14,
|
||||||
|
offset: 0,
|
||||||
|
dy: 0,
|
||||||
|
style: { textAnchor: "middle" },
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Bar dataKey="percentage" radius={[8, 8, 0, 0]}>
|
||||||
|
{data.map((entry, index) => (
|
||||||
|
<Cell key={`cell-${index}`} fill={chartConfig.percentage.color} />
|
||||||
|
))}
|
||||||
|
<LabelList
|
||||||
|
dataKey="percentage"
|
||||||
|
position="top"
|
||||||
|
style={{
|
||||||
|
fill: "#ffffff",
|
||||||
|
fontSize: "12px",
|
||||||
|
fontWeight: "bold",
|
||||||
|
}}
|
||||||
|
formatter={(v: number) => `${formatNumber(Math.round(v))}%`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
</Bar>
|
||||||
|
</BarChart>
|
||||||
|
</ChartContainer>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
13
app/components/ui/skeleton.tsx
Normal file
13
app/components/ui/skeleton.tsx
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
import { cn } from "~/lib/utils"
|
||||||
|
|
||||||
|
function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="skeleton"
|
||||||
|
className={cn("bg-accent animate-pulse rounded-md", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Skeleton }
|
||||||
61
app/components/ui/tooltip.tsx
Normal file
61
app/components/ui/tooltip.tsx
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
|
||||||
|
|
||||||
|
import { cn } from "~/lib/utils"
|
||||||
|
|
||||||
|
function TooltipProvider({
|
||||||
|
delayDuration = 0,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
|
||||||
|
return (
|
||||||
|
<TooltipPrimitive.Provider
|
||||||
|
data-slot="tooltip-provider"
|
||||||
|
delayDuration={delayDuration}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Tooltip({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof TooltipPrimitive.Root>) {
|
||||||
|
return (
|
||||||
|
<TooltipProvider>
|
||||||
|
<TooltipPrimitive.Root data-slot="tooltip" {...props} />
|
||||||
|
</TooltipProvider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function TooltipTrigger({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
|
||||||
|
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function TooltipContent({
|
||||||
|
className,
|
||||||
|
sideOffset = 0,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
|
||||||
|
return (
|
||||||
|
<TooltipPrimitive.Portal>
|
||||||
|
<TooltipPrimitive.Content
|
||||||
|
data-slot="tooltip-content"
|
||||||
|
sideOffset={sideOffset}
|
||||||
|
className={cn(
|
||||||
|
"bg-primary text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<TooltipPrimitive.Arrow className="bg-primary fill-primary z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
|
||||||
|
</TooltipPrimitive.Content>
|
||||||
|
</TooltipPrimitive.Portal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
|
||||||
80
app/root.tsx
80
app/root.tsx
|
|
@ -41,51 +41,51 @@ export function Layout({ children }: { children: React.ReactNode }) {
|
||||||
</head>
|
</head>
|
||||||
<body className="font-persian bg-gray-900 text-white">
|
<body className="font-persian bg-gray-900 text-white">
|
||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
<GlobalRouteGuard>{children}</GlobalRouteGuard>
|
<GlobalRouteGuard>{children}</GlobalRouteGuard>
|
||||||
<Toaster
|
<Toaster
|
||||||
position="top-center"
|
position="top-center"
|
||||||
reverseOrder={false}
|
reverseOrder={false}
|
||||||
gutter={8}
|
gutter={8}
|
||||||
containerClassName=""
|
containerClassName=""
|
||||||
containerStyle={{}}
|
containerStyle={{}}
|
||||||
toastOptions={{
|
toastOptions={{
|
||||||
// Define default options
|
// Define default options
|
||||||
className: "",
|
className: "",
|
||||||
duration: 4000,
|
|
||||||
style: {
|
|
||||||
background: "rgba(31, 41, 55, 0.95)",
|
|
||||||
color: "#fff",
|
|
||||||
fontFamily:
|
|
||||||
"Vazirmatn, Inter, ui-sans-serif, system-ui, sans-serif",
|
|
||||||
direction: "rtl",
|
|
||||||
textAlign: "right",
|
|
||||||
border: "1px solid rgba(16, 185, 129, 0.3)",
|
|
||||||
},
|
|
||||||
// Default options for specific types
|
|
||||||
success: {
|
|
||||||
duration: 3000,
|
|
||||||
style: {
|
|
||||||
background: "rgba(16, 185, 129, 0.9)",
|
|
||||||
color: "#fff",
|
|
||||||
},
|
|
||||||
iconTheme: {
|
|
||||||
primary: "#fff",
|
|
||||||
secondary: "#10b981",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
error: {
|
|
||||||
duration: 4000,
|
duration: 4000,
|
||||||
style: {
|
style: {
|
||||||
background: "rgba(239, 68, 68, 0.9)",
|
background: "rgba(31, 41, 55, 0.95)",
|
||||||
color: "#fff",
|
color: "#fff",
|
||||||
|
fontFamily:
|
||||||
|
"Vazirmatn, Inter, ui-sans-serif, system-ui, sans-serif",
|
||||||
|
direction: "rtl",
|
||||||
|
textAlign: "right",
|
||||||
|
border: "1px solid rgba(16, 185, 129, 0.3)",
|
||||||
},
|
},
|
||||||
iconTheme: {
|
// Default options for specific types
|
||||||
primary: "#fff",
|
success: {
|
||||||
secondary: "#ef4444",
|
duration: 3000,
|
||||||
|
style: {
|
||||||
|
background: "rgba(16, 185, 129, 0.9)",
|
||||||
|
color: "#fff",
|
||||||
|
},
|
||||||
|
iconTheme: {
|
||||||
|
primary: "#fff",
|
||||||
|
secondary: "#10b981",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
error: {
|
||||||
}}
|
duration: 4000,
|
||||||
/>
|
style: {
|
||||||
|
background: "rgba(239, 68, 68, 0.9)",
|
||||||
|
color: "#fff",
|
||||||
|
},
|
||||||
|
iconTheme: {
|
||||||
|
primary: "#fff",
|
||||||
|
secondary: "#ef4444",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
<ScrollRestoration />
|
<ScrollRestoration />
|
||||||
<Scripts />
|
<Scripts />
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@
|
||||||
"@radix-ui/react-select": "^2.2.5",
|
"@radix-ui/react-select": "^2.2.5",
|
||||||
"@radix-ui/react-slot": "^1.0.2",
|
"@radix-ui/react-slot": "^1.0.2",
|
||||||
"@radix-ui/react-tabs": "^1.1.13",
|
"@radix-ui/react-tabs": "^1.1.13",
|
||||||
|
"@radix-ui/react-tooltip": "^1.2.8",
|
||||||
"@react-router/node": "^7.7.0",
|
"@react-router/node": "^7.7.0",
|
||||||
"@react-router/serve": "^7.7.1",
|
"@react-router/serve": "^7.7.1",
|
||||||
"@types/d3": "^7.4.3",
|
"@types/d3": "^7.4.3",
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,9 @@ importers:
|
||||||
'@radix-ui/react-tabs':
|
'@radix-ui/react-tabs':
|
||||||
specifier: ^1.1.13
|
specifier: ^1.1.13
|
||||||
version: 1.1.13(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
|
version: 1.1.13(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
|
||||||
|
'@radix-ui/react-tooltip':
|
||||||
|
specifier: ^1.2.8
|
||||||
|
version: 1.2.8(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
|
||||||
'@react-router/node':
|
'@react-router/node':
|
||||||
specifier: ^7.7.0
|
specifier: ^7.7.0
|
||||||
version: 7.8.2(react-router@7.8.2(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(typescript@5.9.2)
|
version: 7.8.2(react-router@7.8.2(react-dom@19.1.1(react@19.1.1))(react@19.1.1))(typescript@5.9.2)
|
||||||
|
|
@ -755,6 +758,19 @@ packages:
|
||||||
'@types/react-dom':
|
'@types/react-dom':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@radix-ui/react-tooltip@1.2.8':
|
||||||
|
resolution: {integrity: sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': '*'
|
||||||
|
'@types/react-dom': '*'
|
||||||
|
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
'@types/react-dom':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@radix-ui/react-use-callback-ref@1.1.1':
|
'@radix-ui/react-use-callback-ref@1.1.1':
|
||||||
resolution: {integrity: sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==}
|
resolution: {integrity: sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
|
|
@ -3119,6 +3135,26 @@ snapshots:
|
||||||
'@types/react': 19.1.12
|
'@types/react': 19.1.12
|
||||||
'@types/react-dom': 19.1.9(@types/react@19.1.12)
|
'@types/react-dom': 19.1.9(@types/react@19.1.12)
|
||||||
|
|
||||||
|
'@radix-ui/react-tooltip@1.2.8(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)':
|
||||||
|
dependencies:
|
||||||
|
'@radix-ui/primitive': 1.1.3
|
||||||
|
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.12)(react@19.1.1)
|
||||||
|
'@radix-ui/react-context': 1.1.2(@types/react@19.1.12)(react@19.1.1)
|
||||||
|
'@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
|
||||||
|
'@radix-ui/react-id': 1.1.1(@types/react@19.1.12)(react@19.1.1)
|
||||||
|
'@radix-ui/react-popper': 1.2.8(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
|
||||||
|
'@radix-ui/react-portal': 1.1.9(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
|
||||||
|
'@radix-ui/react-presence': 1.1.5(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
|
||||||
|
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
|
||||||
|
'@radix-ui/react-slot': 1.2.3(@types/react@19.1.12)(react@19.1.1)
|
||||||
|
'@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.12)(react@19.1.1)
|
||||||
|
'@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.1.9(@types/react@19.1.12))(@types/react@19.1.12)(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
|
||||||
|
react: 19.1.1
|
||||||
|
react-dom: 19.1.1(react@19.1.1)
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/react': 19.1.12
|
||||||
|
'@types/react-dom': 19.1.9(@types/react@19.1.12)
|
||||||
|
|
||||||
'@radix-ui/react-use-callback-ref@1.1.1(@types/react@19.1.12)(react@19.1.1)':
|
'@radix-ui/react-use-callback-ref@1.1.1(@types/react@19.1.12)(react@19.1.1)':
|
||||||
dependencies:
|
dependencies:
|
||||||
react: 19.1.1
|
react: 19.1.1
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user