Reviewed-on: https://git.pelekan.org/Saeed0920/inogen/pulls/15
Co-authored-by: Saeed Abadiyan <sd.eed1381@gmail.com>
Co-committed-by: Saeed Abadiyan <sd.eed1381@gmail.com>
This commit is contained in:
Saeed AB 2025-10-04 02:21:54 +03:30 committed by Saeed AB
parent b4b023ec32
commit a5ae3cc813
8 changed files with 1211 additions and 609 deletions

View File

@ -616,22 +616,22 @@ export function ProductInnovationPage() {
size="sm" size="sm"
onClick={() => { onClick={() => {
handleProjectDetails(item)}} handleProjectDetails(item)}}
className="text-emerald-400 underline underline-offset-4 font-ligth text-base hover:bg-emerald-500/20 p-2 h-auto" className="text-emerald-400 underline underline-offset-4 font-ligth text-sm hover:bg-emerald-500/20 p-2 h-auto"
> >
جزئیات بیشتر جزئیات بیشتر
</Button> </Button>
); );
case "project_no": case "project_no":
return ( return (
<Badge variant="outline" className="font-mono text-base font-light"> <Badge variant="outline" className="font-mono text-sm font-light">
{String(value)} {String(value)}
</Badge> </Badge>
); );
case "title": case "title":
return <span className="font-light text-base text-white">{String(value)}</span>; return <span className="font-light text-sm text-white">{String(value)}</span>;
case "project_status": case "project_status":
return ( return (
<div className="flex items-center text-base font-light gap-1"> <div className="flex items-center text-sm font-light gap-1">
<Badge <Badge
variant={statusColor(value as projectStatus)} variant={statusColor(value as projectStatus)}
className="font-semibold text-base border-2 p-0 block w-2 h-2 rounded-full" className="font-semibold text-base border-2 p-0 block w-2 h-2 rounded-full"
@ -652,7 +652,7 @@ export function ProductInnovationPage() {
</Badge> </Badge>
); );
default: default:
return <span className="text-white text-base font-light">{String(value) || "-"}</span>; return <span className="text-white text-sm font-light">{String(value) || "-"}</span>;
} }
}; };

View File

@ -8,6 +8,9 @@ import {
Radar, Radar,
Settings, Settings,
Star, Star,
Workflow,
DiscAlbum,
LucideLightbulb
} from "lucide-react"; } from "lucide-react";
import React, { useState } from "react"; import React, { useState } from "react";
import { Link, useLocation } from "react-router"; import { Link, useLocation } from "react-router";
@ -95,15 +98,9 @@ const menuItems: MenuItem[] = [
{ {
id: "ideas", id: "ideas",
label: "ایده‌های فناوری و نوآوری", label: "ایده‌های فناوری و نوآوری",
icon: House, icon: LucideLightbulb,
href: "/dashboard/manage-ideas-tech", href: "/dashboard/manage-ideas-tech",
}, },
{
id: "top-innovations",
label: "نوآور برتر",
icon: Star,
href: "/dashboard/top-innovations",
},
{ {
id: "strategic-alignment", id: "strategic-alignment",
label: "میزان انطباق راهبردی", label: "میزان انطباق راهبردی",

View File

@ -256,7 +256,7 @@ export function InfoPanel({ selectedCompany }: InfoPanelProps) {
{Array.from({ length: 4 }).map((_, i) => ( {Array.from({ length: 4 }).map((_, i) => (
<div <div
key={i} key={i}
className="absolute w-2 h-2 bg-green-400 rounded-full animate-pulse" className="absolute w-2 h-2 bg-pr-green rounded-full animate-pulse"
style={{ style={{
left: `${20 + i * 25}%`, left: `${20 + i * 25}%`,
top: `${30 + Math.random() * 40}%`, top: `${30 + Math.random() * 40}%`,
@ -287,7 +287,7 @@ export function InfoPanel({ selectedCompany }: InfoPanelProps) {
{/* Actor Count Skeleton */} {/* Actor Count Skeleton */}
<CardHeader className="text-center pt-0 pb-4"> <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-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> <div className="w-16 h-8 bg-pr-green bg-opacity-30 rounded animate-pulse mx-auto"></div>
</CardHeader> </CardHeader>
{/* Bar Chart Skeleton */} {/* Bar Chart Skeleton */}
@ -362,7 +362,7 @@ export function InfoPanel({ selectedCompany }: InfoPanelProps) {
{Array.from({ length: 4 }).map((_, i) => ( {Array.from({ length: 4 }).map((_, i) => (
<div <div
key={i} key={i}
className="absolute w-2 h-2 bg-green-400 rounded-full animate-pulse" className="absolute w-2 h-2 bg-pr-green rounded-full animate-pulse"
style={{ style={{
left: `${20 + i * 25}%`, left: `${20 + i * 25}%`,
top: `${30 + Math.random() * 40}%`, top: `${30 + Math.random() * 40}%`,
@ -378,7 +378,7 @@ export function InfoPanel({ selectedCompany }: InfoPanelProps) {
<CardContent className="pt-0 pb-6"> <CardContent className="pt-0 pb-6">
<div className="bg-[rgba(255,255,255,0.1)] rounded-lg p-4 text-center"> <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-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 className="w-12 h-6 bg-pr-green bg-opacity-30 rounded animate-pulse mx-auto"></div>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>

View File

@ -43,6 +43,7 @@ export interface CompanyDetails {
export interface NetworkGraphProps { export interface NetworkGraphProps {
onNodeClick?: (node: CompanyDetails) => void; onNodeClick?: (node: CompanyDetails) => void;
onLoadingChange?: (loading: boolean) => void;
} }
function parseApiResponse(raw: any): any[] { function parseApiResponse(raw: any): any[] {
@ -58,7 +59,7 @@ function isBrowser(): boolean {
return typeof window !== "undefined"; return typeof window !== "undefined";
} }
export function NetworkGraph({ onNodeClick }: NetworkGraphProps) { export function NetworkGraph({ onNodeClick, onLoadingChange }: NetworkGraphProps) {
const svgRef = useRef<SVGSVGElement | null>(null); const svgRef = useRef<SVGSVGElement | null>(null);
const [nodes, setNodes] = useState<Node[]>([]); const [nodes, setNodes] = useState<Node[]>([]);
const [links, setLinks] = useState<Link[]>([]); const [links, setLinks] = useState<Link[]>([]);
@ -441,6 +442,19 @@ export function NetworkGraph({ onNodeClick }: NetworkGraphProps) {
if (d.isCenter) return; if (d.isCenter) return;
if (onNodeClick && d.stageid) { if (onNodeClick && d.stageid) {
// Open dialog immediately with basic info
const basicDetails: CompanyDetails = {
id: d.id,
label: d.label,
category: d.category,
stageid: d.stageid,
fields: [],
};
onNodeClick(basicDetails);
// Start loading
onLoadingChange?.(true);
try { try {
const res = await callAPI(d.stageid); const res = await callAPI(d.stageid);
const responseData = JSON.parse(res.data); const responseData = JSON.parse(res.data);
@ -473,14 +487,10 @@ export function NetworkGraph({ onNodeClick }: NetworkGraphProps) {
onNodeClick(companyDetails); onNodeClick(companyDetails);
} catch (error) { } catch (error) {
console.error("Failed to fetch company details:", error); console.error("Failed to fetch company details:", error);
const basicDetails: CompanyDetails = { // Keep the basic details already shown
id: d.id, } finally {
label: d.label, // Stop loading
category: d.category, onLoadingChange?.(false);
stageid: d.stageid,
fields: [],
};
onNodeClick(basicDetails);
} }
} }
}); });

View File

@ -4,7 +4,7 @@ import { cn } from "~/lib/utils"
interface TableProps extends React.HTMLAttributes<HTMLTableElement> { interface TableProps extends React.HTMLAttributes<HTMLTableElement> {
containerClassName?: string containerClassName?: string
containerRef?: React.RefObject<HTMLDivElement> containerRef?: React.RefObject<HTMLDivElement | null>
} }
const Table = React.forwardRef<HTMLTableElement, TableProps>( const Table = React.forwardRef<HTMLTableElement, TableProps>(

View File

@ -56,10 +56,20 @@ function handleValue(val: any): any {
export default function EcosystemPage() { export default function EcosystemPage() {
const [selectedCompany, setSelectedCompany] = const [selectedCompany, setSelectedCompany] =
React.useState<CompanyDetails | null>(null); React.useState<CompanyDetails | null>(null);
const [isDialogLoading, setIsDialogLoading] = React.useState(false);
const { token } = useAuth(); const { token } = useAuth();
const closeDialog = () => { const closeDialog = () => {
setSelectedCompany(null); setSelectedCompany(null);
setIsDialogLoading(false);
};
const handleNodeClick = (company: CompanyDetails) => {
setSelectedCompany(company);
};
const handleLoadingChange = (loading: boolean) => {
setIsDialogLoading(loading);
}; };
// Construct image URL // Construct image URL
@ -70,7 +80,7 @@ export default function EcosystemPage() {
return ( return (
<ProtectedRoute requireAuth={true}> <ProtectedRoute requireAuth={true}>
<DashboardLayout title="زیست بوم فناوری"> <DashboardLayout title="زیست بوم فناوری">
<div className=""> <div>
<div className="grid grid-cols-1 items-start lg:grid-cols-12 gap-4"> <div className="grid grid-cols-1 items-start lg:grid-cols-12 gap-4">
<div className="lg:col-span-4"> <div className="lg:col-span-4">
<InfoPanel selectedCompany={selectedCompany} /> <InfoPanel selectedCompany={selectedCompany} />
@ -79,7 +89,7 @@ export default function EcosystemPage() {
<div className="lg:col-span-8 h-full"> <div className="lg:col-span-8 h-full">
<Card className="h-full overflow-hidden bg-transparent border-[#3F415A]"> <Card className="h-full overflow-hidden bg-transparent border-[#3F415A]">
<CardContent className="p-0 h-full bg-transparent"> <CardContent className="p-0 h-full bg-transparent">
<NetworkGraph onNodeClick={setSelectedCompany} /> <NetworkGraph onNodeClick={handleNodeClick} onLoadingChange={handleLoadingChange} />
</CardContent> </CardContent>
</Card> </Card>
</div> </div>
@ -91,7 +101,7 @@ export default function EcosystemPage() {
open={!!selectedCompany} open={!!selectedCompany}
onOpenChange={(open) => !open && closeDialog()} 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)]"> <DialogContent className="font-persian max-w-6xl min-h-max bg-[linear-gradient(to_bottom_left,#464861,20%,#111628)]">
<DialogHeader> <DialogHeader>
<DialogTitle className="text-right border-b-2 border-gray-600 pt-2 pb-4 mr-4 text-sm font-semibold"> <DialogTitle className="text-right border-b-2 border-gray-600 pt-2 pb-4 mr-4 text-sm font-semibold">
معرفی معرفی
@ -99,7 +109,44 @@ export default function EcosystemPage() {
</DialogTitle> </DialogTitle>
</DialogHeader> </DialogHeader>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6"> {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 */} {/* Right Column - Description */}
<div className="space-y-4 p-6 border-l-2 border-gray-600"> <div className="space-y-4 p-6 border-l-2 border-gray-600">
{/* Company Image */} {/* Company Image */}
@ -122,7 +169,7 @@ export default function EcosystemPage() {
/> />
) : null} ) : null}
<div <div
className="w-24 h-24 rounded-full bg-gray-600 border-4 border-green-400 flex items-center justify-center" className="w-24 h-24 rounded-full bg-gray-600 border-4 border-pr-green flex items-center justify-center"
style={{ style={{
display: display:
selectedCompany?.stageid && token?.accessToken selectedCompany?.stageid && token?.accessToken
@ -191,6 +238,7 @@ export default function EcosystemPage() {
)} )}
</div> </div>
</div> </div>
)}
</DialogContent> </DialogContent>
</Dialog> </Dialog>
</DashboardLayout> </DashboardLayout>