inogen/app/components/ecosystem/info-panel.tsx

535 lines
17 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import React, { useEffect, useState } from "react";
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
import {
Area,
AreaChart,
CartesianGrid,
ResponsiveContainer,
Tooltip,
XAxis,
YAxis,
} from "recharts";
import { CustomBarChart } from "~/components/ui/custom-bar-chart";
import apiService from "~/lib/api";
import { formatNumber } from "~/lib/utils";
export interface CompanyDetails {
id: string;
label: string;
category: string;
stageid: number;
fields: {
F: string;
N: string;
V: string;
T: number;
U: string;
S: boolean;
}[];
description?: string;
}
export interface InfoPanelProps {
selectedCompany: CompanyDetails | null;
}
interface EcosystemCounts {
knowledge_based_count: string;
consultant_count: string;
startup_count: string;
innovation_center_count: string;
accelerator_count: string;
university_count: string;
fund_count: string;
company_count: string;
actor_count: string;
mou_count: string;
}
interface ProcessActorsResponse {
start_year: string;
total_count: number;
}
interface ProcessActorsData {
year: string;
value: number;
}
export function InfoPanel({ selectedCompany }: InfoPanelProps) {
const [counts, setCounts] = useState<EcosystemCounts | null>(null);
const [processData, setProcessData] = useState<ProcessActorsData[]>([]);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
const fetchCounts = async () => {
setIsLoading(true);
try {
const [countsRes, processRes] = await Promise.all([
apiService.call<EcosystemCounts>({
ecosystem_count_function: {},
}),
apiService.call<ProcessActorsResponse[]>({
process_creating_actors_function: {},
}),
]);
setCounts(
JSON.parse(JSON.parse(countsRes.data).ecosystem_count_function)[0],
);
// Process the years data and fill missing years
const processedData = processYearsData(
JSON.parse(JSON.parse(processRes?.data)?.process_creating_actors),
);
setProcessData(processedData);
} catch (err) {
console.error("Failed to fetch data:", err);
} finally {
setIsLoading(false);
}
};
fetchCounts();
}, []);
// Helper function to safely parse numbers
const parseNumber = (value: string | undefined): number => {
if (!value || value === "") return 0;
const parsed = parseInt(value, 10);
return isNaN(parsed) ? 0 : parsed;
};
// Helper function to process years data and fill missing years
const processYearsData = (
data: ProcessActorsResponse[],
): ProcessActorsData[] => {
if (!data || data.length === 0) return [];
const years = data
.map((item) => parseInt(item.start_year))
.sort((a, b) => a - b);
const minYear = years[0];
const maxYear = years[years.length - 1];
const result: ProcessActorsData[] = [];
// Create a map for quick lookup
const dataMap = data.reduce(
(acc, item) => {
acc[item.start_year] = item.total_count;
return acc;
},
{} as Record<string, number>,
);
for (let year = minYear; year <= maxYear; year++) {
result.push({
year: year.toString(),
value: dataMap[year.toString()] || 0,
});
}
return result;
};
// Convert Persian years to display format without commas
const formatPersianYear = (year: string): string => {
const map: Record<string, string> = {
"0": "۰",
"1": "۱",
"2": "۲",
"3": "۳",
"4": "۴",
"5": "۵",
"6": "۶",
"7": "۷",
"8": "۸",
"9": "۹",
};
return year.replace(/[0-9]/g, (d) => map[d] ?? d);
};
// Transform counts into chart-friendly data
const barData = counts
? [
{
label: "دانش بنیان",
value: parseNumber(counts.knowledge_based_count),
},
{ label: "مشاور", value: parseNumber(counts.consultant_count) },
{ label: "استارتاپ", value: parseNumber(counts.startup_count) },
{
label: "مرکز نوآوری",
value: parseNumber(counts.innovation_center_count),
},
{ label: "شتابدهنده", value: parseNumber(counts.accelerator_count) },
{ label: "دانشگاه", value: parseNumber(counts.university_count) },
{ label: "صندوق های مالی", value: parseNumber(counts.fund_count) },
{ label: "شرکت", value: parseNumber(counts.company_count) },
]
: [];
if (isLoading) {
return (
<Card className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] min-h-full flex flex-col">
{/* Header Skeleton */}
<CardHeader className="text-center pb-2">
<div className="w-48 h-6 bg-gray-600 rounded animate-pulse mx-auto mb-4"></div>
</CardHeader>
{/* Actor Count Skeleton */}
<CardHeader className="text-center pt-0 pb-4">
<div className="w-full h-5 bg-gray-600 rounded animate-pulse mx-auto mb-2"></div>
</CardHeader>
{/* Bar Chart Skeleton */}
<CardContent className="flex-1 px-6">
<div className="w-full space-y-4">
{Array.from({ length: 8 }).map((_, i) => (
<div
key={i}
className="flex items-center gap-3"
style={{ animationDelay: `${i * 100}ms` }}
>
{/* Label skeleton */}
<div className="w-24 h-4 bg-gray-600 rounded animate-pulse"></div>
{/* Bar skeleton */}
<div className="flex-1 bg-gray-700 rounded-full h-6">
<div
className="h-6 bg-gray-500 rounded-full animate-pulse"
style={{ width: `${Math.random() * 60 + 20}%` }}
></div>
</div>
{/* Value skeleton */}
<div className="w-8 h-4 bg-gray-600 rounded animate-pulse"></div>
</div>
))}
</div>
</CardContent>
{/* Area Chart Skeleton */}
<CardContent className="px-6 pb-4">
<div className="mb-4">
<div className="w-40 h-5 bg-gray-600 rounded animate-pulse mb-4"></div>
</div>
<div className="h-64 bg-gray-700 rounded-lg relative overflow-hidden">
{/* Chart skeleton */}
<div className="absolute inset-4">
{/* Y-axis skeleton */}
<div className="absolute left-0 top-0 bottom-8 w-px bg-gray-600"></div>
{/* X-axis skeleton */}
<div className="absolute left-0 bottom-8 right-0 h-px bg-gray-600"></div>
{/* Area chart skeleton */}
<svg className="w-full h-full">
<defs>
<linearGradient
id="areaGradient"
x1="0%"
y1="0%"
x2="0%"
y2="100%"
>
<stop offset="0%" stopColor="#34D399" stopOpacity="0.3" />
<stop offset="100%" stopColor="#34D399" stopOpacity="0.1" />
</linearGradient>
</defs>
<path
d="M 20 150 Q 60 100 100 120 T 180 80 T 260 90 T 340 60"
stroke="#34D399"
strokeWidth="2"
fill="none"
className="animate-pulse"
/>
<path
d="M 20 150 Q 60 100 100 120 T 180 80 T 260 90 T 340 60 L 340 180 L 20 180 Z"
fill="url(#areaGradient)"
className="animate-pulse"
/>
</svg>
{/* Data points skeleton */}
{Array.from({ length: 4 }).map((_, i) => (
<div
key={i}
className="absolute w-2 h-2 bg-green-400 rounded-full animate-pulse"
style={{
left: `${20 + i * 25}%`,
top: `${30 + Math.random() * 40}%`,
animationDelay: `${i * 200}ms`,
}}
></div>
))}
</div>
</div>
</CardContent>
{/* Footer Skeleton */}
<CardContent className="pt-0 pb-6">
<div className="bg-[rgba(255,255,255,0.1)] rounded-lg p-4 text-center"></div>
</CardContent>
</Card>
);
}
if (isLoading) {
return (
<Card className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] min-h-full flex flex-col">
{/* Header Skeleton */}
<CardHeader className="text-center pb-2">
<div className="w-48 h-6 rounded animate-pulse mx-auto mb-4"></div>
</CardHeader>
{/* Actor Count Skeleton */}
<CardHeader className="text-center pt-0 pb-4">
<div className="w-36 h-5 rounded animate-pulse mx-auto mb-2"></div>
<div className="w-16 h-8 bg-green-400 bg-opacity-30 rounded animate-pulse mx-auto"></div>
</CardHeader>
{/* Bar Chart Skeleton */}
<CardContent className="flex-1 px-6">
<div className="w-full space-y-4">
{Array.from({ length: 8 }).map((_, i) => (
<div
key={i}
className="flex items-center gap-3"
style={{ animationDelay: `${i * 100}ms` }}
>
{/* Label skeleton */}
<div className="w-24 h-4 bg-gray-600 rounded animate-pulse"></div>
{/* Bar skeleton */}
<div className="flex-1 bg-gray-700 rounded-full h-6">
<div
className="h-6 bg-gray-500 rounded-full animate-pulse"
style={{ width: `${Math.random() * 60 + 20}%` }}
></div>
</div>
{/* Value skeleton */}
<div className="w-8 h-4 bg-gray-600 rounded animate-pulse"></div>
</div>
))}
</div>
</CardContent>
{/* Area Chart Skeleton */}
<CardContent className="px-6 pb-4">
<div className="mb-4">
<div className="w-40 h-5 bg-gray-600 rounded animate-pulse mb-4"></div>
</div>
<div className="h-64 bg-gray-700 rounded-lg relative overflow-hidden">
{/* Chart skeleton */}
<div className="absolute inset-4">
{/* Y-axis skeleton */}
<div className="absolute left-0 top-0 bottom-8 w-px bg-gray-600"></div>
{/* X-axis skeleton */}
<div className="absolute left-0 bottom-8 right-0 h-px bg-gray-600"></div>
{/* Area chart skeleton */}
<svg className="w-full h-full">
<defs>
<linearGradient
id="areaGradient"
x1="0%"
y1="0%"
x2="0%"
y2="100%"
>
<stop offset="0%" stopColor="#34D399" stopOpacity="0.3" />
<stop offset="100%" stopColor="#34D399" stopOpacity="0.1" />
</linearGradient>
</defs>
<path
d="M 20 150 Q 60 100 100 120 T 180 80 T 260 90 T 340 60"
stroke="#34D399"
strokeWidth="2"
fill="none"
className="animate-pulse"
/>
<path
d="M 20 150 Q 60 100 100 120 T 180 80 T 260 90 T 340 60 L 340 180 L 20 180 Z"
fill="url(#areaGradient)"
className="animate-pulse"
/>
</svg>
{/* Data points skeleton */}
{Array.from({ length: 4 }).map((_, i) => (
<div
key={i}
className="absolute w-2 h-2 bg-green-400 rounded-full animate-pulse"
style={{
left: `${20 + i * 25}%`,
top: `${30 + Math.random() * 40}%`,
animationDelay: `${i * 200}ms`,
}}
></div>
))}
</div>
</div>
</CardContent>
{/* Footer Skeleton */}
<CardContent className="pt-0 pb-6">
<div className="bg-[rgba(255,255,255,0.1)] rounded-lg p-4 text-center">
<div className="w-28 h-4 bg-gray-600 rounded animate-pulse mx-auto mb-1"></div>
<div className="w-12 h-6 bg-green-400 bg-opacity-30 rounded animate-pulse mx-auto"></div>
</div>
</CardContent>
</Card>
);
}
if (!counts) {
return (
<Card className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)] min-h-full">
<CardContent className="flex items-center justify-center h-64">
<div className="text-sm text-gray-300 font-persian">
خطا در بارگذاری دادهها.
</div>
</CardContent>
</Card>
);
}
return (
<div className="space-y-4">
<Card className="bg-[linear-gradient(to_bottom_left,#464861,50%,#111628)]">
<CardHeader className="text-center pt-4 pb-3 border-b-2 border-[#3F415A]">
<CardTitle className="font-persian text-base font-semibold text-white">
وضعیت زیستبوم فناوری و نوآوری
</CardTitle>
</CardHeader>
<CardHeader className="text-center pb-2 border-b-2 border-[#3F415A]">
<CardTitle className="font-persian text-sm text-white flex justify-between px-4">
تعداد تفاهم نامه ها
<span className="font-bold text-3xl">
{formatNumber(counts.mou_count)}
</span>
</CardTitle>
</CardHeader>
<CardHeader className="text-center pb-2 border-b-2 border-[#3F415A]">
<CardTitle className="font-persian text-sm text-white flex justify-between px-4">
تعداد بازیگران
<span className="font-bold text-3xl">
{formatNumber(counts.actor_count)}
</span>
</CardTitle>
</CardHeader>
{/* Actor Count Display */}
<CardHeader className="text-right pt-4 mt-2 pb-2 text-sm font-semibold w-full">
تنوع بازیگران
</CardHeader>
{/* Middle - Bar Chart */}
<CardContent className="flex-1 px-6 border-b-2 border-[#3F415A]">
<div className="w-full">
<CustomBarChart
data={barData.map((item) => ({
label: item.label,
value: item.value,
valueSuffix: "",
valuePrefix: "",
maxValue: Math.max(...barData.map((d) => d.value)),
}))}
barHeight="h-5"
showAxisLabels={false}
/>
</div>
</CardContent>
{/* Area Chart Section */}
<CardContent className="p-2">
<div className="px-4">
<CardTitle className="font-persian text-sm font-semibold text-white mb-2">
روند ایجاد بازیگران در طول سالها
</CardTitle>
</div>
<div className="h-42">
{processData.length > 0 ? (
<ResponsiveContainer width="100%" height="100%">
<AreaChart
accessibilityLayer
data={processData}
margin={{ top: 25, right: 30, left: 0, bottom: 0 }}
>
<defs>
<linearGradient id="fillDesktop" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#3AEA83" stopOpacity={1} />
<stop offset="100%" stopColor="#3AEA83" stopOpacity={0} />
</linearGradient>
</defs>
<CartesianGrid
vertical={false}
stroke="rgba(255,255,255,0.1)"
/>
<XAxis
dataKey="year"
stroke="#9ca3af"
fontSize={12}
tickLine={false}
tickMargin={8}
axisLine={false}
tickFormatter={formatPersianYear}
/>
<YAxis
stroke="#9ca3af"
fontSize={12}
tickMargin={12}
tickLine={false}
axisLine={false}
tickFormatter={(value) => formatNumber(value)}
/>
<Tooltip cursor={false} content={<></>} />
{/* ✅ Use gradient for fill */}
<Area
type="monotone"
dataKey="value"
stroke="#3AEA83"
fill="url(#fillDesktop)"
strokeWidth={2}
activeDot={({ cx, cy, payload }) => (
<g>
{/* Small circle */}
<circle cx={cx} cy={cy} r={5} fill="#3AEA83" stroke="#fff" strokeWidth={2} />
{/* Year label above point */}
<text
x={cx}
y={cy - 10}
textAnchor="middle"
fontSize={12}
fontWeight="bold"
fill="#3AEA83"
>
{formatPersianYear(payload.year)}
</text>
</g>
)}
/>
</AreaChart>
</ResponsiveContainer>
) : (
<div className="flex items-center justify-center h-full text-gray-400 font-persian">
دادهای برای نمایش وجود ندارد
</div>
)}
</div>
</CardContent>
</Card>
</div>
);
}
export default InfoPanel;