diff --git a/app/components/charts/BubbleChart.tsx b/app/components/charts/BubbleChart.tsx index f8b9a6d..9f9a9c9 100644 --- a/app/components/charts/BubbleChart.tsx +++ b/app/components/charts/BubbleChart.tsx @@ -1,18 +1,15 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ /* eslint-disable no-unexpected-multiline */ /* eslint-disable @typescript-eslint/no-explicit-any */ import { FC, useEffect, useRef, useState } from "react"; import * as d3 from "d3"; import { NodeType } from "~/types"; -import { ZoomControl } from "../zoom/ZoomControl"; -import ModalInformation from "../information/ModalInformation"; import { useScreenSize } from "~/context/ScreenSizeContext"; -import { Breadcrumb } from "../breadcrumb/Breadcrumb"; interface BubbleChartProps { data: NodeType; - modalData: any; + onSelectNodePath?: (args: any) => void; onSelectNode?: (args: any) => void; + onZoom?: (args: any) => void; } const colors = [ @@ -23,45 +20,21 @@ const colors = [ "#E5C8A6", "#4D5D6D", ]; -const minNodeRadius = 100; const BubbleChart: FC = ({ data, - modalData, + onSelectNodePath, onSelectNode, }) => { const svgRef = useRef(null); const [isSVGRendered, setIsSVGRendered] = useState(false); - const [zoomPercentage, setZoomPercentage] = useState(100); const [selectedNode, setSelectedNode] = useState(null); - const [isInfoModalOpen, setIsInfoModalOpen] = useState(false); const { isDesktop, isTablet } = useScreenSize(); const size = isDesktop ? 900 : isTablet ? 600 : 300; const domainMinValue = 0; const domainMaxValue = 5; const minFontSize = 1; const maxFontSize = 22; - const [nodeAncestors, setNodeAncestors] = useState([]); - - const handleIsInfoModalOpen = () => { - setIsInfoModalOpen(true); - }; - - const handleIsInfoModalClose = () => { - setSelectedNode(null); - setIsInfoModalOpen(false); - }; - - const handleZoomChange = (newZoomPercentage: number) => { - setZoomPercentage(newZoomPercentage); - // TODO: Add here the zoom handler for the chart - }; - - useEffect(() => { - if (selectedNode) { - handleIsInfoModalOpen(); - } - }, [selectedNode]); useEffect(() => { if (!svgRef.current || isSVGRendered) return; @@ -123,6 +96,7 @@ const BubbleChart: FC = ({ .style("visibility", "visible"); } else { setSelectedNode(d as any); + onSelectNode && onSelectNode((d?.data as any)?.name ?? ""); event.stopPropagation(); } @@ -133,8 +107,7 @@ const BubbleChart: FC = ({ currentNode = currentNode.parent; } - onSelectNode && onSelectNode(nodePath); - setNodeAncestors(nodePath); + onSelectNodePath && onSelectNodePath(nodePath); }); const label = svg @@ -228,6 +201,7 @@ const BubbleChart: FC = ({ function zoom(event: d3.D3ZoomEvent, d: any) { if (selectedNode !== null || !d.children) return; + // eslint-disable-next-line @typescript-eslint/no-unused-vars const focus0 = focus; focus = d; @@ -235,6 +209,7 @@ const BubbleChart: FC = ({ const transition = svg .transition() .duration((event as any).altKey ? 7500 : 750) + // eslint-disable-next-line @typescript-eslint/no-unused-vars .tween("zoom", (d) => { const i = d3.interpolateZoom(view, [focus.x, focus.y, focus.r * 2]); return (t) => zoomTo(i(t)); @@ -254,30 +229,9 @@ const BubbleChart: FC = ({ }); } setIsSVGRendered(true); - }, [data, isSVGRendered, onSelectNode, selectedNode, size]); - - return ( - <> - - {isInfoModalOpen && selectedNode && !selectedNode?.children && ( - - )} + }, [data, isSVGRendered, onSelectNode, onSelectNodePath, selectedNode, size]); -
- -
-
- -
- - ); + return ; }; export default BubbleChart; diff --git a/app/components/information/ModalInformation.tsx b/app/components/information/ModalInformation.tsx index 788bc39..500d971 100644 --- a/app/components/information/ModalInformation.tsx +++ b/app/components/information/ModalInformation.tsx @@ -8,7 +8,7 @@ import Pill from "../common/Pill"; interface ModalInformationProps { nodeName: string; - modalData: AIProducts; + modalData: AIProducts | null; onClose: () => void; className?: string; } @@ -20,10 +20,13 @@ const ModalInformation: FC = ({ className, }) => { const name = nodeName; - const product = modalData.find((product) => product.name === name); + const product = + modalData && modalData.find((product) => product.name === name); const relatedTools = modalData - ?.filter((p) => p.name !== name && p.ecosystem === product?.ecosystem) - .map((p) => p.name); + ? modalData + ?.filter((p) => p.name !== name && p.ecosystem === product?.ecosystem) + .map((p) => p.name) + : []; // TODO - Replace information with actual data from the node const bestFeatures: any[] = []; // TODO - Replace information with actual data from the node @@ -41,7 +44,9 @@ const ModalInformation: FC = ({ return ( product && ( -
+

{product?.name} diff --git a/app/components/navigation/ViewSwitcher.tsx b/app/components/navigation/ViewSwitcher.tsx index 5067441..a0c3925 100644 --- a/app/components/navigation/ViewSwitcher.tsx +++ b/app/components/navigation/ViewSwitcher.tsx @@ -33,7 +33,7 @@ const ViewSwitcher: FC = ({ onSwitch }) => { }; return ( -
+
void; } -const AIProductTable: FC = ({ products }) => { - const [selectedProduct, setSelectedProduct] = useState(null); - const [isInfoModalOpen, setIsInfoModalOpen] = useState(false); +const AIProductTable: FC = ({ + products, + onViewDetails, +}) => { const [filters, setFilters] = useState({ licence: "All", ecosystem: "All", @@ -79,15 +81,9 @@ const AIProductTable: FC = ({ products }) => { ) ); - const handleIsInfoOpenModal = (product: AIProduct) => { - setSelectedProduct(product); - setIsInfoModalOpen(true); - } - - const handleIsInfoModalClose = () => { - setSelectedProduct(null); - setIsInfoModalOpen(false); - } + const handleViewDetails = (productName: string) => { + onViewDetails(productName); + }; return ( <> @@ -169,7 +165,7 @@ const AIProductTable: FC = ({ products }) => { Name Category - Ecosystem + AI Model License Details @@ -181,7 +177,7 @@ const AIProductTable: FC = ({ products }) => { className="bg-secondary font-nunito" > - {product.name} + {product.name} {product.category.join(", ")} @@ -193,7 +189,10 @@ const AIProductTable: FC = ({ products }) => { {product.licence} - handleIsInfoOpenModal(product)}> + handleViewDetails(product.name)} + > @@ -204,14 +203,6 @@ const AIProductTable: FC = ({ products }) => {
- {isInfoModalOpen && selectedProduct && ( - - )} ); }; diff --git a/app/context/ModalContext.tsx b/app/context/ModalContext.tsx new file mode 100644 index 0000000..1b15f18 --- /dev/null +++ b/app/context/ModalContext.tsx @@ -0,0 +1,30 @@ +import { createContext, useState, ReactNode, FC } from "react"; + +interface ModalContextProps { + isModalOpen: boolean; + openModal: () => void; + closeModal: () => void; +} + +export const ModalContext = createContext({ + isModalOpen: false, + openModal: () => {}, + closeModal: () => {}, +}); + +interface ModalProviderProps { + children: ReactNode; +} + +export const ModalProvider: FC = ({ children }) => { + const [isModalOpen, setIsModalOpen] = useState(false); + + const openModal = () => setIsModalOpen(true); + const closeModal = () => setIsModalOpen(false); + + return ( + + {children} + + ); +}; diff --git a/app/root.tsx b/app/root.tsx index ea54ffb..5b9cc2e 100644 --- a/app/root.tsx +++ b/app/root.tsx @@ -7,6 +7,7 @@ import { } from "@remix-run/react"; import "./tailwind.css"; import { ScreenSizeProvider } from "./context/ScreenSizeContext"; +import { ModalProvider } from "./context/ModalContext"; export function Layout({ children }: { children: React.ReactNode }) { return ( @@ -20,7 +21,7 @@ export function Layout({ children }: { children: React.ReactNode }) { - {children} + {children} diff --git a/app/routes/_index.tsx b/app/routes/_index.tsx index 3fbdb64..029ae34 100644 --- a/app/routes/_index.tsx +++ b/app/routes/_index.tsx @@ -1,13 +1,17 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import type { MetaFunction } from "@remix-run/node"; -import { useEffect, useState } from "react"; +import { useContext, useEffect, useState } from "react"; +import { Breadcrumb } from "~/components/breadcrumb/Breadcrumb"; import BubbleChart from "~/components/charts/BubbleChart"; //import SunburstChart from "~/components/charts/SunburstChart"; import HeaderIcon from "~/components/icons/HeaderIcon"; +import ModalInformation from "~/components/information/ModalInformation"; import { Loader } from "~/components/loader/Loader"; import { TopNavigation } from "~/components/navigation/TopNavigation"; import ViewSwitcher, { ViewType } from "~/components/navigation/ViewSwitcher"; import AIProductTable from "~/components/tables/AIProductTable"; +import { ZoomControl } from "~/components/zoom/ZoomControl"; +import { ModalContext } from "~/context/ModalContext"; import { NotificationType } from "~/types"; export const meta: MetaFunction = () => { @@ -21,9 +25,29 @@ export const meta: MetaFunction = () => { }; export default function Index() { + const { isModalOpen, openModal, closeModal } = useContext(ModalContext); const [jsonData, setJsonData] = useState(null); const [jsonModalData, setJsonModalData] = useState(null); const [notifications, setNotifications] = useState([]); + const [nodeAncestors, setNodeAncestors] = useState([]); + const [zoomPercentage, setZoomPercentage] = useState(100); + const [productName, setProductName] = useState(""); + + const handleIsInfoModalClose = () => { + setProductName(""); + closeModal(); + }; + + useEffect(() => { + if (productName.length > 0) { + openModal(); + } + }, [openModal, productName]); + + const handleZoomChange = (newZoomPercentage: number) => { + setZoomPercentage(newZoomPercentage); + }; + const newNotifications = notifications.length > 0 && notifications.some((notification) => { @@ -65,48 +89,84 @@ export default function Index() { ]); }, []); + const handleOnSwitch = (view: ViewType) => { + setCurrentView(view); + setProductName(""); + closeModal(); + }; + return ( -
-
- -
-
- -
-
- -
- {!jsonData || !jsonModalData ? ( - - ) : ( -
-
-
- + <> +
+
+ +
+
+ +
+
+ +
+ {!jsonData || !jsonModalData ? ( + + ) : ( +
+
+
+ +
-
-
-
- +
+
+ +
-
+ )} + {currentView === ViewType.BubbleChart && ( +
+ +
+ )} + {currentView === ViewType.BubbleChart && ( +
+ +
+ )} +
+ {isModalOpen && productName.length > 0 && ( + )} -
+ ); }