258 lines
10 KiB
TypeScript
258 lines
10 KiB
TypeScript
import moment from "moment-jalaali";
|
||
import React from "react";
|
||
import { ProtectedRoute } from "~/components/auth/protected-route";
|
||
import { DashboardLayout } from "~/components/dashboard/layout";
|
||
import { InfoPanel } from "~/components/ecosystem/info-panel";
|
||
import { NetworkGraph } from "~/components/ecosystem/network-graph";
|
||
import { Card, CardContent } from "~/components/ui/card";
|
||
import {
|
||
Dialog,
|
||
DialogContent,
|
||
DialogHeader,
|
||
DialogTitle,
|
||
} from "~/components/ui/dialog";
|
||
import { useAuth } from "~/contexts/auth-context";
|
||
import type { Route } from "./+types/ecosystem";
|
||
|
||
// Get API base URL at module level to avoid process.env access in browser
|
||
const API_BASE_URL =
|
||
//بندر امام
|
||
// import.meta.env.VITE_API_URL || "https://inogen-back.pelekan.org/api";
|
||
//آپادانا
|
||
import.meta.env.VITE_API_URL || "https://APADANA-IATM-back.pelekan.org/api";
|
||
//نوری
|
||
// import.meta.env.VITE_API_URL || "https://NOPC-IATM-back.pelekan.org/api";
|
||
|
||
|
||
|
||
// Import the CompanyDetails type
|
||
import { Hexagon } from "lucide-react";
|
||
import type { CompanyDetails } from "~/components/ecosystem/network-graph";
|
||
|
||
export function meta({}: Route.MetaArgs) {
|
||
return [
|
||
{ title: "زیست بوم فناوری" },
|
||
{
|
||
name: "description",
|
||
content: "نمایش زیست بوم فناوری با گراف شبکهای شرکتها",
|
||
},
|
||
];
|
||
}
|
||
|
||
moment.loadPersian({ usePersianDigits: true });
|
||
|
||
function handleValue(val: any): any {
|
||
if (val == null) return val;
|
||
if (
|
||
typeof val === "string" &&
|
||
/^\d{4}[-/]\d{2}[-/]\d{2}( \d{2}:\d{2}(:\d{2})?)?$/.test(val)
|
||
) {
|
||
return moment(val, "YYYY-MM-DD HH:mm:ss").format("YYYY/MM/DD");
|
||
}
|
||
if (
|
||
typeof val === "number" ||
|
||
(typeof val === "string" && /^-?\d+$/.test(val))
|
||
) {
|
||
return val.toString().replace(/\d/g, (d) => "۰۱۲۳۴۵۶۷۸۹"[+d]);
|
||
}
|
||
return val;
|
||
}
|
||
|
||
export default function EcosystemPage() {
|
||
const [selectedCompany, setSelectedCompany] =
|
||
React.useState<CompanyDetails | null>(null);
|
||
const [isDialogLoading, setIsDialogLoading] = React.useState(false);
|
||
const { token } = useAuth();
|
||
|
||
const closeDialog = () => {
|
||
setSelectedCompany(null);
|
||
setIsDialogLoading(false);
|
||
};
|
||
|
||
const handleNodeClick = (company: CompanyDetails) => {
|
||
setSelectedCompany(company);
|
||
};
|
||
|
||
const handleLoadingChange = (loading: boolean) => {
|
||
setIsDialogLoading(loading);
|
||
};
|
||
|
||
// Construct image URL
|
||
const getImageUrl = (stageid: number) => {
|
||
return `${API_BASE_URL}/getimage?stageID=${stageid}&nameOrID=image&token=${token?.accessToken}`;
|
||
};
|
||
|
||
return (
|
||
<ProtectedRoute requireAuth={true}>
|
||
<DashboardLayout title="زیست بوم فناوری">
|
||
<div>
|
||
<div className="grid grid-cols-1 items-start lg:grid-cols-12 gap-4">
|
||
<div className="lg:col-span-4">
|
||
<InfoPanel selectedCompany={selectedCompany} />
|
||
</div>
|
||
|
||
<div className="lg:col-span-8 h-full">
|
||
<Card className="h-full overflow-hidden bg-transparent border-[#3F415A]">
|
||
<CardContent className="p-0 h-full bg-transparent">
|
||
<NetworkGraph
|
||
onNodeClick={handleNodeClick}
|
||
onLoadingChange={handleLoadingChange}
|
||
/>
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Node info dialog */}
|
||
<Dialog
|
||
open={!!selectedCompany}
|
||
onOpenChange={(open) => !open && closeDialog()}
|
||
>
|
||
<DialogContent className="font-persian max-w-6xl min-h-max bg-[linear-gradient(to_bottom_left,#464861,20%,#111628)]">
|
||
<DialogHeader>
|
||
<DialogTitle className="text-right border-b-2 border-gray-600 pt-2 pb-4 mr-4 text-sm font-semibold">
|
||
معرفی
|
||
<span> {selectedCompany?.category}</span>
|
||
</DialogTitle>
|
||
</DialogHeader>
|
||
|
||
{isDialogLoading ? (
|
||
<div className="grid grid-cols-1 md:grid-cols-2 p-4 gap-6">
|
||
{/* Right Column - Loading Skeleton */}
|
||
<div className="space-y-4 p-6 border-l-2 border-gray-600">
|
||
{/* Company Image & Title Skeleton */}
|
||
<div className="flex justify-between px-10 items-center mb-4">
|
||
<div className="h-8 bg-gray-600 rounded animate-pulse w-48"></div>
|
||
<div className="w-12 h-12 bg-gray-600 rounded-2xl animate-pulse"></div>
|
||
</div>
|
||
{/* Description Skeleton */}
|
||
<div className="p-4 rounded-lg space-y-2">
|
||
<div className="h-4 bg-gray-600 rounded animate-pulse w-full"></div>
|
||
<div className="h-4 bg-gray-600 rounded animate-pulse w-5/6"></div>
|
||
<div className="h-4 bg-gray-600 rounded animate-pulse w-4/6"></div>
|
||
<div className="h-4 bg-gray-600 rounded animate-pulse w-3/6"></div>
|
||
</div>
|
||
</div>
|
||
{/* Left Column - Loading Skeleton */}
|
||
<div className="space-y-2">
|
||
<div className="h-6 bg-gray-600 rounded animate-pulse w-32"></div>
|
||
<div className="space-y-3 px-2">
|
||
{Array.from({ length: 6 }).map((_, index) => (
|
||
<div
|
||
key={index}
|
||
className="flex justify-between items-center rounded-lg"
|
||
>
|
||
<div className="flex items-center gap-1">
|
||
<div className="h-4 w-4 bg-gray-600 rounded animate-pulse"></div>
|
||
<div className="h-4 bg-gray-600 rounded animate-pulse w-24"></div>
|
||
</div>
|
||
<div className="h-4 bg-gray-600 rounded animate-pulse w-20"></div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
) : (
|
||
<div className="grid p-4 pb-6 grid-cols-1 md:grid-cols-2 gap-6">
|
||
{/* Right Column - Description */}
|
||
<div className="space-y-4 p-6 border-l-2 border-gray-600">
|
||
{/* Company Image */}
|
||
<div className="flex justify-between px-10 items-center text-3xl font-bold mb-4">
|
||
{selectedCompany?.label || ""}
|
||
{selectedCompany?.stageid && token?.accessToken ? (
|
||
<img
|
||
src={getImageUrl(selectedCompany.stageid)}
|
||
alt={selectedCompany?.label || ""}
|
||
className="w-12 h-12 object-cover rounded-2xl"
|
||
onError={(e) => {
|
||
// Hide image and show fallback on error
|
||
e.currentTarget.style.display = "none";
|
||
if (e.currentTarget.nextSibling) {
|
||
(
|
||
e.currentTarget.nextSibling as HTMLElement
|
||
).style.display = "flex";
|
||
}
|
||
}}
|
||
/>
|
||
) : null}
|
||
<div
|
||
className="w-24 h-24 rounded-full bg-gray-600 border-4 border-pr-green flex items-center justify-center"
|
||
style={{
|
||
display:
|
||
selectedCompany?.stageid && token?.accessToken
|
||
? "none"
|
||
: "flex",
|
||
}}
|
||
>
|
||
<svg
|
||
className="w-10 h-10 text-gray-400"
|
||
fill="none"
|
||
stroke="currentColor"
|
||
viewBox="0 0 24 24"
|
||
>
|
||
<path
|
||
strokeLinecap="round"
|
||
strokeLinejoin="round"
|
||
strokeWidth={2}
|
||
d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"
|
||
/>
|
||
</svg>
|
||
</div>
|
||
</div>
|
||
{selectedCompany?.description ? (
|
||
<div className="p-4 rounded-lg">
|
||
<p className="font-persian text-sm font-normal leading-relaxed">
|
||
{selectedCompany.description}
|
||
</p>
|
||
</div>
|
||
) : (
|
||
<div className="text-gray-500 font-persian text-sm">
|
||
توضیحات در دسترس نیست
|
||
</div>
|
||
)}
|
||
</div>
|
||
{/* Left Column - Company Fields */}
|
||
<div className="space-y-2">
|
||
<h3 className="font-persian gap-1 flex text-sm font-semibold">
|
||
اطلاعات
|
||
<span>{selectedCompany?.category}</span>
|
||
</h3>
|
||
{selectedCompany?.fields &&
|
||
selectedCompany.fields.length > 0 ? (
|
||
<div className="space-y-3 px-2">
|
||
{selectedCompany.fields.map((field, index) => (
|
||
<div
|
||
key={index}
|
||
className="flex justify-between items-center rounded-lg"
|
||
>
|
||
<span className="font-persian flex items-center gap-1 text-sm font-light">
|
||
<Hexagon className="text-pr-green h-4 w-4" />
|
||
{field.N}:
|
||
</span>
|
||
<span className="text-right min-w-1/3">
|
||
<span className="font-persian text-sm font-normal text-right">
|
||
{handleValue(field.V)}
|
||
{field.U && (
|
||
<span className="mr-1">({field.U})</span>
|
||
)}
|
||
</span>
|
||
</span>
|
||
</div>
|
||
))}
|
||
</div>
|
||
) : (
|
||
<div className="text-gray-500 font-persian text-sm">
|
||
اطلاعات تکمیلی در دسترس نیست
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
)}
|
||
</DialogContent>
|
||
</Dialog>
|
||
</DashboardLayout>
|
||
</ProtectedRoute>
|
||
);
|
||
}
|