inogen/app/routes/ecosystem.tsx

197 lines
7.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

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.

import type { Route } from "./+types/ecosystem";
import React from "react";
import { ProtectedRoute } from "~/components/auth/protected-route";
import { DashboardLayout } from "~/components/dashboard/layout";
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "~/components/ui/dialog";
import { NetworkGraph } from "~/components/ecosystem/network-graph";
import { InfoPanel } from "~/components/ecosystem/info-panel";
import { useAuth } from "~/contexts/auth-context";
import moment from "moment-jalaali";
// 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 the CompanyDetails type
import type { CompanyDetails } from "~/components/ecosystem/network-graph";
import { formatNumber } from "~/lib/utils";
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 { token } = useAuth();
const closeDialog = () => {
setSelectedCompany(null);
};
// 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 className="p-4 lg:p-6">
<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">
<CardContent className="p-0 h-full">
<NetworkGraph onNodeClick={setSelectedCompany} />
</CardContent>
</Card>
</div>
</div>
</div>
{/* Node info dialog */}
<Dialog
open={!!selectedCompany}
onOpenChange={(open) => !open && closeDialog()}
>
<DialogContent className="font-persian max-w-6xl max-h-[75vh] overflow-y-auto bg-[linear-gradient(to_bottom_left,#464861,20%,#111628)]">
<DialogHeader>
<DialogTitle className="text-right border-b-2 border-gray-600 py-2 mr-4 text-xl">
معرفی
<span> {selectedCompany?.category}</span>
</DialogTitle>
<DialogDescription className="text-center text-green-400"></DialogDescription>
</DialogHeader>
<div className="grid 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-14 h-14 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-green-400 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 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-lg font-bold">
اطلاعات
<span>{selectedCompany?.category}</span>
</h3>
{selectedCompany?.fields &&
selectedCompany.fields.length > 0 ? (
<div className="space-y-3">
{selectedCompany.fields.map((field, index) => (
<div
key={index}
className="flex justify-between items-center rounded-lg"
>
<span className="font-persian font-light">
{field.N}:
</span>
<span className="font-persian font-light text-right">
{handleValue(field.V)}
{field.U && <span className="mr-1">({field.U})</span>}
</span>
</div>
))}
</div>
) : (
<div className="text-gray-500 font-persian text-sm">
اطلاعات تکمیلی در دسترس نیست
</div>
)}
</div>
</div>
</DialogContent>
</Dialog>
</DashboardLayout>
</ProtectedRoute>
);
}