From fccdaf2fdc8659fb27ddc67fe04a0fc07859f9e1 Mon Sep 17 00:00:00 2001 From: yanamavani Date: Mon, 30 Mar 2026 22:23:30 +0530 Subject: [PATCH 1/5] Add full OrgExplorer dashboard: Overview, HealthScore, ActivityChart , NetworkGraph , RepoTable , Insights & CSV export --- src/components/Cards/statCard.tsx | 13 ++ src/components/Charts/ActivityChart.tsx | 121 +++++++++++++ src/components/Graph/NetworkGraph.tsx | 212 +++++++++++++++++++++++ src/components/HealthScore.tsx | 28 +++ src/components/Input/OrgInput.tsx | 22 +++ src/components/Insights/Insightpanel.tsx | 16 ++ src/components/Tables/RepoTable.tsx | 80 +++++++++ src/layout/DashboardLayout.tsx | 23 +++ src/layout/Sidebar.tsx | 58 +++++++ src/layout/Topbar.tsx | 64 +++++++ src/pages/GraphPage.tsx | 32 ++++ src/pages/Overview.tsx | 119 +++++++++++++ src/pages/Repositories.tsx | 32 ++++ src/services/githubService.ts | 25 +++ src/types/github.ts | 16 ++ src/utils/calculateScore.ts | 48 +++++ src/utils/exportCSV.ts | 15 ++ src/utils/insightEngine.ts | 90 ++++++++++ src/utils/insights.ts | 63 +++++++ src/utils/mergeOrgs.ts | 13 ++ 20 files changed, 1090 insertions(+) create mode 100644 src/components/Cards/statCard.tsx create mode 100644 src/components/Charts/ActivityChart.tsx create mode 100644 src/components/Graph/NetworkGraph.tsx create mode 100644 src/components/HealthScore.tsx create mode 100644 src/components/Input/OrgInput.tsx create mode 100644 src/components/Insights/Insightpanel.tsx create mode 100644 src/components/Tables/RepoTable.tsx create mode 100644 src/layout/DashboardLayout.tsx create mode 100644 src/layout/Sidebar.tsx create mode 100644 src/layout/Topbar.tsx create mode 100644 src/pages/GraphPage.tsx create mode 100644 src/pages/Overview.tsx create mode 100644 src/pages/Repositories.tsx create mode 100644 src/services/githubService.ts create mode 100644 src/types/github.ts create mode 100644 src/utils/calculateScore.ts create mode 100644 src/utils/exportCSV.ts create mode 100644 src/utils/insightEngine.ts create mode 100644 src/utils/insights.ts create mode 100644 src/utils/mergeOrgs.ts diff --git a/src/components/Cards/statCard.tsx b/src/components/Cards/statCard.tsx new file mode 100644 index 0000000..2bca9ee --- /dev/null +++ b/src/components/Cards/statCard.tsx @@ -0,0 +1,13 @@ +interface Props { + title: string; + value: string | number; +} + +export default function StatCard({ title, value }: Props) { + return ( +
+

{title}

+

{value}

+
+ ); +} \ No newline at end of file diff --git a/src/components/Charts/ActivityChart.tsx b/src/components/Charts/ActivityChart.tsx new file mode 100644 index 0000000..149bcaf --- /dev/null +++ b/src/components/Charts/ActivityChart.tsx @@ -0,0 +1,121 @@ +import { useState, useMemo } from "react"; +import { + LineChart, Line, + BarChart, Bar, + PieChart, Pie, Cell, LabelList, + XAxis, YAxis, Tooltip, Legend +} from "recharts"; + +const tooltipFormatter = ( + value?: number | string, + name?: string +): [string, string] => { + return [ + value !== undefined ? value.toString() : "-", + name !== undefined ? name : "-" + ]; +}; + +export default function ActivityChart({ repos }: any) { + const [chartType, setChartType] = useState("line"); + const [filter, setFilter] = useState("top"); + + const COLORS = ["#22c55e", "#3b82f6", "#f59e0b", "#ef4444", "#8b5cf6", "#ec4899"]; + + const data = useMemo(() => { + let processed = [...repos]; + + if (filter === "top") { + processed.sort((a, b) => b.stargazers_count - a.stargazers_count); + } + if (filter === "inactive") { + const now = new Date().getTime(); + processed = processed.filter(r => + now - new Date(r.updated_at).getTime() > 90 * 24 * 60 * 60 * 1000 + ); + } + + return processed.slice(0, 8).map((r: any) => ({ + name: r.name, + stars: r.stargazers_count, + forks: r.forks_count + })); + }, [repos, filter]); + + return ( +
+ +
+ {["line", "bar", "pie"].map(type => ( + + ))} +
+ +
+ + +
+ + {chartType === "line" && ( + + + + + + + + + )} + + {chartType === "bar" && ( + + + + + + + + + + + + + )} + + {chartType === "pie" && ( + + `${name}: ${value}`} + > + {data.map((_, index) => ( + + ))} + + + + + )} + +
+ ); +} diff --git a/src/components/Graph/NetworkGraph.tsx b/src/components/Graph/NetworkGraph.tsx new file mode 100644 index 0000000..e416c6c --- /dev/null +++ b/src/components/Graph/NetworkGraph.tsx @@ -0,0 +1,212 @@ +import ForceGraph2D from "react-force-graph-2d"; +import { useEffect, useState, useRef } from "react"; +import { fetchRepoContributors } from "../../services/githubService"; +import * as d3 from "d3-force"; + +export default function NetworkGraph({ repos }: any) { + const [graphData, setGraphData] = useState({ + nodes: [], + links: [] + }); + + const fgRef = useRef(null); + + // IMAGE CACHE + const imageCache = useRef<{ [key: string]: HTMLImageElement }>({}); + + // BUILD GRAPH + useEffect(() => { + const buildGraph = async () => { + const nodes: any[] = []; + const links: any[] = []; + const addedUsers = new Set(); + + for (let repo of repos.slice(0, 6)) { + nodes.push({ + id: repo.name, + type: "repo", + stars: repo.stargazers_count + }); + + try { + const contributors = await fetchRepoContributors(repo.contributors_url); + + contributors.slice(0, 5).forEach((c: any) => { + if (!addedUsers.has(c.login)) { + addedUsers.add(c.login); + + nodes.push({ + id: c.login, + type: "user", + contributions: c.contributions, + avatar: c.avatar_url + }); + } + + links.push({ + source: c.login, + target: repo.name, + weight: c.contributions + }); + }); + + } catch (e) { + console.error(e); + } + } + + setGraphData({ nodes, links }); + }; + + if (repos.length) buildGraph(); + }, [repos]); + + // FORCE SETTINGS + useEffect(() => { + if (!fgRef.current) return; + + fgRef.current.d3Force("center", d3.forceCenter(0, 0)); + fgRef.current.d3Force("charge", d3.forceManyBody().strength(-100)); + fgRef.current.d3Force("link", d3.forceLink().distance(70)); + + fgRef.current.d3Force( + "radial", + d3.forceRadial((node: any) => { + return node.type === "repo" ? 80 : 180; + }).strength(0.8) + ); + + }, [graphData]); + + useEffect(() => { + if (!fgRef.current) return; + + // trigger render after mount + setTimeout(() => { + fgRef.current.zoomToFit(400); + }, 300); +}, [graphData]); + + return ( +
+ + { + const limit = 200; + + if(Math.abs(x) > limit || Math.abs(y) > limit) { + fgRef.current.centerAt(0, 0, 400); + } + }} + // AUTO CENTER + FREEZE + onEngineStop={() => { + fgRef.current.centerAt(0, 0, 400); + fgRef.current?.zoomToFit(400); + + graphData.nodes.forEach((node: any) => { + node.fx = node.x; + node.fy = node.y; + }); + + fgRef.current?.pauseAnimation(); + }} + + d3VelocityDecay={0.3} + d3AlphaDecay={0.02} + cooldownTicks={100} + warmupTicks={100} + + // TOOLTIP + nodeLabel={(node: any) => + node.type === "repo" + ? `📦 ${node.id} (⭐ ${node.stars})` + : `👤 ${node.id} (${node.contributions})` + } + + // NODE RENDER (FIXED AVATAR) + nodeCanvasObject={(node: any, ctx, globalScale) => { + const size = + node.type === "repo" + ? 10 + Math.log(node.stars + 1) + : 5; + + const fontSize = 9 / globalScale; + + if (node.type === "user" && node.avatar) { + let img = imageCache.current[node.avatar]; + + if (!img) { + img = new Image(); + img.src = node.avatar; + imageCache.current[node.avatar] = img; + } + + ctx.save(); + ctx.beginPath(); + ctx.arc(node.x, node.y, size, 0, 2 * Math.PI); + ctx.closePath(); + ctx.clip(); + + if (img.complete) { + ctx.drawImage( + img, + node.x - size, + node.y - size, + size * 2, + size * 2 + ); + } + + ctx.restore(); + + // white border + ctx.beginPath(); + ctx.arc(node.x, node.y, size, 0, 2 * Math.PI); + ctx.strokeStyle = "#ffffff"; + ctx.lineWidth = 1; + ctx.stroke(); + + } else { + // repo node + ctx.beginPath(); + ctx.arc(node.x, node.y, size, 0, 2 * Math.PI); + ctx.fillStyle = "#22c55e"; + ctx.fill(); + } + + // label + ctx.font = `${fontSize}px Inter`; + ctx.fillStyle = "#e2e8f0"; + ctx.fillText(node.id, node.x + size + 2, node.y + size / 2); + }} + + // EDGE DESIGN (VISIBLE) + linkWidth={(link: any) => + Math.max(1, Math.log(link.weight + 1)) + } + + linkColor={() => "rgba(200,200,200,0.6)"} + + linkDirectionalParticles={1} + linkDirectionalParticleSpeed={0.002} + + // HOVER + onNodeHover={(node) => { + document.body.style.cursor = node ? "pointer" : "default"; + }} + /> +
+ ); +} \ No newline at end of file diff --git a/src/components/HealthScore.tsx b/src/components/HealthScore.tsx new file mode 100644 index 0000000..dae57f4 --- /dev/null +++ b/src/components/HealthScore.tsx @@ -0,0 +1,28 @@ +export default function HealthScore({ score, label }: any) { + return ( +
+ +

+ Organization Health Score +

+ +
+ + {score}/100 + + + {label} + +
+ + {/* Progress bar */} +
+
+
+ +
+ ); +} \ No newline at end of file diff --git a/src/components/Input/OrgInput.tsx b/src/components/Input/OrgInput.tsx new file mode 100644 index 0000000..3418c63 --- /dev/null +++ b/src/components/Input/OrgInput.tsx @@ -0,0 +1,22 @@ +import { useState } from "react"; + +export default function OrgInput({ onSubmit }: any) { + const [value , setValue] = useState("Aossie-Org") + return ( +
+ setValue(e.target.value)} + /> + + +
+ ); +} \ No newline at end of file diff --git a/src/components/Insights/Insightpanel.tsx b/src/components/Insights/Insightpanel.tsx new file mode 100644 index 0000000..283671d --- /dev/null +++ b/src/components/Insights/Insightpanel.tsx @@ -0,0 +1,16 @@ +import type { Insight } from "../../types/github"; +import StatCard from "../Cards/statCard"; + +export default function InsightPanel({ data }: { data: any }) { + return ( + <> +
+ + + + +
+ + + ); +} \ No newline at end of file diff --git a/src/components/Tables/RepoTable.tsx b/src/components/Tables/RepoTable.tsx new file mode 100644 index 0000000..3266137 --- /dev/null +++ b/src/components/Tables/RepoTable.tsx @@ -0,0 +1,80 @@ +import { useState } from "react"; + +export default function RepoTable({ repos }: any) { + const [sortKey, setSortKey] = useState("stars"); + + const sorted = [...repos].sort((a, b) => { + if (sortKey === "stars") return b.stargazers_count - a.stargazers_count; + if (sortKey === "forks") return b.forks_count - a.forks_count; + return 0; + }); + + return ( +
+ + {/* HEADER */} +
+

+ Repository List +

+ + {/* SORT BUTTONS */} +
+ + + +
+
+ + {/* TABLE */} +
+ + + + + + + + + + + + {sorted.map((r: any) => ( + + + + + + ))} + + +
RepositoryStarsForks
{r.name} + {r.stargazers_count} + + {r.forks_count} +
+
+
+ ); +} \ No newline at end of file diff --git a/src/layout/DashboardLayout.tsx b/src/layout/DashboardLayout.tsx new file mode 100644 index 0000000..67e9def --- /dev/null +++ b/src/layout/DashboardLayout.tsx @@ -0,0 +1,23 @@ +import Sidebar from "./Sidebar"; +import Topbar from "./Topbar"; + +export default function DashboardLayout({ children, orgInput }: any) { + return ( +
+ + {/* Sidebar */} + + + {/* Main */} +
+ + + +
+ {children} +
+ +
+
+ ); +} \ No newline at end of file diff --git a/src/layout/Sidebar.tsx b/src/layout/Sidebar.tsx new file mode 100644 index 0000000..451c955 --- /dev/null +++ b/src/layout/Sidebar.tsx @@ -0,0 +1,58 @@ + +import { Link, useLocation } from "react-router-dom"; + + +export default function Sidebar() { + + const location = useLocation(); + + return ( +
+ +
+
+ Logo +
+ + +
+ +
+ ⚙ Settings +
+ +
+ ); +} \ No newline at end of file diff --git a/src/layout/Topbar.tsx b/src/layout/Topbar.tsx new file mode 100644 index 0000000..9e7f7ac --- /dev/null +++ b/src/layout/Topbar.tsx @@ -0,0 +1,64 @@ +import { useEffect, useState } from "react"; + +export default function Topbar({ orgInput }: any) { + const [orgName, setOrgName] = useState(""); + const [logo, setLogo] = useState(""); + const [debouncedInput, setDebouncedInput] = useState(""); + + // Debounce + useEffect(() => { + const timer = setTimeout(() => { + setDebouncedInput(orgInput); + }, 500); // 500ms delay + + return () => clearTimeout(timer); + }, [orgInput]); + + // API call only after debounce + useEffect(() => { + if (!debouncedInput) return; + + const orgs = debouncedInput.split(",").map((o: string) => o.trim()); + + // name update + setOrgName(orgs.join(" + ")); + + // fetch only first org logo + fetch(`https://api.github.com/orgs/${orgs[0]}`) + .then((res) => { + if (!res.ok) { + throw new Error("Org not found"); + } + return res.json(); + }) + .then((data) => { + setLogo(data.avatar_url); + }) + .catch((err) => { + console.error("Error fetching org:", err); + setLogo(""); + }); + + }, [debouncedInput]); + + return ( +
+ + {/* LEFT */} +
+ {logo ? ( + + ) : ( +
+ )} + +

+ {orgName || "OrgExplorer"} +

+
+
+ ); +} \ No newline at end of file diff --git a/src/pages/GraphPage.tsx b/src/pages/GraphPage.tsx new file mode 100644 index 0000000..f8463ea --- /dev/null +++ b/src/pages/GraphPage.tsx @@ -0,0 +1,32 @@ +import NetworkGraph from "../components/Graph/NetworkGraph"; + +export default function GraphPage() { + const repos = JSON.parse(localStorage.getItem("repos") || "[]"); + + if (!repos.length) { + return ( +
+ No data found 🚫
+ Please analyze organizations first. +
+ ); + } + + return ( +
+ {/*

+ Contributor Network Graph +

*/} + +

+ Contributor Collaboration Network 🌐 +

+ + + +

+ Visualizes relationships between repositories and contributors across multiple organizations. +

+
+ ); +} \ No newline at end of file diff --git a/src/pages/Overview.tsx b/src/pages/Overview.tsx new file mode 100644 index 0000000..a7edd7f --- /dev/null +++ b/src/pages/Overview.tsx @@ -0,0 +1,119 @@ +import { useState, useEffect } from "react"; +import OrgInput from "../components/Input/OrgInput"; +import InsightPanel from "../components/Insights/Insightpanel"; +import ActivityChart from "../components/Charts/ActivityChart"; +import { fetchOrgRepos } from "../services/githubService"; +import { mergeRepos } from "../utils/mergeOrgs"; +import { getInsights } from "../utils/insightEngine"; +import { exportCSV } from "../utils/exportCSV"; +import HealthScore from "../components/HealthScore"; +import { calculateOrgHealthScore } from "../utils/calculateScore"; +import type { Repo, Insight } from "../types/github"; +import { generateInsights } from "../utils/insights"; + + +// PROPS TYPE +type Props = { + orgInput: string; + setOrgInput: React.Dispatch>; +}; + +export default function Overview({ orgInput, setOrgInput }: Props) { + + const [repos, setRepos] = useState([]); + const [data, setData] = useState(null); + const [loading, setLoading] = useState(false); + + const { score, label } = calculateOrgHealthScore(repos); + + // RESTORE DATA + useEffect(() => { + const savedRepos = localStorage.getItem("repos"); + const savedInput = localStorage.getItem("orgInput"); + + if (savedRepos) { + const parsed: Repo[] = JSON.parse(savedRepos); + setRepos(parsed); + setData(getInsights(parsed)); + } + + if (savedInput) { + setOrgInput(savedInput); // sync with topbar + } + }, [setOrgInput]); + + // ANALYZE + const handleSubmit = async (input: string) => { + setLoading(true); + try { + setOrgInput(input); // GLOBAL UPDATE + + const orgs = input.split(",").map(o => o.trim()); + + const results = await Promise.all(orgs.map(fetchOrgRepos)); + const merged: Repo[] = mergeRepos(results); + + console.log("TOTAL REPOS:", merged.length); + + setRepos(merged); + + localStorage.setItem("repos", JSON.stringify(merged)); + localStorage.setItem("orgInput", input); + + const insights: Insight = getInsights(merged); + setData(insights); + + } catch (e) { + console.error(e); + } + + setLoading(false); + }; + + const insights = generateInsights(repos); + + return ( +
+ + {/* INPUT */} + + + {/* LOADING */} + {loading &&

Loading...

} + + {data && } + +
+

+ Insights +

+ +
    + {insights.map((insight, i) => ( +
  • • {insight}
  • + ))} +
+
+ +
+ +
+ + + {/* CHART + EXPORT */} + {repos.length > 0 && ( +
+ + {/* CHART */} + + +
+ )} + +
+ ); +} diff --git a/src/pages/Repositories.tsx b/src/pages/Repositories.tsx new file mode 100644 index 0000000..7fa2352 --- /dev/null +++ b/src/pages/Repositories.tsx @@ -0,0 +1,32 @@ +import RepoTable from "../components/Tables/RepoTable"; +import { exportCSV } from "../utils/exportCSV"; + +export default function Repositories() { + const repos = JSON.parse(localStorage.getItem("repos") || "[]"); + + return ( + <> +
+

+ Repositories +

+ + {!repos.length ? ( +

+ No data found. Please go to Dashboard and analyze org. +

+ ) : ( + + )} +
+ {/* EXPORT */} + + + + ); +} \ No newline at end of file diff --git a/src/services/githubService.ts b/src/services/githubService.ts new file mode 100644 index 0000000..287508d --- /dev/null +++ b/src/services/githubService.ts @@ -0,0 +1,25 @@ +const BASE = "https://api.github.com"; + +const TOKEN = import.meta.env.VITE_GITHUB_TOKEN; + +export const fetchOrgRepos = async (org: string) => { + const res = await fetch(`${BASE}/orgs/${org}/repos?per_page=100`, { + headers: { + Authorization: `token ${TOKEN}` + } + }); + + if (!res.ok) throw new Error("API Error"); + return res.json(); +}; + +export const fetchRepoContributors = async (url: string) => { + const res = await fetch(url, { + headers: { + Authorization: `token ${TOKEN}` + } + }); + + if (!res.ok) throw new Error("Contributor API Error"); + return res.json(); +}; \ No newline at end of file diff --git a/src/types/github.ts b/src/types/github.ts new file mode 100644 index 0000000..e4bac02 --- /dev/null +++ b/src/types/github.ts @@ -0,0 +1,16 @@ +export interface Repo { + id: number; + name: string; + stargazers_count: number; + forks_count: number; + updated_at: string; +} + +export interface Insight { + totalRepos: number; + totalStars: number; + totalForks: number; + inactivePercent: string; + topRepos : Repo[]; + insights: string[]; +} \ No newline at end of file diff --git a/src/utils/calculateScore.ts b/src/utils/calculateScore.ts new file mode 100644 index 0000000..ccc87bb --- /dev/null +++ b/src/utils/calculateScore.ts @@ -0,0 +1,48 @@ +export const calculateOrgHealthScore = (repos: any[]) => { + if (!repos || repos.length === 0) return { score: 0, label: "No Data" }; + + const totalRepos = repos.length; + + const activeRepos = repos.filter(repo => { + const days = + (Date.now() - new Date(repo.pushed_at).getTime()) / + (1000 * 60 * 60 * 24); + return days < 30; + }).length; + + const avgStars = + repos.reduce((sum, r) => sum + r.stargazers_count, 0) / totalRepos; + + const avgForks = + repos.reduce((sum, r) => sum + r.forks_count, 0) / totalRepos; + + const staleRepos = repos.filter(repo => { + const days = + (Date.now() - new Date(repo.pushed_at).getTime()) / + (1000 * 60 * 60 * 24); + return days > 180; + }).length; + + let score = 0; + + // Activity score (40) + score += (activeRepos / totalRepos) * 40; + + // Popularity score (30) + score += Math.min(avgStars, 100) * 0.3; + + // Engagement score (20) + score += Math.min(avgForks, 50) * 0.4; + + // Penalty (10) + score -= (staleRepos / totalRepos) * 10; + + score = Math.round(score); + + let label = "Poor"; + if (score > 75) label = "Excellent 🚀"; + else if (score > 50) label = "Good 👍"; + else if (score > 30) label = "Average ⚠️"; + + return { score, label }; +}; \ No newline at end of file diff --git a/src/utils/exportCSV.ts b/src/utils/exportCSV.ts new file mode 100644 index 0000000..573190b --- /dev/null +++ b/src/utils/exportCSV.ts @@ -0,0 +1,15 @@ +export const exportCSV = (repos: any[]) => { + const rows = repos.map(r => + `${r.name},${r.stargazers_count},${r.forks_count}` + ); + + const csv = "Name,Stars,Forks\n" + rows.join("\n"); + + const blob = new Blob([csv]); + const url = URL.createObjectURL(blob); + + const a = document.createElement("a"); + a.href = url; + a.download = "repos.csv"; + a.click(); +}; \ No newline at end of file diff --git a/src/utils/insightEngine.ts b/src/utils/insightEngine.ts new file mode 100644 index 0000000..3848389 --- /dev/null +++ b/src/utils/insightEngine.ts @@ -0,0 +1,90 @@ +import type { Repo, Insight } from "../types/github"; + +export interface InsightResult extends Insight { + insights: string[]; +} + +export const getInsights = (repos: Repo[]): InsightResult => { + const now = new Date(); + + const inactive = repos.filter(r => + (now.getTime() - new Date(r.updated_at).getTime()) > + 90 * 24 * 60 * 60 * 1000 + ); + + const totalStars = repos.reduce((sum, r) => sum + r.stargazers_count, 0); + const totalForks = repos.reduce((s, r) => s + r.forks_count, 0); + + const topRepos = [...repos] + .sort((a, b) => b.stargazers_count - a.stargazers_count) + .slice(0, 5); + + const inactivePercent = (inactive.length / repos.length) * 100; + + // NEW: INSIGHT GENERATION + const insights: string[] = []; + + // Inactive repos insight + if (inactivePercent > 50) { + insights.push("⚠ More than 50% repositories are inactive → maintenance risk"); + } else if (inactivePercent > 30) { + insights.push("⚠ Significant number of repositories are inactive"); + } else { + insights.push("✅ Most repositories are actively maintained"); + } + + // Star concentration insight + if (topRepos.length > 0) { + const topStar = topRepos[0].stargazers_count; + + if (topStar > totalStars * 0.5) { + insights.push("⚡ One repository dominates more than 50% of total stars"); + } else if (topStar > totalStars * 0.3) { + insights.push("⚡ A few repositories dominate the ecosystem"); + } + } + + // Fork vs Star ratio insight + if (totalStars > 0) { + const ratio = totalForks / totalStars; + + if (ratio > 0.6) { + insights.push("🔁 High fork-to-star ratio → strong developer engagement"); + } else if (ratio < 0.2) { + insights.push("📉 Low fork activity compared to stars"); + } + } + + // Recently active repos insight + const recent = repos.filter(r => + (now.getTime() - new Date(r.updated_at).getTime()) < + 30 * 24 * 60 * 60 * 1000 + ); + + if (recent.length > repos.length * 0.5) { + insights.push("🚀 High recent activity across repositories"); + } else if (recent.length < repos.length * 0.2) { + insights.push("🐢 Low recent activity → possible slowdown"); + } + + // Repo size distribution insight + const lowStarRepos = repos.filter(r => r.stargazers_count < 10); + + if (lowStarRepos.length > repos.length * 0.6) { + insights.push("📦 Majority of repositories have low visibility (<10 stars)"); + } + + // Growth potential insight + if (totalStars > 1000 && inactivePercent < 30) { + insights.push("📈 Organization shows strong growth potential"); + } + + return { + totalRepos: repos.length, + totalStars, + totalForks, + inactivePercent: inactivePercent.toFixed(1), + topRepos, + insights + }; +}; \ No newline at end of file diff --git a/src/utils/insights.ts b/src/utils/insights.ts new file mode 100644 index 0000000..2565c00 --- /dev/null +++ b/src/utils/insights.ts @@ -0,0 +1,63 @@ +export function generateInsights(repos: any[]) { + if (!repos || repos.length === 0) return []; + + const insights: string[] = []; + + // Most starred repo + const topRepo = [...repos].sort( + (a, b) => b.stargazers_count - a.stargazers_count + )[0]; + + insights.push(`⭐ Most popular repo: ${topRepo.name}`); + + // Low activity repos + const lowActivity = repos.filter((r) => r.stargazers_count < 5); + if (lowActivity.length > 0) { + insights.push(`⚠️ ${lowActivity.length} repos have very low stars`); + } + + // Fork heavy repos + const forkHeavy = repos.filter((r) => r.forks_count > r.stargazers_count); + if (forkHeavy.length > 0) { + insights.push(`🍴 Some repos are fork-heavy but not popular`); + } + + // Recently updated + const recent = repos.filter((r) => { + const updated = new Date(r.updated_at); + const now = new Date(); + return (now.getTime() - updated.getTime()) / (1000 * 60 * 60 * 24) < 30; + }); + + if (recent.length > repos.length / 2) { + insights.push(`🚀 Org is actively maintained (many recent updates)`); + } + + // Stale repos + const stale = repos.filter((r) => { + const updated = new Date(r.updated_at); + const now = new Date(); + return (now.getTime() - updated.getTime()) / (1000 * 60 * 60 * 24) > 180; + }); + + if (stale.length > 0) { + insights.push(`💀 ${stale.length} repos are stale (>6 months no updates)`); + } + + // Language dominance + const langMap: any = {}; + repos.forEach((r) => { + if (!r.language) return; + langMap[r.language] = (langMap[r.language] || 0) + 1; + }); + + const topLang = Object.keys(langMap).sort( + (a, b) => langMap[b] - langMap[a] + )[0]; + + if (topLang) { + insights.push(`💻 Most used language: ${topLang}`); + } + + return insights; +} \ No newline at end of file diff --git a/src/utils/mergeOrgs.ts b/src/utils/mergeOrgs.ts new file mode 100644 index 0000000..cb1e0a7 --- /dev/null +++ b/src/utils/mergeOrgs.ts @@ -0,0 +1,13 @@ +import type { Repo } from "../types/github"; + +export const mergeRepos = (allRepos: Repo[][]): Repo[] => { + const map = new Map(); + + allRepos.flat().forEach(repo => { + if (!map.has(repo.id)) { + map.set(repo.id, repo); + } + }); + + return Array.from(map.values()); +}; \ No newline at end of file From 35316fc4b6e20f81440b12f9b19848f916938d1c Mon Sep 17 00:00:00 2001 From: yanamavani Date: Thu, 2 Apr 2026 21:03:28 +0530 Subject: [PATCH 2/5] fix graph layout --- package-lock.json | 2057 +++++++++++++++++++--- package.json | 9 +- src/App.css | 52 + src/App.tsx | 34 +- src/components/Graph/NetworkGraph.tsx | 170 +- src/components/Insights/Insightpanel.tsx | 2 +- src/index.css | 10 + src/layout/DashboardLayout.tsx | 2 +- src/layout/Topbar.tsx | 76 +- src/main.tsx | 19 +- src/pages/GraphPage.tsx | 7 +- src/pages/Overview.tsx | 3 +- vite.config.ts | 4 +- 13 files changed, 2085 insertions(+), 360 deletions(-) diff --git a/package-lock.json b/package-lock.json index ab0e168..63b60a4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,21 @@ { - "name": "orgexplorer", + "name": "OrgExplorer", "version": "0.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "orgexplorer", + "name": "OrgExplorer", "version": "0.0.0", "dependencies": { + "@tailwindcss/vite": "^4.2.2", + "axios": "^1.14.0", + "d3-force": "^3.0.0", "react": "^19.2.0", - "react-dom": "^19.2.0" + "react-dom": "^19.2.0", + "react-force-graph-2d": "^1.29.1", + "react-router-dom": "^7.13.2", + "recharts": "^3.8.1" }, "devDependencies": { "@eslint/js": "^9.39.1", @@ -21,6 +27,7 @@ "eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-refresh": "^0.4.24", "globals": "^16.5.0", + "tailwindcss": "^4.2.2", "typescript": "~5.9.3", "typescript-eslint": "^8.46.4", "vite": "npm:rolldown-vite@7.2.5" @@ -312,7 +319,6 @@ "version": "1.8.1", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.8.1.tgz", "integrity": "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -324,7 +330,6 @@ "version": "1.8.1", "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz", "integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -335,7 +340,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz", "integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -555,7 +559,6 @@ "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", @@ -566,7 +569,6 @@ "version": "2.3.5", "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", @@ -577,7 +579,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.0.0" @@ -587,14 +588,12 @@ "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.31", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -605,7 +604,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz", "integrity": "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -622,7 +620,6 @@ "version": "0.97.0", "resolved": "https://registry.npmjs.org/@oxc-project/runtime/-/runtime-0.97.0.tgz", "integrity": "sha512-yH0zw7z+jEws4dZ4IUKoix5Lh3yhqIJWF9Dc8PWvhpo7U7O+lJrv7ZZL4BeRO0la8LBQFwcCewtLBnVV7hPe/w==", - "dev": true, "license": "MIT", "engines": { "node": "^20.19.0 || >=22.12.0" @@ -632,12 +629,47 @@ "version": "0.97.0", "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.97.0.tgz", "integrity": "sha512-lxmZK4xFrdvU0yZiDwgVQTCvh2gHWBJCBk5ALsrtsBWhs0uDIi+FTOnXRQeQfs304imdvTdaakT/lqwQ8hkOXQ==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/Boshen" } }, + "node_modules/@reduxjs/toolkit": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.11.2.tgz", + "integrity": "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@standard-schema/utils": "^0.3.0", + "immer": "^11.0.0", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, + "node_modules/@reduxjs/toolkit/node_modules/immer": { + "version": "11.1.4", + "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.4.tgz", + "integrity": "sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/@rolldown/binding-android-arm64": { "version": "1.0.0-beta.50", "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-beta.50.tgz", @@ -645,7 +677,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -662,7 +693,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -679,7 +709,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -696,7 +725,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -713,7 +741,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -730,7 +757,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -747,7 +773,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -764,7 +789,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -781,7 +805,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -798,7 +821,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -815,7 +837,6 @@ "cpu": [ "wasm32" ], - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -832,7 +853,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -849,7 +869,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -866,7 +885,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -883,214 +901,806 @@ "dev": true, "license": "MIT" }, - "node_modules/@tybys/wasm-util": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", - "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@types/babel__core": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", - "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7", - "@types/babel__generator": "*", - "@types/babel__template": "*", - "@types/babel__traverse": "*" - } - }, - "node_modules/@types/babel__generator": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", - "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__template": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", - "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__traverse": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", - "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.28.2" - } - }, - "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", "license": "MIT" }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", "license": "MIT" }, - "node_modules/@types/node": { - "version": "24.10.9", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.9.tgz", - "integrity": "sha512-ne4A0IpG3+2ETuREInjPNhUGis1SFjv1d5asp8MzEAGtOZeTeHVDOYqOgqfhvseqg/iXty2hjBf1zAOb7RNiNw==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~7.16.0" - } - }, - "node_modules/@types/react": { - "version": "19.2.10", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.10.tgz", - "integrity": "sha512-WPigyYuGhgZ/cTPRXB2EwUw+XvsRA3GqHlsP4qteqrnnjDrApbS7MxcGr/hke5iUoeB7E/gQtrs9I37zAJ0Vjw==", - "dev": true, + "node_modules/@tailwindcss/node": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.2.tgz", + "integrity": "sha512-pXS+wJ2gZpVXqFaUEjojq7jzMpTGf8rU6ipJz5ovJV6PUGmlJ+jvIwGrzdHdQ80Sg+wmQxUFuoW1UAAwHNEdFA==", "license": "MIT", "dependencies": { - "csstype": "^3.2.2" - } - }, - "node_modules/@types/react-dom": { - "version": "19.2.3", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", - "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "@types/react": "^19.2.0" - } - }, - "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.54.0.tgz", - "integrity": "sha512-hAAP5io/7csFStuOmR782YmTthKBJ9ND3WVL60hcOjvtGFb+HJxH4O5huAcmcZ9v9G8P+JETiZ/G1B8MALnWZQ==", - "dev": true, - "license": "MIT", + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.19.0", + "jiti": "^2.6.1", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.2.2" + } + }, + "node_modules/@tailwindcss/node/node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "license": "MPL-2.0", "dependencies": { - "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.54.0", - "@typescript-eslint/type-utils": "8.54.0", - "@typescript-eslint/utils": "8.54.0", - "@typescript-eslint/visitor-keys": "8.54.0", - "ignore": "^7.0.5", - "natural-compare": "^1.4.0", - "ts-api-utils": "^2.4.0" + "detect-libc": "^2.0.3" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">= 12.0.0" }, "funding": { "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^8.54.0", - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", - "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/@typescript-eslint/parser": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.54.0.tgz", - "integrity": "sha512-BtE0k6cjwjLZoZixN0t5AKP0kSzlGu7FctRXYuPAm//aaiZhmfq1JwdYpYr1brzEspYyFeF+8XF5j2VK6oalrA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/scope-manager": "8.54.0", - "@typescript-eslint/types": "8.54.0", - "@typescript-eslint/typescript-estree": "8.54.0", - "@typescript-eslint/visitor-keys": "8.54.0", - "debug": "^4.4.3" + "url": "https://opencollective.com/parcel" }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/@tailwindcss/node/node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">= 12.0.0" }, "funding": { "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <6.0.0" + "url": "https://opencollective.com/parcel" } }, - "node_modules/@typescript-eslint/project-service": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.54.0.tgz", - "integrity": "sha512-YPf+rvJ1s7MyiWM4uTRhE4DvBXrEV+d8oC3P9Y2eT7S+HBS0clybdMIPnhiATi9vZOYDc7OQ1L/i6ga6NFYK/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.54.0", - "@typescript-eslint/types": "^8.54.0", - "debug": "^4.4.3" - }, + "node_modules/@tailwindcss/node/node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">= 12.0.0" }, "funding": { "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" + "url": "https://opencollective.com/parcel" } }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.54.0.tgz", - "integrity": "sha512-27rYVQku26j/PbHYcVfRPonmOlVI6gihHtXFbTdB5sb6qA0wdAQAbyXFVarQ5t4HRojIz64IV90YtsjQSSGlQg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.54.0", - "@typescript-eslint/visitor-keys": "8.54.0" - }, + "node_modules/@tailwindcss/node/node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": ">= 12.0.0" }, "funding": { "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "url": "https://opencollective.com/parcel" } }, - "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.54.0.tgz", - "integrity": "sha512-dRgOyT2hPk/JwxNMZDsIXDgyl9axdJI3ogZ2XWhBPsnZUv+hPesa5iuhdYt2gzwA9t8RE5ytOJ6xB0moV0Ujvw==", + "node_modules/@tailwindcss/node/node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@tailwindcss/node/node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@tailwindcss/node/node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@tailwindcss/node/node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@tailwindcss/node/node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@tailwindcss/node/node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@tailwindcss/node/node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@tailwindcss/node/node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.2.tgz", + "integrity": "sha512-qEUA07+E5kehxYp9BVMpq9E8vnJuBHfJEC0vPC5e7iL/hw7HR61aDKoVoKzrG+QKp56vhNZe4qwkRmMC0zDLvg==", + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.2.2", + "@tailwindcss/oxide-darwin-arm64": "4.2.2", + "@tailwindcss/oxide-darwin-x64": "4.2.2", + "@tailwindcss/oxide-freebsd-x64": "4.2.2", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.2", + "@tailwindcss/oxide-linux-arm64-gnu": "4.2.2", + "@tailwindcss/oxide-linux-arm64-musl": "4.2.2", + "@tailwindcss/oxide-linux-x64-gnu": "4.2.2", + "@tailwindcss/oxide-linux-x64-musl": "4.2.2", + "@tailwindcss/oxide-wasm32-wasi": "4.2.2", + "@tailwindcss/oxide-win32-arm64-msvc": "4.2.2", + "@tailwindcss/oxide-win32-x64-msvc": "4.2.2" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.2.tgz", + "integrity": "sha512-dXGR1n+P3B6748jZO/SvHZq7qBOqqzQ+yFrXpoOWWALWndF9MoSKAT3Q0fYgAzYzGhxNYOoysRvYlpixRBBoDg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.2.tgz", + "integrity": "sha512-iq9Qjr6knfMpZHj55/37ouZeykwbDqF21gPFtfnhCCKGDcPI/21FKC9XdMO/XyBM7qKORx6UIhGgg6jLl7BZlg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.2.tgz", + "integrity": "sha512-BlR+2c3nzc8f2G639LpL89YY4bdcIdUmiOOkv2GQv4/4M0vJlpXEa0JXNHhCHU7VWOKWT/CjqHdTP8aUuDJkuw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.2.tgz", + "integrity": "sha512-YUqUgrGMSu2CDO82hzlQ5qSb5xmx3RUrke/QgnoEx7KvmRJHQuZHZmZTLSuuHwFf0DJPybFMXMYf+WJdxHy/nQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.2.tgz", + "integrity": "sha512-FPdhvsW6g06T9BWT0qTwiVZYE2WIFo2dY5aCSpjG/S/u1tby+wXoslXS0kl3/KXnULlLr1E3NPRRw0g7t2kgaQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.2.tgz", + "integrity": "sha512-4og1V+ftEPXGttOO7eCmW7VICmzzJWgMx+QXAJRAhjrSjumCwWqMfkDrNu1LXEQzNAwz28NCUpucgQPrR4S2yw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.2.tgz", + "integrity": "sha512-oCfG/mS+/+XRlwNjnsNLVwnMWYH7tn/kYPsNPh+JSOMlnt93mYNCKHYzylRhI51X+TbR+ufNhhKKzm6QkqX8ag==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.2.tgz", + "integrity": "sha512-rTAGAkDgqbXHNp/xW0iugLVmX62wOp2PoE39BTCGKjv3Iocf6AFbRP/wZT/kuCxC9QBh9Pu8XPkv/zCZB2mcMg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.2.tgz", + "integrity": "sha512-XW3t3qwbIwiSyRCggeO2zxe3KWaEbM0/kW9e8+0XpBgyKU4ATYzcVSMKteZJ1iukJ3HgHBjbg9P5YPRCVUxlnQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.2.tgz", + "integrity": "sha512-eKSztKsmEsn1O5lJ4ZAfyn41NfG7vzCg496YiGtMDV86jz1q/irhms5O0VrY6ZwTUkFy/EKG3RfWgxSI3VbZ8Q==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.8.1", + "@emnapi/runtime": "^1.8.1", + "@emnapi/wasi-threads": "^1.1.0", + "@napi-rs/wasm-runtime": "^1.1.1", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.2.tgz", + "integrity": "sha512-qPmaQM4iKu5mxpsrWZMOZRgZv1tOZpUm+zdhhQP0VhJfyGGO3aUKdbh3gDZc/dPLQwW4eSqWGrrcWNBZWUWaXQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.2.tgz", + "integrity": "sha512-1T/37VvI7WyH66b+vqHj/cLwnCxt7Qt3WFu5Q8hk65aOvlwAhs7rAp1VkulBJw/N4tMirXjVnylTR72uI0HGcA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.2.2.tgz", + "integrity": "sha512-mEiF5HO1QqCLXoNEfXVA1Tzo+cYsrqV7w9Juj2wdUFyW07JRenqMG225MvPwr3ZD9N1bFQj46X7r33iHxLUW0w==", + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.2.2", + "@tailwindcss/oxide": "4.2.2", + "tailwindcss": "4.2.2" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7 || ^8" + } + }, + "node_modules/@tweenjs/tween.js": { + "version": "25.0.0", + "resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-25.0.0.tgz", + "integrity": "sha512-XKLA6syeBUaPzx4j3qwMqzzq+V4uo72BnlbOjmuljLrRqdsd3qnzvZZoxvMHZ23ndsRS4aufU6JOZYpCbU6T1A==", + "license": "MIT" + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-shape": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz", + "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.10.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.9.tgz", + "integrity": "sha512-ne4A0IpG3+2ETuREInjPNhUGis1SFjv1d5asp8MzEAGtOZeTeHVDOYqOgqfhvseqg/iXty2hjBf1zAOb7RNiNw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.10", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.10.tgz", + "integrity": "sha512-WPigyYuGhgZ/cTPRXB2EwUw+XvsRA3GqHlsP4qteqrnnjDrApbS7MxcGr/hke5iUoeB7E/gQtrs9I37zAJ0Vjw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.54.0.tgz", + "integrity": "sha512-hAAP5io/7csFStuOmR782YmTthKBJ9ND3WVL60hcOjvtGFb+HJxH4O5huAcmcZ9v9G8P+JETiZ/G1B8MALnWZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.54.0", + "@typescript-eslint/type-utils": "8.54.0", + "@typescript-eslint/utils": "8.54.0", + "@typescript-eslint/visitor-keys": "8.54.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.54.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.54.0.tgz", + "integrity": "sha512-BtE0k6cjwjLZoZixN0t5AKP0kSzlGu7FctRXYuPAm//aaiZhmfq1JwdYpYr1brzEspYyFeF+8XF5j2VK6oalrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.54.0", + "@typescript-eslint/types": "8.54.0", + "@typescript-eslint/typescript-estree": "8.54.0", + "@typescript-eslint/visitor-keys": "8.54.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.54.0.tgz", + "integrity": "sha512-YPf+rvJ1s7MyiWM4uTRhE4DvBXrEV+d8oC3P9Y2eT7S+HBS0clybdMIPnhiATi9vZOYDc7OQ1L/i6ga6NFYK/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.54.0", + "@typescript-eslint/types": "^8.54.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.54.0.tgz", + "integrity": "sha512-27rYVQku26j/PbHYcVfRPonmOlVI6gihHtXFbTdB5sb6qA0wdAQAbyXFVarQ5t4HRojIz64IV90YtsjQSSGlQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.54.0", + "@typescript-eslint/visitor-keys": "8.54.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.54.0.tgz", + "integrity": "sha512-dRgOyT2hPk/JwxNMZDsIXDgyl9axdJI3ogZ2XWhBPsnZUv+hPesa5iuhdYt2gzwA9t8RE5ytOJ6xB0moV0Ujvw==", "dev": true, "license": "MIT", "engines": { @@ -1273,6 +1883,15 @@ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, + "node_modules/accessor-fn": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/accessor-fn/-/accessor-fn-1.5.3.tgz", + "integrity": "sha512-rkAofCwe/FvYFUlMB0v0gWmhqtfAtV1IUkdPbfhTUyYniu5LrC0A0UJkTH0Jv3S8SvwkmfuAlY+mQIJATdocMA==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/acorn": { "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", @@ -1336,6 +1955,23 @@ "dev": true, "license": "Python-2.0" }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.14.0.tgz", + "integrity": "sha512-3Y8yrqLSwjuzpXuZ0oIYZ/XGgLwUIBU3uLvbcpb0pidD9ctpShJd43KSlEEkVQg6DS0G9NKyzOvBfUtDKEyHvQ==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", + "proxy-from-env": "^2.1.0" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -1353,6 +1989,16 @@ "baseline-browser-mapping": "dist/cli.js" } }, + "node_modules/bezier-js": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/bezier-js/-/bezier-js-6.1.4.tgz", + "integrity": "sha512-PA0FW9ZpcHbojUCMu28z9Vg/fNkwTj5YhusSAjHHDfHDGLxJ6YUKrAN2vk1fP2MMOxVw4Oko16FMlRGVBGqLKg==", + "license": "MIT", + "funding": { + "type": "individual", + "url": "https://github.com/Pomax/bezierjs/blob/master/FUNDING.md" + } + }, "node_modules/brace-expansion": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", @@ -1398,6 +2044,19 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -1429,6 +2088,18 @@ ], "license": "CC-BY-4.0" }, + "node_modules/canvas-color-tracker": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/canvas-color-tracker/-/canvas-color-tracker-1.3.2.tgz", + "integrity": "sha512-ryQkDX26yJ3CXzb3hxUVNlg1NKE4REc5crLBq661Nxzr8TNd236SaEf2ffYLXyI5tSABSeguHLqcVq4vf9L3Zg==", + "license": "MIT", + "dependencies": { + "tinycolor2": "^1.6.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -1446,6 +2117,15 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -1466,6 +2146,18 @@ "dev": true, "license": "MIT" }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -1480,6 +2172,19 @@ "dev": true, "license": "MIT" }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -1499,9 +2204,260 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "dev": true, + "devOptional": true, + "license": "MIT" + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-binarytree": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/d3-binarytree/-/d3-binarytree-1.0.2.tgz", + "integrity": "sha512-cElUNH+sHu95L04m92pG73t2MEJXKu+GeKUN1TJkFsu93E5W8E9Sc3kHEGJKgenGvj19m6upSn2EunvMgMD2Yw==", + "license": "MIT" + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-force": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", + "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-force-3d": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/d3-force-3d/-/d3-force-3d-3.0.6.tgz", + "integrity": "sha512-4tsKHUPLOVkyfEffZo1v6sFHvGFwAIIjt/W8IThbp08DYAsXZck+2pSHEG5W1+gQgEvFLdZkYvmJAbRM2EzMnA==", + "license": "MIT", + "dependencies": { + "d3-binarytree": "1", + "d3-dispatch": "1 - 3", + "d3-octree": "1", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-octree": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/d3-octree/-/d3-octree-1.1.0.tgz", + "integrity": "sha512-F8gPlqpP+HwRPMO/8uOu5wjH110+6q4cgJvgJT6vlpy3BEaDIKlTZrgHKZSp/i1InRpVfh4puY/kvL6MxK930A==", "license": "MIT" }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-quadtree": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", + "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-interpolate": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -1520,6 +2476,12 @@ } } }, + "node_modules/decimal.js-light": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", + "license": "MIT" + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -1527,16 +2489,38 @@ "dev": true, "license": "MIT" }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "dev": true, "license": "Apache-2.0", "engines": { "node": ">=8" } }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/electron-to-chromium": { "version": "1.5.283", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.283.tgz", @@ -1544,6 +2528,74 @@ "dev": true, "license": "ISC" }, + "node_modules/enhanced-resolve": { + "version": "5.20.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.1.tgz", + "integrity": "sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-toolkit": { + "version": "1.45.1", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.45.1.tgz", + "integrity": "sha512-/jhoOj/Fx+A+IIyDNOvO3TItGmlMKhtX8ISAHKE90c4b/k1tqaqEZ+uUqfpU8DMnW5cgNJv606zS55jGvza0Xw==", + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks" + ] + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -1751,6 +2803,12 @@ "node": ">=0.10.0" } }, + "node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "license": "MIT" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -1776,7 +2834,6 @@ "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, "license": "MIT", "engines": { "node": ">=12.0.0" @@ -1841,11 +2898,86 @@ "dev": true, "license": "ISC" }, + "node_modules/float-tooltip": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/float-tooltip/-/float-tooltip-1.7.5.tgz", + "integrity": "sha512-/kXzuDnnBqyyWyhDMH7+PfP8J/oXiAavGzcRxASOMRHFuReDtofizLLJsf7nnDLAfEaMW4pVWaXrAjtnglpEkg==", + "license": "MIT", + "dependencies": { + "d3-selection": "2 - 3", + "kapsule": "^1.16", + "preact": "10" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/force-graph": { + "version": "1.51.2", + "resolved": "https://registry.npmjs.org/force-graph/-/force-graph-1.51.2.tgz", + "integrity": "sha512-zZNdMqx8qIQGurgnbgYIUsdXxSfvhfRSIdncsKGv/twUOZpwCsk9hPHmdjdcme1+epATgb41G0rkIGHJ0Wydng==", + "license": "MIT", + "dependencies": { + "@tweenjs/tween.js": "18 - 25", + "accessor-fn": "1", + "bezier-js": "3 - 6", + "canvas-color-tracker": "^1.3", + "d3-array": "1 - 3", + "d3-drag": "2 - 3", + "d3-force-3d": "2 - 3", + "d3-scale": "1 - 4", + "d3-scale-chromatic": "1 - 3", + "d3-selection": "2 - 3", + "d3-zoom": "2 - 3", + "float-tooltip": "^1.7", + "index-array-by": "1", + "kapsule": "^1.16", + "lodash-es": "4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -1856,6 +2988,15 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -1866,6 +3007,43 @@ "node": ">=6.9.0" } }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -1892,6 +3070,24 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -1902,6 +3098,45 @@ "node": ">=8" } }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/hermes-estree": { "version": "0.25.1", "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", @@ -1929,6 +3164,16 @@ "node": ">= 4" } }, + "node_modules/immer": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz", + "integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/import-fresh": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", @@ -1956,6 +3201,24 @@ "node": ">=0.8.19" } }, + "node_modules/index-array-by": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/index-array-by/-/index-array-by-1.4.2.tgz", + "integrity": "sha512-SP23P27OUKzXWEC/TOyWlwLviofQkCSCKONnc62eItjp69yCZZPqDQtr3Pw5gJDnPeUMqExmKydNZaJO0FU9pw==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -1986,11 +3249,28 @@ "dev": true, "license": "ISC" }, + "node_modules/jerrypick": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/jerrypick/-/jerrypick-1.1.2.tgz", + "integrity": "sha512-YKnxXEekXKzhpf7CLYA0A+oDP8V0OhICNCr5lv96FvSsDEmrb0GKM776JgQvHTMjr7DTTPEVv/1Ciaw0uEWzBA==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true, "license": "MIT" }, "node_modules/js-yaml": { @@ -2053,6 +3333,18 @@ "node": ">=6" } }, + "node_modules/kapsule": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/kapsule/-/kapsule-1.16.3.tgz", + "integrity": "sha512-4+5mNNf4vZDSwPhKprKwz3330iisPrb08JyMgbsdFrimBCKNHecua/WBwvVg3n7vwx0C1ARjfhwIpbrbd9n5wg==", + "license": "MIT", + "dependencies": { + "lodash-es": "4" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -2081,7 +3373,6 @@ "version": "1.31.1", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.31.1.tgz", "integrity": "sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ==", - "dev": true, "license": "MPL-2.0", "dependencies": { "detect-libc": "^2.0.3" @@ -2114,7 +3405,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -2135,7 +3425,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -2156,7 +3445,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -2177,7 +3465,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -2198,7 +3485,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -2219,7 +3505,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -2240,7 +3525,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -2261,7 +3545,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -2282,7 +3565,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -2303,7 +3585,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -2324,7 +3605,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -2354,6 +3634,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash-es": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.23.tgz", + "integrity": "sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==", + "license": "MIT" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -2361,6 +3647,18 @@ "dev": true, "license": "MIT" }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -2371,6 +3669,45 @@ "yallist": "^3.0.2" } }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -2395,7 +3732,6 @@ "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "dev": true, "funding": [ { "type": "github", @@ -2424,6 +3760,15 @@ "dev": true, "license": "MIT" }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -2511,14 +3856,12 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, "license": "ISC" }, "node_modules/picomatch": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -2531,7 +3874,6 @@ "version": "8.5.6", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", - "dev": true, "funding": [ { "type": "opencollective", @@ -2556,6 +3898,16 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/preact": { + "version": "10.29.0", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.29.0.tgz", + "integrity": "sha512-wSAGyk2bYR1c7t3SZ3jHcM6xy0lcBcDel6lODcs9ME6Th++Dx2KU+6D3HD8wMMKGA8Wpw7OMd3/4RGzYRpzwRg==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -2566,6 +3918,32 @@ "node": ">= 0.8.0" } }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, + "node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -2597,6 +3975,68 @@ "react": "^19.2.4" } }, + "node_modules/react-force-graph-2d": { + "version": "1.29.1", + "resolved": "https://registry.npmjs.org/react-force-graph-2d/-/react-force-graph-2d-1.29.1.tgz", + "integrity": "sha512-1Rl/1Z3xy2iTHKj6a0jRXGyiI86xUti81K+jBQZ+Oe46csaMikp47L5AjrzA9hY9fNGD63X8ffrqnvaORukCuQ==", + "license": "MIT", + "dependencies": { + "force-graph": "^1.51", + "prop-types": "15", + "react-kapsule": "^2.5" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "react": "*" + } + }, + "node_modules/react-is": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.4.tgz", + "integrity": "sha512-W+EWGn2v0ApPKgKKCy/7s7WHXkboGcsrXE+2joLyVxkbyVQfO3MUEaUQDHoSmb8TFFrSKYa9mw64WZHNHSDzYA==", + "license": "MIT", + "peer": true + }, + "node_modules/react-kapsule": { + "version": "2.5.7", + "resolved": "https://registry.npmjs.org/react-kapsule/-/react-kapsule-2.5.7.tgz", + "integrity": "sha512-kifAF4ZPD77qZKc4CKLmozq6GY1sBzPEJTIJb0wWFK6HsePJatK3jXplZn2eeAt3x67CDozgi7/rO8fNQ/AL7A==", + "license": "MIT", + "dependencies": { + "jerrypick": "^1.1.1" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "react": ">=16.13.1" + } + }, + "node_modules/react-redux": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", + "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", + "license": "MIT", + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, "node_modules/react-refresh": { "version": "0.18.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", @@ -2607,6 +4047,95 @@ "node": ">=0.10.0" } }, + "node_modules/react-router": { + "version": "7.13.2", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.13.2.tgz", + "integrity": "sha512-tX1Aee+ArlKQP+NIUd7SE6Li+CiGKwQtbS+FfRxPX6Pe4vHOo6nr9d++u5cwg+Z8K/x8tP+7qLmujDtfrAoUJA==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.13.2", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.13.2.tgz", + "integrity": "sha512-aR7SUORwTqAW0JDeiWF07e9SBE9qGpByR9I8kJT5h/FrBKxPMS6TiC7rmVO+gC0q52Bx7JnjWe8Z1sR9faN4YA==", + "license": "MIT", + "dependencies": { + "react-router": "7.13.2" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/recharts": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.8.1.tgz", + "integrity": "sha512-mwzmO1s9sFL0TduUpwndxCUNoXsBw3u3E/0+A+cLcrSfQitSG62L32N69GhqUrrT5qKcAE3pCGVINC6pqkBBQg==", + "license": "MIT", + "workspaces": [ + "www" + ], + "dependencies": { + "@reduxjs/toolkit": "^1.9.0 || 2.x.x", + "clsx": "^2.1.1", + "decimal.js-light": "^2.5.1", + "es-toolkit": "^1.39.3", + "eventemitter3": "^5.0.1", + "immer": "^10.1.1", + "react-redux": "8.x.x || 9.x.x", + "reselect": "5.1.1", + "tiny-invariant": "^1.3.3", + "use-sync-external-store": "^1.2.2", + "victory-vendor": "^37.0.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "license": "MIT" + }, + "node_modules/redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", + "license": "MIT", + "peerDependencies": { + "redux": "^5.0.0" + } + }, + "node_modules/reselect": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", + "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==", + "license": "MIT" + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -2621,7 +4150,6 @@ "version": "1.0.0-beta.50", "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-beta.50.tgz", "integrity": "sha512-JFULvCNl/anKn99eKjOSEubi0lLmNqQDAjyEMME2T4CwezUDL0i6t1O9xZsu2OMehPnV2caNefWpGF+8TnzB6A==", - "dev": true, "license": "MIT", "dependencies": { "@oxc-project/types": "=0.97.0", @@ -2654,7 +4182,6 @@ "version": "1.0.0-beta.50", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.50.tgz", "integrity": "sha512-5e76wQiQVeL1ICOZVUg4LSOVYg9jyhGCin+icYozhsUzM+fHE7kddi1bdiE0jwVqTfkjba3jUFbEkoC9WkdvyA==", - "dev": true, "license": "MIT" }, "node_modules/scheduler": { @@ -2673,6 +4200,12 @@ "semver": "bin/semver.js" } }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -2700,7 +4233,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -2732,11 +4264,41 @@ "node": ">=8" } }, + "node_modules/tailwindcss": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.2.tgz", + "integrity": "sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q==", + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.2.tgz", + "integrity": "sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, + "node_modules/tinycolor2": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz", + "integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==", + "license": "MIT" + }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", - "dev": true, "license": "MIT", "dependencies": { "fdir": "^6.5.0", @@ -2766,7 +4328,6 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, "license": "0BSD", "optional": true }, @@ -2825,7 +4386,7 @@ "version": "7.16.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/update-browserslist-db": { @@ -2869,12 +4430,42 @@ "punycode": "^2.1.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/victory-vendor": { + "version": "37.3.6", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz", + "integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==", + "license": "MIT AND ISC", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, "node_modules/vite": { "name": "rolldown-vite", "version": "7.2.5", "resolved": "https://registry.npmjs.org/rolldown-vite/-/rolldown-vite-7.2.5.tgz", "integrity": "sha512-u09tdk/huMiN8xwoiBbig197jKdCamQTtOruSalOzbqGje3jdHiV0njQlAW0YvzoahkirFePNQ4RYlfnRQpXZA==", - "dev": true, "license": "MIT", "dependencies": { "@oxc-project/runtime": "0.97.0", diff --git a/package.json b/package.json index d75669c..230c781 100644 --- a/package.json +++ b/package.json @@ -10,8 +10,14 @@ "preview": "vite preview" }, "dependencies": { + "@tailwindcss/vite": "^4.2.2", + "axios": "^1.14.0", + "d3-force": "^3.0.0", "react": "^19.2.0", - "react-dom": "^19.2.0" + "react-dom": "^19.2.0", + "react-force-graph-2d": "^1.29.1", + "react-router-dom": "^7.13.2", + "recharts": "^3.8.1" }, "devDependencies": { "@eslint/js": "^9.39.1", @@ -23,6 +29,7 @@ "eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-refresh": "^0.4.24", "globals": "^16.5.0", + "tailwindcss": "^4.2.2", "typescript": "~5.9.3", "typescript-eslint": "^8.46.4", "vite": "npm:rolldown-vite@7.2.5" diff --git a/src/App.css b/src/App.css index 027945e..3d4e1e9 100644 --- a/src/App.css +++ b/src/App.css @@ -1,6 +1,58 @@ +/* #root { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} */ +/* @import "tailwindcss"; */ #root { max-width: 1280px; margin: 0 auto; padding: 2rem; text-align: center; +} + +body { + background: #0f172a; + color: #e2e8f0; + font-family: "Inter", sans-serif; +} + +.chart-container { + margin-top: 30px; + padding: 20px; + border-radius: 16px; + background: rgba(255, 255, 255, 0.05); + backdrop-filter: blur(10px); + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); +} + +h1 { + font-size: 32px; + margin-bottom: 20px; +} + +h2 { + margin-bottom: 10px; +} + +table { + width: 100%; + border-collapse: collapse; + margin-top: 20px; +} + +th, td { + padding: 10px; + border-bottom: 1px solid #444; + text-align: left; +} + +th { + cursor: pointer; + color: #38bdf8; +} + +tr:hover { + background: rgba(255,255,255,0.05); } \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx index 0a3deb1..9b6a970 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,12 +1,30 @@ -import './App.css' +import { useState } from "react"; +import { Routes, Route } from "react-router-dom"; -function App() { +import DashboardLayout from "./layout/DashboardLayout"; +import Overview from "./pages/Overview"; +import Repositories from "./pages/Repositories"; +import GraphPage from "./pages/GraphPage"; + +export default function App() { + const [orgInput, setOrgInput] = useState(""); return ( - <> -

Hello, OrgExplorer!

- - ) -} + + + + } + /> -export default App + } /> + } /> + + + ); +} \ No newline at end of file diff --git a/src/components/Graph/NetworkGraph.tsx b/src/components/Graph/NetworkGraph.tsx index e416c6c..5db2136 100644 --- a/src/components/Graph/NetworkGraph.tsx +++ b/src/components/Graph/NetworkGraph.tsx @@ -1,27 +1,25 @@ import ForceGraph2D from "react-force-graph-2d"; import { useEffect, useState, useRef } from "react"; import { fetchRepoContributors } from "../../services/githubService"; -import * as d3 from "d3-force"; export default function NetworkGraph({ repos }: any) { + const [graphData, setGraphData] = useState({ nodes: [], links: [] }); - const fgRef = useRef(null); - - // IMAGE CACHE - const imageCache = useRef<{ [key: string]: HTMLImageElement }>({}); + const fgRef = useRef(null); // FIX - // BUILD GRAPH useEffect(() => { const buildGraph = async () => { const nodes: any[] = []; const links: any[] = []; - const addedUsers = new Set(); + const userMap = new Map(); - for (let repo of repos.slice(0, 6)) { + for (let repo of repos.slice(0, 10)) { + + // Repo node nodes.push({ id: repo.name, type: "repo", @@ -31,23 +29,38 @@ export default function NetworkGraph({ repos }: any) { try { const contributors = await fetchRepoContributors(repo.contributors_url); - contributors.slice(0, 5).forEach((c: any) => { - if (!addedUsers.has(c.login)) { - addedUsers.add(c.login); + contributors.slice(0, 10).forEach((c: any) => { + + // USER NODE (unique) + if (!userMap.has(c.login)) { + userMap.set(c.login, true); nodes.push({ id: c.login, type: "user", - contributions: c.contributions, - avatar: c.avatar_url + img: c.avatar_url, + contributions: c.contributions }); } + //LINK repo-user links.push({ source: c.login, target: repo.name, weight: c.contributions }); + + // USER ↔ USER CONNECTION (dense graph) + contributors.slice(0, 5).forEach((other: any) => { + if (other.login !== c.login) { + links.push({ + source: c.login, + target: other.login, + weight: 1 + }); + } + }); + }); } catch (e) { @@ -61,151 +74,98 @@ export default function NetworkGraph({ repos }: any) { if (repos.length) buildGraph(); }, [repos]); - // FORCE SETTINGS useEffect(() => { - if (!fgRef.current) return; + if (!fgRef.current) return; - fgRef.current.d3Force("center", d3.forceCenter(0, 0)); - fgRef.current.d3Force("charge", d3.forceManyBody().strength(-100)); - fgRef.current.d3Force("link", d3.forceLink().distance(70)); + const fg = fgRef.current; - fgRef.current.d3Force( - "radial", - d3.forceRadial((node: any) => { - return node.type === "repo" ? 80 : 180; - }).strength(0.8) - ); + // Charge force (node spread) + fg.d3Force("charge").strength(-120); - }, [graphData]); + // Link distance + fg.d3Force("link").distance(80); - useEffect(() => { - if (!fgRef.current) return; - - // trigger render after mount - setTimeout(() => { - fgRef.current.zoomToFit(400); - }, 300); -}, [graphData]); + // Centering + fg.d3Force("center", null); + }, [graphData]); return ( -
+
{ - const limit = 200; + // ONLY ZOOM allowed + enableZoomInteraction={true} - if(Math.abs(x) > limit || Math.abs(y) > limit) { - fgRef.current.centerAt(0, 0, 400); - } - }} - // AUTO CENTER + FREEZE + // REMOVE AUTO MOVE + cooldownTicks={0} + + // STOP physics after load onEngineStop={() => { - fgRef.current.centerAt(0, 0, 400); fgRef.current?.zoomToFit(400); - - graphData.nodes.forEach((node: any) => { - node.fx = node.x; - node.fy = node.y; - }); - - fgRef.current?.pauseAnimation(); }} - d3VelocityDecay={0.3} - d3AlphaDecay={0.02} - cooldownTicks={100} - warmupTicks={100} + d3VelocityDecay={0.9} // fast stop + d3AlphaDecay={0.1} // instant stable - // TOOLTIP nodeLabel={(node: any) => node.type === "repo" - ? `📦 ${node.id} (⭐ ${node.stars})` - : `👤 ${node.id} (${node.contributions})` + ? `📦 ${node.id}` + : `👤 ${node.id}` } - // NODE RENDER (FIXED AVATAR) + // NODE DRAW (DP + NAME) nodeCanvasObject={(node: any, ctx, globalScale) => { const size = node.type === "repo" - ? 10 + Math.log(node.stars + 1) - : 5; - - const fontSize = 9 / globalScale; + ? 10 + : 6; - if (node.type === "user" && node.avatar) { - let img = imageCache.current[node.avatar]; + const fontSize = 10 / globalScale; - if (!img) { - img = new Image(); - img.src = node.avatar; - imageCache.current[node.avatar] = img; - } + if (node.type === "user" && node.img) { + const img = new Image(); + img.src = node.img; ctx.save(); ctx.beginPath(); ctx.arc(node.x, node.y, size, 0, 2 * Math.PI); ctx.closePath(); ctx.clip(); - - if (img.complete) { - ctx.drawImage( - img, - node.x - size, - node.y - size, - size * 2, - size * 2 - ); - } - + ctx.drawImage(img, node.x - size, node.y - size, size * 2, size * 2); ctx.restore(); - - // white border - ctx.beginPath(); - ctx.arc(node.x, node.y, size, 0, 2 * Math.PI); - ctx.strokeStyle = "#ffffff"; - ctx.lineWidth = 1; - ctx.stroke(); - } else { - // repo node ctx.beginPath(); ctx.arc(node.x, node.y, size, 0, 2 * Math.PI); ctx.fillStyle = "#22c55e"; ctx.fill(); } - // label + // 🏷 NAME ctx.font = `${fontSize}px Inter`; ctx.fillStyle = "#e2e8f0"; - ctx.fillText(node.id, node.x + size + 2, node.y + size / 2); + ctx.fillText(node.id, node.x + size + 2, node.y); }} - // EDGE DESIGN (VISIBLE) - linkWidth={(link: any) => - Math.max(1, Math.log(link.weight + 1)) - } + // 🔗 LINKS + linkWidth={(link: any) => Math.log(link.weight + 1)} - linkColor={() => "rgba(200,200,200,0.6)"} + linkColor={() => "#22c55e"} linkDirectionalParticles={1} linkDirectionalParticleSpeed={0.002} - - // HOVER - onNodeHover={(node) => { - document.body.style.cursor = node ? "pointer" : "default"; - }} + linkDirectionalParticleWidth={1} + linkDirectionalParticleColor={() => "#22c55e"} />
); diff --git a/src/components/Insights/Insightpanel.tsx b/src/components/Insights/Insightpanel.tsx index 283671d..71043ff 100644 --- a/src/components/Insights/Insightpanel.tsx +++ b/src/components/Insights/Insightpanel.tsx @@ -1,4 +1,4 @@ -import type { Insight } from "../../types/github"; + import StatCard from "../Cards/statCard"; export default function InsightPanel({ data }: { data: any }) { diff --git a/src/index.css b/src/index.css index e0dbee4..20f3253 100644 --- a/src/index.css +++ b/src/index.css @@ -1,3 +1,13 @@ +@import "tailwindcss"; + +@tailwind base; +@tailwind components; +@tailwind utilities; + +body { + @apply bg-slate-900 text-white; +} + :root { font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; line-height: 1.5; diff --git a/src/layout/DashboardLayout.tsx b/src/layout/DashboardLayout.tsx index 67e9def..8353d79 100644 --- a/src/layout/DashboardLayout.tsx +++ b/src/layout/DashboardLayout.tsx @@ -13,7 +13,7 @@ export default function DashboardLayout({ children, orgInput }: any) { -
+
{children}
diff --git a/src/layout/Topbar.tsx b/src/layout/Topbar.tsx index 9e7f7ac..d8095f1 100644 --- a/src/layout/Topbar.tsx +++ b/src/layout/Topbar.tsx @@ -61,4 +61,78 @@ export default function Topbar({ orgInput }: any) {
); -} \ No newline at end of file +} + +// import { useEffect, useState } from "react"; + +// export default function Topbar({ orgInput }: any) { +// const [orgName, setOrgName] = useState(""); +// const [logo, setLogo] = useState(""); +// const [debouncedInput, setDebouncedInput] = useState(""); + +// // ✅ STEP 1: Debounce (IMPORTANT) +// useEffect(() => { +// const timer = setTimeout(() => { +// setDebouncedInput(orgInput); +// }, 500); // 500ms delay + +// return () => clearTimeout(timer); +// }, [orgInput]); + +// // ✅ STEP 2: API call only after debounce +// useEffect(() => { +// if (!debouncedInput) return; + +// const orgs = debouncedInput.split(",").map((o: string) => o.trim()); + +// // 👉 name update +// setOrgName(orgs.join(" + ")); + +// // 👉 fetch only first org logo +// fetch(`https://api.github.com/orgs/${orgs[0]}`) +// .then((res) => { +// if (!res.ok) { +// throw new Error("Org not found"); +// } +// return res.json(); +// }) +// .then((data) => { +// setLogo(data.avatar_url); +// }) +// .catch((err) => { +// console.error("Error fetching org:", err); +// setLogo(""); // reset if error +// }); + +// }, [debouncedInput]); + +// return ( +//
+ +// {/* LEFT */} +//
+// {logo ? ( +// +// ) : ( +//
+// )} + +//

+// {orgName || "OrgExplorer"} +//

+//
+ +// {/* RIGHT */} +//
+// 🔔 +//
+//
+// Tom Cook +//
+//
+//
+// ); +// } diff --git a/src/main.tsx b/src/main.tsx index bef5202..735710d 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,10 +1,25 @@ +// import { StrictMode } from 'react' +// import { createRoot } from 'react-dom/client' +// import './index.css' +// import App from './App.tsx' + +// createRoot(document.getElementById('root')!).render( +// +// +// , +// ) + import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' import './index.css' import App from './App.tsx' +import { BrowserRouter } from "react-router-dom"; createRoot(document.getElementById('root')!).render( - + + + + , -) +) \ No newline at end of file diff --git a/src/pages/GraphPage.tsx b/src/pages/GraphPage.tsx index f8463ea..129df77 100644 --- a/src/pages/GraphPage.tsx +++ b/src/pages/GraphPage.tsx @@ -14,11 +14,8 @@ export default function GraphPage() { return (
- {/*

- Contributor Network Graph -

*/} -

+

Contributor Collaboration Network 🌐

@@ -29,4 +26,4 @@ export default function GraphPage() {

); -} \ No newline at end of file +} diff --git a/src/pages/Overview.tsx b/src/pages/Overview.tsx index a7edd7f..fbcb747 100644 --- a/src/pages/Overview.tsx +++ b/src/pages/Overview.tsx @@ -5,7 +5,6 @@ import ActivityChart from "../components/Charts/ActivityChart"; import { fetchOrgRepos } from "../services/githubService"; import { mergeRepos } from "../utils/mergeOrgs"; import { getInsights } from "../utils/insightEngine"; -import { exportCSV } from "../utils/exportCSV"; import HealthScore from "../components/HealthScore"; import { calculateOrgHealthScore } from "../utils/calculateScore"; import type { Repo, Insight } from "../types/github"; @@ -99,7 +98,7 @@ export default function Overview({ orgInput, setOrgInput }: Props) { -
+
diff --git a/vite.config.ts b/vite.config.ts index 8b0f57b..2d12e47 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,7 +1,9 @@ import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' +import tailwindcss from '@tailwindcss/vite' // https://vite.dev/config/ export default defineConfig({ - plugins: [react()], + plugins: [ tailwindcss(), + react()], }) From 94a446ca289c8a7af5b84ccfc1346ce44f3f369e Mon Sep 17 00:00:00 2001 From: yanamavani Date: Mon, 20 Apr 2026 04:41:15 +0530 Subject: [PATCH 3/5] Added PR analytics dashboard --- note.txt | 2836 ++++++++++++++++++++++ note2.txt | 297 +++ package-lock.json | 10 + package.json | 1 + src/App.tsx | 9 +- src/components/Charts/ActivityChart.tsx | 390 ++- src/components/Graph/NetworkGraph.tsx | 599 ++++- src/components/Input/OrgInput.tsx | 7 +- src/components/Insights/Insightpanel.tsx | 45 +- src/components/Tables/RepoTable.tsx | 207 +- src/components/TopContributors.tsx | 122 + src/layout/DashboardLayout.tsx | 6 +- src/layout/Topbar.tsx | 137 +- src/main.tsx | 11 - src/pages/ContributorDetail.tsx | 508 ++++ src/pages/GraphPage.tsx | 8 +- src/pages/Overview.tsx | 77 +- src/pages/RepoDetails.tsx | 182 ++ src/pages/Repositories.tsx | 14 +- src/services/githubService.ts | 30 +- src/utils/calculateScore.ts | 6 +- src/utils/exportCSV.ts | 54 +- src/utils/insightEngine.ts | 8 +- src/utils/insights.ts | 126 +- 24 files changed, 5162 insertions(+), 528 deletions(-) create mode 100644 note.txt create mode 100644 note2.txt create mode 100644 src/components/TopContributors.tsx create mode 100644 src/pages/ContributorDetail.tsx create mode 100644 src/pages/RepoDetails.tsx diff --git a/note.txt b/note.txt new file mode 100644 index 0000000..b943938 --- /dev/null +++ b/note.txt @@ -0,0 +1,2836 @@ +components/Cards/statCard.tsx = +interface Props { + title: string; + value: string | number; +} + +export default function StatCard({ title, value }: Props) { + return ( +
+

{title}

+

{value}

+
+ ); +} +components/Charts/ActivityChart.tsx = + + +import { useEffect, useState } from "react"; +import { + LineChart, + Line, + XAxis, + YAxis, + Tooltip, + Legend, + ResponsiveContainer, + CartesianGrid, +} from "recharts"; + +let timeout: any; +const TOKEN = import.meta.env.VITE_GITHUB_TOKEN; + +export default function ActivityChart({ orgs }: { orgs: string[] }) { + const [data, setData] = useState([]); + const [filter, setFilter] = useState("30"); + + const getDays = () => { + return filter === "7" ? 7 : 30; + }; + + // useEffect(() => { + // if (!orgs || orgs.length === 0) return; + // fetchData(); + // }, [orgs, filter]); + useEffect(() => { + if (!orgs || orgs.length === 0) return; + + clearTimeout(timeout); + + timeout = setTimeout(() => { + fetchData(); + }, 800); // wait 0.8 sec + + }, [filter, orgs]); + + const fetchData = async () => { + try { + const days = getDays(); + const now = new Date(); + + // 🔥 PARALLEL FETCH (FAST) + const requests = orgs.flatMap(org => [ + fetch(`https://api.github.com/search/issues?q=org:${org}+type:pr&per_page=100`, { + headers: { Authorization: `token ${TOKEN}` } + }).then(res => res.json()), + + fetch(`https://api.github.com/search/issues?q=org:${org}+type:issue&per_page=100`, { + headers: { Authorization: `token ${TOKEN}` } + }).then(res => res.json()) + ]); + + const results = await Promise.all(requests); + + let allPRs: any[] = []; + let allIssues: any[] = []; + + // 🔥 SPLIT RESULTS + // results.forEach((res, i) => { + // if (i % 2 === 0) { + // if (Array.isArray(res.items)) allPRs.push(...res.items); + // } else { + // if (Array.isArray(res.items)) allIssues.push(...res.items); + // } + // }); + results.forEach((res, i) => { + if (res && res.items && Array.isArray(res.items)) { + if (i % 2 === 0) { + allPRs.push(...res.items); + } else { + allIssues.push(...res.items); + } + } else { + console.warn("Invalid org or API error", res); + } + }); + const chartMap: any = {}; + + // PRs + allPRs.forEach((pr: any) => { + const date = new Date(pr.created_at); + const diff = (now.getTime() - date.getTime()) / (1000 * 60 * 60 * 24); + + if (diff <= days) { + const key = date.toLocaleDateString("en-CA"); // YYYY-MM-DD format + + if (!chartMap[key]) { + chartMap[key] = { + date: key, + prCreated: 0, + prMerged: 0, + issuesCreated: 0, + issuesClosed: 0 + }; + } + + chartMap[key].prCreated++; + + if (pr.state === "closed") { + chartMap[key].prMerged++; + } + } + }); + + // Issues + allIssues.forEach((issue: any) => { + const date = new Date(issue.created_at); + const diff = (now.getTime() - date.getTime()) / (1000 * 60 * 60 * 24); + + if (diff <= days) { + // const key = date.toISOString().split("T")[0]; + const key = date.toLocaleDateString("en-CA"); + + if (!chartMap[key]) { + chartMap[key] = { + date: key, + prCreated: 0, + prMerged: 0, + issuesCreated: 0, + issuesClosed: 0 + }; + } + + chartMap[key].issuesCreated++; + + if (issue.state === "closed") { + chartMap[key].issuesClosed++; + } + } + }); + + // const finalData = Object.values(chartMap).sort( + // (a: any, b: any) => + // new Date(a.date).getTime() - new Date(b.date).getTime() + // ); + + // setData(finalData); + const result: any[] = []; + + for (let i = days; i >= 0; i--) { + const d = new Date(); + d.setDate(d.getDate() - i); + + // const key = d.toISOString().split("T")[0]; + const key = d.toLocaleDateString("en-CA"); + + result.push({ + date: key, + prCreated: chartMap[key]?.prCreated || 0, + prMerged: chartMap[key]?.prMerged || 0, + issuesCreated: chartMap[key]?.issuesCreated || 0, + issuesClosed: chartMap[key]?.issuesClosed || 0 + }); + } + + setData(result); + + } catch (err) { + console.error(err); + } + }; + + const formatDate = (date: string) => { + const d = new Date(date); + return d.toLocaleDateString("en-US", { + month: "short", + day: "numeric", + }); + }; + const orgCount = orgs.length; + return ( +
+ + {/* FILTER */} +
+ {/* + + */} + + + + + {/* */} +
+ + {/* CHART TITLE */} + {/*

+ 📊 Combined Activity (All Organizations) +

*/} +

+ {orgCount > 1 + ? `📊 Combined Activity (${orgCount} Organizations)` + : `📊 Activity (${orgs[0] || "Organization"})`} +

+ {/* AREA CHART */} + + + + + + + + + + + + + + + + + + {/* */} + + + + + {/* SIMPLE CLEAN LINES */} + + + + + + + + + + + +
+ ); +} +============================================================================================= +components/Graph/NetworkGraph.tsx = +import ForceGraph2D from "react-force-graph-2d"; +import { useEffect, useRef, useState } from "react"; +import { fetchRepoContributors } from "../../services/githubService"; +import { exportCSV } from "../../utils/exportCSV"; +import { GoPeople, GoRepo, GoGitBranch, GoLink } from "react-icons/go"; +import { FaLink } from "react-icons/fa"; +import { IoPeople } from "react-icons/io5"; + + +type NodeType = { + id: string; + type: "repo" | "user"; + label: string; + img?: string; + + stars?: number; + forks?: number; + issues?: number; + + contributions?: number; + activity?: number; + + size?: number; + org?: string; +}; + +type LinkType = { + source: string; + target: string; + weight: number; +}; + +export default function NetworkGraph({ repos }: any) { + const fgRef = useRef(null); + + const [graphData, setGraphData] = useState<{ + nodes: NodeType[]; + links: LinkType[]; + }>({ nodes: [], links: [] }); + + const [selectedNode, setSelectedNode] = useState(null); + const [stats, setStats] = useState({ + users: 0, + repos: 0, + edges: 0, + }); + const [hoverNode, setHoverNode] = useState(null); + const [focusNode, setFocusNode] = useState(null); + + /* ───────── BUILD GRAPH (MULTI ORG FIX) ───────── */ + useEffect(() => { + if (!repos?.length) return; + + const build = async () => { + const nodes: NodeType[] = []; + const links: LinkType[] = []; + const userMap = new Map(); + + // GROUP BY ORG + const orgGrouped: Record = {}; + + repos.forEach((repo: any) => { + const org = repo.full_name.split("/")[0]; + if (!orgGrouped[org]) orgGrouped[org] = []; + orgGrouped[org].push(repo); + }); + + // TAKE 4 REPOS PER ORG + const selectedRepos: any[] = []; + Object.values(orgGrouped).forEach((list: any[]) => { + selectedRepos.push(...list.slice(0, 4)); + }); + + for (const repo of selectedRepos) { + const org = repo.full_name.split("/")[0]; + + // REPO NODE + nodes.push({ + id: repo.full_name, + type: "repo", + label: repo.name, + org, + stars: repo.stargazers_count, + forks: repo.forks_count, + issues: repo.open_issues_count, + activity: new Date(repo.updated_at).getTime(), + size: + Math.log((repo.stargazers_count || 1) + 1) * 4 + + Math.log((repo.forks_count || 1) + 1) * 2 + + 8, + }); + + try { + const contributors = await fetchRepoContributors( + `${repo.contributors_url}?per_page=30` + ); + + contributors.slice(0, 15).forEach((c: any) => { + if (!c?.login) return; + + if (!userMap.has(c.login)) { + userMap.set(c.login, true); + + nodes.push({ + id: c.login, + type: "user", + label: c.login, + img: c.avatar_url, + contributions: c.contributions, + activity: c.contributions, + size: Math.log((c.contributions || 1) + 1) * 3 + 6, + }); + } + + links.push({ + source: c.login, + target: repo.full_name, + weight: c.contributions || 1, + }); + }); + } catch (err) { + console.log(err); + } + } + setStats({ + users: nodes.filter(n => n.type === "user").length, + repos: nodes.filter(n => n.type === "repo").length, + edges: links.length, + }); + setGraphData({ nodes, links }); + }; + + + build(); + }, [repos]); + + + /* ───────── FORCE LAYOUT (MULTI ORG FIXED) ───────── */ + useEffect(() => { + if (!fgRef.current) return; + + const fg = fgRef.current; + + fg.d3Force("charge").strength(-320); + + fg.d3Force("link").distance((l: any) => + Math.max(80, 200 - Math.log2(l.weight + 1) * 25) + ); + + // ORGS + const orgs: string[] = Array.from( + new Set( + graphData.nodes + .filter((n) => n.type === "repo" && n.org) + .map((n) => n.org as string) + ) + ); + + const orgMap: Record = {}; + const gap = 400; + + orgs.forEach((org, i) => { + orgMap[org] = (i - (orgs.length - 1) / 2) * gap; + }); + + // X FORCE + fg.d3Force("x", (node: any) => { + if (node.type === "repo") { + return node.org ? orgMap[node.org] ?? 0 : 0; + } + + const linked = graphData.links.find((l) => l.source === node.id); + + if (!linked) return 0; + + const repoNode = graphData.nodes.find( + (n) => n.id === linked.target + ) as NodeType | undefined; + + if (!repoNode || !repoNode.org) return 0; + + return orgMap[repoNode.org] ?? 0; + }); + + // Y FORCE (activity) + fg.d3Force("y", (node: any) => { + const act = node.activity || 1; + const norm = Math.min(1, Math.log(act + 1) / 10); + + return node.type === "repo" + ? -300 * norm + : 300 * (1 - norm); + }); + }, [graphData]); + + // ======================================================================================== + const isConnectedToFocus = (nodeId: string) => { + if (!focusNode) return true; + + const focusId = focusNode.id; + + return graphData.links.some((l: any) => { + const s = typeof l.source === "object" ? l.source.id : l.source; + const t = typeof l.target === "object" ? l.target.id : l.target; + + return ( + (s === nodeId && t === focusId) || + (t === nodeId && s === focusId) + ); + }); + }; + // ======================================================= + /* ───────── NODE DRAW ───────── */ + + const drawNode = (node: any, ctx: CanvasRenderingContext2D, scale: number) => { + const padding = 6; + + // dynamic width based on text + const label = node.label || ""; + ctx.font = `${9 / scale}px Arial`; + const textWidth = ctx.measureText(label).width; + + const width = Math.max(80, textWidth + 40); // auto width + const height = 52; + + const x = node.x - width / 2; + const y = node.y - height / 2; + + // helper for id (IMPORTANT FIX) + const getId = (val: any) => + typeof val === "object" ? val.id : val; + + let opacity = 1; + + // PRIORITY: FOCUS MODE + if (focusNode) { + const isConnected = isConnectedToFocus(node.id); + + if (node.id !== focusNode.id && !isConnected) { + opacity = 0.1; + } + } + + // SECOND: HOVER MODE + else if (hoverNode) { + const getId = (val: any) => + typeof val === "object" ? val.id : val; + + const isConnected = graphData.links.some((l: any) => { + const s = getId(l.source); + const t = getId(l.target); + + return ( + (s === node.id && t === hoverNode.id) || + (t === node.id && s === hoverNode.id) + ); + }); + + if (node.id !== hoverNode.id && !isConnected) { + opacity = 0.1; + } + } + + ctx.globalAlpha = opacity; + + // CARD BACKGROUND (glass style) + ctx.fillStyle = "rgba(15, 23, 42, 0.9)"; + ctx.strokeStyle = node.type === "repo" ? "#facc15" : "#22c55e"; + ctx.lineWidth = 1.5; + + ctx.beginPath(); + ctx.roundRect(x, y, width, height, 10); + ctx.fill(); + ctx.stroke(); + + // avatar + if (node.img) { + const img = new Image(); + img.src = node.img; + + ctx.save(); + ctx.beginPath(); + ctx.roundRect(x + padding, y + padding, 22, 22, 4); + ctx.clip(); + ctx.drawImage(img, x + padding, y + padding, 22, 22); + ctx.restore(); + } + + // TEXT CLIP (IMPORTANT — no overflow) + ctx.save(); + ctx.beginPath(); + ctx.rect(x + 30, y + 5, width - 35, 20); + ctx.clip(); + + ctx.fillStyle = "#e5e7eb"; + ctx.fillText(label, x + 30, y + 18); + ctx.restore(); + + // 📊 contributions / stats + if (node.contributions) { + ctx.fillStyle = "#22c55e"; + ctx.font = `${8 / scale}px Arial`; + ctx.fillText(`${node.contributions} commits`, x + 30, y + 35); + } + + if (node.type === "repo") { + ctx.fillStyle = "#facc15"; + ctx.font = `${8 / scale}px Arial`; + ctx.fillText("repo", x + 30, y + 35); + } + + ctx.globalAlpha = 1; + + if (focusNode) { + const isConnected = isConnectedToFocus(node.id); + + if (node.id !== focusNode.id && !isConnected) { + opacity = 0.1; // dim others + } + } + }; + /* ───────── LINK DRAW ───────── */ + + const topContributor = graphData.nodes + .filter(n => n.type === "user") + .sort((a, b) => (b.contributions || 0) - (a.contributions || 0))[0]; + + const mostActiveRepo = graphData.nodes + .filter(n => n.type === "repo") + .sort((a, b) => (b.activity || 0) - (a.activity || 0))[0]; + + const strongestLink = graphData.links + .sort((a, b) => b.weight - a.weight)[0]; + + const getId = (val: any) => + typeof val === "object" ? val.id : val; + + // TOTAL UNIQUE CONTRIBUTORS + const totalUniqueContributors = new Set( + graphData.nodes + .filter(n => n.type === "user") + .map(n => n.id) + ).size; + + + // SHARED CONTRIBUTORS + const contributorOrgs: Record> = {}; + + graphData.links.forEach((l: any) => { + const user = typeof l.source === "object" ? l.source.id : l.source; + const repo = graphData.nodes.find(n => n.id === l.target); + + if (!repo?.org) return; + + if (!contributorOrgs[user]) { + contributorOrgs[user] = new Set(); + } + + contributorOrgs[user].add(repo.org); + }); + + const sharedContributors = Object.values(contributorOrgs).filter( + (orgSet) => orgSet.size > 1 + ).length; + + const drawLink = (link: any, ctx: CanvasRenderingContext2D) => { + const getId = (val: any) => + typeof val === "object" ? val.id : val; + + const s = link.source; + const t = link.target; + + if (!s?.x || !t?.x) return; + + let opacity = 0.3; + + if (focusNode) { + const sourceId = getId(link.source); + const targetId = getId(link.target); + + if (sourceId === focusNode.id || targetId === focusNode.id) { + opacity = 1; + } else { + opacity = 0.05; + } + } else if (hoverNode) { + const sourceId = getId(link.source); + const targetId = getId(link.target); + + if (sourceId === hoverNode.id || targetId === hoverNode.id) { + opacity = 1; + } else { + opacity = 0.05; + } + } + + ctx.beginPath(); + ctx.moveTo(s.x, s.y); + ctx.lineTo(t.x, t.y); + + ctx.strokeStyle = `rgba(34,197,94,${opacity})`; + ctx.lineWidth = Math.max(1, Math.log2(link.weight + 1)); + + ctx.stroke(); + }; + /* ───────── UI ───────── */ + + return ( +
+ +
+ + {/* LEFT SIDE (FULL WIDTH STATS) */} +
+ +
+ {stats.users} Contributors +
+ +
+ {stats.repos} Repos +
+ +
+ {stats.edges} Links +
+ +
+ {totalUniqueContributors} Total +
+ +
+ {sharedContributors} Shared +
+ +
+ Top Contributor : {topContributor?.label} +
+ +
+ Most Active Repo : {mostActiveRepo?.label} +
+ +
+ {getId(strongestLink?.source)} → {getId(strongestLink?.target)} +
+ +
+ + {/* RIGHT SIDE BUTTON */} + + +
+ + {selectedNode && ( +
+ + + +
+ {selectedNode.img && ( + + )} +
+

{selectedNode.label}

+

+ {selectedNode.type === "user" ? "Contributor" : "Repository"} +

+
+
+ + {selectedNode.type === "user" ? ( + <> +

Commits: {selectedNode.contributions}

+ + ) : ( + <> +

⭐ Stars: {selectedNode.stars}

+

🍴 Forks: {selectedNode.forks}

+ + )} + + + Open GitHub → + +
+ + + )} + + { + const width = 100; + const height = 60; + + ctx.fillStyle = color; + ctx.fillRect( + node.x - width / 2, + node.y - height / 2, + width, + height + ); + }} + + onNodeHover={(node: any) => { + setHoverNode(node); + document.body.style.cursor = node ? "pointer" : "default"; + }} + + onNodeClick={(node: any) => { + setSelectedNode(node); + setFocusNode(node); + }} + + linkDirectionalParticles={2} + linkDirectionalParticleSpeed={0.004} + + onEngineStop={() => fgRef.current?.zoomToFit(400)} + /> +
+ ); +} + +============================================================================================ +components/Input/OrgInput.tsx == +export default function OrgInput({ onSubmit, value, setValue }: any) { + return ( +
+ setValue(e.target.value)} + /> + + +
+ ); +} +============================================================================================== +components/Insights/Insightpanel.tsx == + +import StatCard from "../Cards/statCard"; +import { IoIosStarOutline } from "react-icons/io" +import { GoRepo } from "react-icons/go"; +import { useNavigate } from "react-router-dom"; + +export default function InsightPanel({ data }: { data: any }) { + const navigate = useNavigate(); + return ( + <> +
+ + + + +
+
+

High Activity Repositories

+ +
+ + + {data.topRepos.map((repo: any, i: number) => ( +
navigate(`/repo/${repo.name}`, { state: repo })} + className="flex justify-between items-center p-2 rounded hover:bg-gray-800 transition cursor-pointer" + > + + {repo.name} + + + + {repo.stargazers_count} + +
+ ))} +
+
+ +
+

+ Key Insights +

+ +
+ {data.insights.map((insight: string, i: number) => ( +
+ {insight} +
+ ))} +
+
+ + ); +} + + + +=========================================================================== +components/Tables/RepoTable.tsx == +import { useState } from "react"; +import { FaCodeBranch } from "react-icons/fa"; +import { MdOutlineReportGmailerrorred } from "react-icons/md"; +import { useNavigate } from "react-router-dom"; +import { IoIosStarOutline } from "react-icons/io"; + +console.log("Rendering RepoTable component..."); +export default function RepoTable({ repos }: any) { + const [sortKey, setSortKey] = useState("stars"); + const [search, setSearch] = useState(""); + const [language, setLanguage] = useState("all"); + + const navigate = useNavigate(); + + // 🔥 UNIQUE LANGUAGES + const languages = [ + "all", + ...new Set(repos.map((r: any) => r.language).filter(Boolean)), + ]; + + // 🔥 FILTER + SEARCH + const filtered = repos.filter((r: any) => { + const matchSearch = + r.name.toLowerCase().includes(search.toLowerCase()) || + (r.language || "").toLowerCase().includes(search.toLowerCase()); + + const matchLang = + language === "all" || r.language === language; + + return matchSearch && matchLang; + }); + + // 🔥 SORT + const sorted = [...filtered].sort((a, b) => { + if (sortKey === "stars") return b.stargazers_count - a.stargazers_count; + if (sortKey === "forks") return b.forks_count - a.forks_count; + if (sortKey === "issues") return b.open_issues_count - a.open_issues_count; + return 0; + }); + + // 🔥 STATUS + const getStatus = (updated_at: string) => { + const days = + (Date.now() - new Date(updated_at).getTime()) / + (1000 * 60 * 60 * 24); + + return days < 30 ? "Active" : "Inactive"; + }; + + return ( +
+ + {/* 🔥 TOP BAR */} +
+ + {/* TITLE */} +

+ Repositories ({sorted.length}) +

+ + {/* SEARCH */} + setSearch(e.target.value)} + className="px-3 py-2 rounded bg-gray-800 text-white border border-gray-600" + /> + + {/* LANGUAGE FILTER */} + +
+ + {/* 🔥 SORT BUTTONS */} +
+ + + + + +
+ + {/* 🔥 TABLE */} +
+ + + + + + + + + + + + + + + + {sorted.map((r: any) => ( + navigate(`/repo/${r.name}`, { state: r })} + className="border-t border-gray-700 hover:bg-gray-800 transition cursor-pointer" + > + {/* 1. REPO */} + + + {/* 2. STARS */} + + + {/* 3. FORKS */} + + + {/* 4. ISSUES */} + + + {/* 5. LANGUAGE */} + + + {/* 6. LAST UPDATED */} + + + {/* 7. STATUS */} + + + ))} + +
Repository Stars Forks IssuesLanguageLast UpdatedStatus
+ {r.name} +
+ {r.description || "No description"} +
+
+
+ {r.stargazers_count} +
+
+
+ {r.forks_count} +
+
+
+ {r.open_issues_count || 0} +
+
+ + {r.language || "N/A"} + + + {new Date(r.updated_at).toLocaleDateString()} + + {getStatus(r.updated_at).includes("Active") ? ( + 🟢 Active + ) : ( + 🔴 Inactive + )} +
+
+
+ ); +} +========================================================= +src/components/HealthScore.tsx +export default function HealthScore({ score, label }: any) { + return ( +
+ +

+ Organization Health Score +

+ +
+ + {score}/100 + + + {label} + +
+ + {/* Progress bar */} +
+
+
+ +
+ ); +} +================================================================== +src/components/TopContributors.tsx + + +import { useEffect, useState } from "react"; +import { useNavigate } from "react-router-dom"; +import { FaTrophy } from "react-icons/fa"; + +const TOKEN = import.meta.env.VITE_GITHUB_TOKEN; + +export default function TopContributors({ repos }: any) { + const [contributors, setContributors] = useState([]); + const navigate = useNavigate(); + + useEffect(() => { + const fetchData = async () => { + const map: any = {}; + + for (let repo of repos.slice(0, 5)) { + try { + const res = await fetch(repo.contributors_url, { + headers: { + Authorization: `token ${TOKEN}` + } + }); + + const data = await res.json(); + + // ✅ ERROR HANDLE + if (!Array.isArray(data)) { + console.error("GitHub API Error:", data); + continue; + } + + data.forEach((c: any) => { + if (!map[c.login]) { + map[c.login] = { + login: c.login, + avatar: c.avatar_url, + contributions: 0, + url: c.html_url + }; + } + + map[c.login].contributions += c.contributions; + }); + + } catch (e) { + console.error("Fetch error:", e); + } + } + + const sorted = Object.values(map) + .sort((a: any, b: any) => b.contributions - a.contributions) + .slice(0, 5); + + setContributors(sorted); + }; + + if (repos.length) fetchData(); + }, [repos]); + + return ( +
+

+ + Top Contributors +

+ + {/* 🔥 Horizontal Scroll */} +
+ + {contributors.map((c, i) => ( +
+ navigate(`/contributor/${c.login}`, { state: c }) + } + className="min-w-[220px] bg-[#111827] p-4 rounded-lg cursor-pointer hover:bg-gray-800 transition border border-gray-700" + > + + +

+ #{i + 1} {c.login} +

+ +

+ {c.contributions} contributions +

+
+ ))} + +
+
+ ); +} +============================================================== +src/layout/DashboardLayout.tsx == +import Sidebar from "./Sidebar"; +import Topbar from "./Topbar"; + +export default function DashboardLayout({ children, orgInput, orgLogo }: any) { + return ( +
+ + {/* Sidebar */} + + + {/* Main */} +
+ + + +
+ {children} +
+ +
+
+ ); +} +====================================================================== +src/layout/Sidebar.tsx === + + +import { Link, useLocation } from "react-router-dom"; + + +export default function Sidebar() { + + const location = useLocation(); + + return ( +
+ +
+
+ Logo +
+ + +
+ +
+ ⚙ Settings +
+ +
+ ); +} +================================================================================== +src/layout/Topbar.tsx == + +export default function Topbar({ orgInput, logo }: any) { + + const orgs = orgInput + ? orgInput.split(",").map((o: string) => o.trim()) + : []; + + return ( +
+ +
+ + {logo ? ( + + ) : ( +
+ )} + +

+ {orgs.join(" + ") || "OrgExplorer"} +

+ +
+
+ ); +} +================================== +src/pages/RepoDetails.tsx============= +import { useLocation } from "react-router-dom"; +import { useEffect, useState } from "react"; +import { + AreaChart, + Area, + XAxis, + YAxis, + Tooltip, + ResponsiveContainer +} from "recharts"; +import { FaCodeBranch, FaGithub } from "react-icons/fa"; +import { IoIosStarOutline } from "react-icons/io"; +import { MdOutlineReportGmailerrorred } from "react-icons/md"; +; + +const TOKEN = import.meta.env.VITE_GITHUB_TOKEN; + +export default function RepoDetails() { + const { state } = useLocation(); + const repo = state; + + const [contributors, setContributors] = useState([]); + const [chartData, setChartData] = useState([]); + + useEffect(() => { + if (!repo) return; + + fetchContributors(); + fetchActivity(); + }, []); + + // 👨‍💻 CONTRIBUTORS + const fetchContributors = async () => { + const res = await fetch(repo.contributors_url, { + headers: { Authorization: `token ${TOKEN}` } + }); + const data = await res.json(); + setContributors(data.slice(0, 4)); + }; + + // 📊 ACTIVITY CHART + const fetchActivity = async () => { + const res = await fetch( + `https://api.github.com/repos/${repo.full_name}/issues?per_page=100`, + { headers: { Authorization: `token ${TOKEN}` } } + ); + + const data = await res.json(); + + const map: any = {}; + + data.forEach((item: any) => { + const d = new Date(item.created_at) + .toISOString() + .split("T")[0]; + + if (!map[d]) map[d] = { date: d, issues: 0 }; + + map[d].issues++; + }); + + const result = Object.values(map); + setChartData(result); + }; + + if (!repo) { + return

No repo data

; + } + + const formatDate = (date: string) => { + const d = new Date(date); + return d.toLocaleDateString("en-US", { + month: "short", + day: "numeric", + }); +}; + + return ( +
+ + {/* TITLE */} +

+ {repo.name} +

+ + {/* ⭐ STAT CARDS */} +
+ + {/* STARS */} +
+ +
+

Stars

+

{repo.stargazers_count}

+
+
+ + {/* FORKS */} +
+ +
+

Forks

+

{repo.forks_count}

+
+
+ + {/* ISSUES */} +
+ window.open(repo.html_url + "/issues", "_blank") + } + > + +
+

Issues

+

{repo.open_issues_count}

+
+
+ +
+ + {/* 📊 AREA CHART */} +
+

📊 Issues Activity

+ + + + + + + + + + +
+ + {/* 👨‍💻 CONTRIBUTORS */} +
+

👨‍💻 Top Contributors

+ +
+ {contributors.map((c: any) => ( + //
+
window.open(c.html_url, "_blank")} + className="flex items-center gap-3 bg-gray-800 p-3 rounded cursor-pointer hover:bg-gray-700 transition" + > + + +
+

{c.login}

+

+ Contributions: {c.contributions} +

+
+
+ ))} +
+
+ + {/* 🔗 GITHUB LINK */} + + +
+ ); +} + +==================================================================================== +src/pages/GraphPage.tsx === + +import NetworkGraph from "../components/Graph/NetworkGraph"; + +export default function GraphPage() { + const repos = JSON.parse(localStorage.getItem("repos") || "[]"); + + if (!repos.length) { + return ( +
+ No data found 🚫
+ Please analyze organizations first. +
+ ); + } + + return ( +
+ +

+ Contributor Collaboration Network 🌐 +

+ + + +

+ Visualizes relationships between repositories and contributors across multiple organizations. +

+
+ ); +} + +=================================================================== +src/pages/Overview.tsx == + +import { useState, useEffect } from "react"; +import OrgInput from "../components/Input/OrgInput"; +import InsightPanel from "../components/Insights/Insightpanel"; +import ActivityChart from "../components/Charts/ActivityChart"; +import { fetchOrgRepos } from "../services/githubService"; +import { mergeRepos } from "../utils/mergeOrgs"; +import { getInsights } from "../utils/insightEngine"; +import HealthScore from "../components/HealthScore"; +import { calculateOrgHealthScore } from "../utils/calculateScore"; +import type { Repo, Insight } from "../types/github"; +import { generateInsights } from "../utils/insights"; +import TopContributors from "../components/TopContributors"; + +type Props = { + orgInput: string; + setOrgInput: React.Dispatch>; + setOrgLogo : React.Dispatch>; +}; +export default function Overview({ orgInput, setOrgInput, setOrgLogo }: Props) { + + const [repos, setRepos] = useState([]); + const [data, setData] = useState(null); + const [loading, setLoading] = useState(false); + + const { score, label } = calculateOrgHealthScore(repos); + + // 🔥 DEFAULT LOAD + useEffect(() => { + const savedRepos = localStorage.getItem("repos"); + const savedInput = localStorage.getItem("orgInput"); + const savedLogo = localStorage.getItem("orgLogo"); + + if (!savedInput) { + handleSubmit("AOSSIE-Org"); // default + return; + } + + if (savedRepos) { + const parsed: Repo[] = JSON.parse(savedRepos); + setRepos(parsed); + setData(getInsights(parsed)); + } + + if (savedInput) { + setOrgInput(savedInput); + } + + if (savedLogo) { + setOrgLogo(savedLogo); // 🔥 GLOBAL SET + } + + }, []); + + // 🔥 ANALYZE + const handleSubmit = async (input: string) => { + setLoading(true); + + try { + setOrgInput(input); + + const orgs = input.split(",").map(o => o.trim()).filter(Boolean); + + // 🔥 FAST LOGO (NO WAIT) + fetch(`https://api.github.com/orgs/${orgs[0]}`) + .then(res => res.json()) + .then(data => { + setOrgLogo(data.avatar_url); // 🔥 GLOBAL + localStorage.setItem("orgLogo", data.avatar_url); + }) + .catch(() => setOrgLogo("")); + + // 🔥 MULTI ORG DATA + const results = await Promise.all(orgs.map(fetchOrgRepos)); + const merged: Repo[] = mergeRepos(results); + + setRepos(merged); + + localStorage.setItem("repos", JSON.stringify(merged)); + localStorage.setItem("orgInput", input); + + const insights: Insight = getInsights(merged); + setData(insights); + + } catch (e) { + console.error(e); + } + + setLoading(false); + }; + + const insights = generateInsights(repos); + + return ( +
+ + + + {loading &&

Loading...

} + + {data && } + + {/* INSIGHTS */} +
+

+ Insights +

+ +
    + {insights.map((insight, i) => ( +
  • • {insight}
  • + ))} +
+
+ +
+ +
+ + {/* CHART */} + {repos.length > 0 && ( + o.trim()).filter(Boolean)} + /> + )} + + {/* CONTRIBUTORS */} + {repos.length > 0 && ( + + )} + +
+ ); +} +======================================================================================== +src/pages/Repositories.tsx === +import { useLocation } from "react-router-dom"; +import { useEffect, useState } from "react"; +import { + AreaChart, + Area, + XAxis, + YAxis, + Tooltip, + ResponsiveContainer +} from "recharts"; +import { FaCodeBranch, FaGithub } from "react-icons/fa"; +import { IoIosStarOutline } from "react-icons/io"; +import { MdOutlineReportGmailerrorred } from "react-icons/md"; +; + +const TOKEN = import.meta.env.VITE_GITHUB_TOKEN; + +export default function RepoDetails() { + const { state } = useLocation(); + const repo = state; + + const [contributors, setContributors] = useState([]); + const [chartData, setChartData] = useState([]); + + useEffect(() => { + if (!repo) return; + + fetchContributors(); + fetchActivity(); + }, []); + + // CONTRIBUTORS + const fetchContributors = async () => { + const res = await fetch(repo.contributors_url, { + headers: { Authorization: `token ${TOKEN}` } + }); + const data = await res.json(); + setContributors(data.slice(0, 4)); + }; + + // ACTIVITY CHART + const fetchActivity = async () => { + const res = await fetch( + `https://api.github.com/repos/${repo.full_name}/issues?per_page=100`, + { headers: { Authorization: `token ${TOKEN}` } } + ); + + const data = await res.json(); + + const map: any = {}; + + data.forEach((item: any) => { + const d = new Date(item.created_at) + .toISOString() + .split("T")[0]; + + if (!map[d]) map[d] = { date: d, issues: 0 }; + + map[d].issues++; + }); + + const result = Object.values(map); + setChartData(result); + }; + + if (!repo) { + return

No repo data

; + } + + const formatDate = (date: string) => { + const d = new Date(date); + return d.toLocaleDateString("en-US", { + month: "short", + day: "numeric", + }); + }; + + return ( +
+ + {/* TITLE */} +

+ {repo.name} +

+ + {/* STAT CARDS */} +
+ + {/* STARS */} +
+ +
+

Stars

+

{repo.stargazers_count}

+
+
+ + {/* FORKS */} +
+ +
+

Forks

+

{repo.forks_count}

+
+
+ + {/* ISSUES */} +
+ window.open(repo.html_url + "/issues", "_blank") + } + > + +
+

Issues

+

{repo.open_issues_count}

+
+
+ +
+ + {/* AREA CHART */} +
+

Issues Activity

+ + + + + + + + + + +
+ + {/* CONTRIBUTORS */} +
+

Top Contributors

+ +
+ {contributors.map((c: any) => ( + +
window.open(c.html_url, "_blank")} + className="flex items-center gap-3 bg-gray-800 p-3 rounded cursor-pointer hover:bg-gray-700 transition" + > + + +
+

{c.login}

+

+ Contributions: {c.contributions} +

+
+
+ ))} +
+
+ + {/* GITHUB LINK */} + + +
+ ); +} + +=========================================================================== +src/pages/ContributorDetail.tsx + +import { useEffect, useState } from "react"; +import { useParams } from "react-router-dom"; +import { + AreaChart, Area, XAxis, YAxis, Tooltip, CartesianGrid, ResponsiveContainer, Legend +} from "recharts"; +import { FaCodeBranch } from "react-icons/fa"; + + +const TOKEN = import.meta.env.VITE_GITHUB_TOKEN; + +export default function ContributorDetail() { + const { username } = useParams(); + + const [user, setUser] = useState(null); + const [prs, setPrs] = useState([]); + const [issues, setIssues] = useState([]); + const [sortKey, setSortKey] = useState("prCreated"); + const [sortOrder, setSortOrder] = useState("desc"); + const [events, setEvents] = useState([]); + const [filter, setFilter] = useState("7"); + + useEffect(() => { + if (!username) return; + + const fetchData = async () => { + try { + // 👤 USER + const userRes = await fetch(`https://api.github.com/users/${username}`, { + headers: { Authorization: `token ${TOKEN}` } + }); + const userData = await userRes.json(); + setUser(userData); + + // 🔀 PRs + const prRes = await fetch( + `https://api.github.com/search/issues?q=author:${username}+type:pr`, + { headers: { Authorization: `token ${TOKEN}` } } + ); + const prData = await prRes.json(); + setPrs(prData.items || []); + + // 🐞 Issues + const issueRes = await fetch( + `https://api.github.com/search/issues?q=author:${username}+type:issue`, + { headers: { Authorization: `token ${TOKEN}` } } + ); + const issueData = await issueRes.json(); + setIssues(issueData.items || []); + + // 📅 EVENTS (Recent Activity) + const eventRes = await fetch( + `https://api.github.com/users/${username}/events`, + { headers: { Authorization: `token ${TOKEN}` } } + ); + const eventData = await eventRes.json(); + setEvents(eventData || []); + console.log("events", eventData); + + } catch (err) { + console.error(err); + } + }; + + fetchData(); + + + }, [username]); + + // const timeAgo = (date: string) => { + // const diff = (new Date().getTime() - new Date(date).getTime()) / 1000; + + // const days = Math.floor(diff / 86400); + // if (days > 0) return `${days} day${days > 1 ? "s" : ""} ago`; + + // const hours = Math.floor(diff / 3600); + // if (hours > 0) return `${hours} hour${hours > 1 ? "s" : ""} ago`; + + // const mins = Math.floor(diff / 60); + // return `${mins} min ago`; + // }; + + function timeAgo(date: string) { + const seconds = Math.floor((new Date().getTime() - new Date(date).getTime()) / 1000); + + const intervals: any = { + year: 31536000, + month: 2592000, + week: 604800, + day: 86400, + hour: 3600, + minute: 60, + }; + + for (let key in intervals) { + const interval = Math.floor(seconds / intervals[key]); + if (interval > 1) return `${interval} ${key}s ago`; + if (interval === 1) return `1 ${key} ago`; + } + + return "just now"; + } + + const filteredEvents = events.filter((e: any) => { + const days = filter === "7" ? 7 : 30; + const eventDate = new Date(e.created_at); + const now = new Date(); + + return (now.getTime() - eventDate.getTime()) / (1000 * 60 * 60 * 24) <= days; + }); + + if (!user) return

Loading...

; + + // 📊 CALCULATIONS + const totalPR = prs.length; + const mergedPR = prs.filter(p => p.pull_request?.merged_at).length; + const mergeRate = totalPR ? Math.round((mergedPR / totalPR) * 100) : 0; + const totalIssues = issues.length; + + const score = Math.round( + 0.5 * mergeRate + + 0.3 * Math.min(totalIssues * 5, 100) + + 0.2 * 70 + ); + + // 📁 REPO-WISE STATS + const repoStats: any = {}; + + // PR data + prs.forEach((pr: any) => { + // const repo = pr.repository_url.split("/").pop(); + const parts = pr.repository_url.split("/"); + const repo = parts[parts.length - 2] + "/" + parts[parts.length - 1]; + + if (!repoStats[repo]) { + repoStats[repo] = { + repo, + prCreated: 0, + prMerged: 0, + issuesSolved: 0, + }; + } + + repoStats[repo].prCreated++; + + if (pr.pull_request?.merged_at) { + repoStats[repo].prMerged++; + } + }); + + // Issue data + issues.forEach((issue: any) => { + // const repo = issue.repository_url.split("/").pop(); + const parts = issue.repository_url.split("/"); + const repo = parts[parts.length - 2] + "/" + parts[parts.length - 1]; + + if (!repoStats[repo]) { + repoStats[repo] = { + repo, + prCreated: 0, + prMerged: 0, + issuesSolved: 0, + }; + } + + repoStats[repo].issuesSolved++; + }); + + const repoList = Object.values(repoStats); + const sortedRepos = [...repoList].sort((a: any, b: any) => { + const valA = a[sortKey]; + const valB = b[sortKey]; + + if (sortOrder === "asc") return valA - valB; + return valB - valA; + }); + + const chartData = repoList.map((r: any) => ({ + name: r.repo.split("/")[1], // sirf repo name + created: r.prCreated, + merged: r.prMerged + })); + + + return ( +
+ + {/* 🔥 TOP SECTION */} +
+ + + +
+

{user.login}

+ +

+ ⭐ Quality Score: {score}/100 +

+ +

+ 📊 Merge Rate: {mergeRate}% +

+ +

80 ? "text-green-400" : + score > 50 ? "text-yellow-400" : + "text-red-400" + }`}> + {score > 80 ? "🟢 High Quality Contributor" : + score > 50 ? "🟡 Medium Quality" : + "🔴 Low Quality"} +

+
+
+ + {/* 📊 STATS CARDS */} +
+
+ PR +

{totalPR}

+
+ +
+

PR Merged

+

{mergedPR}

+
+ +
+

Issues Solved

+

{totalIssues}

+
+ +
+

Issues Created

+

{totalIssues}

+
+
+ +
+ +

+ PR Activity (Created vs Merged) +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + {/* 📁 REPOSITORIES TABLE */} +
+ +

+ Repository Contributions +

+ + {repoList.length === 0 ? ( +

No repository data found

+ ) : ( +
+ + + {/* HEADER */} + {/* + + + + + + + + */} + + + + + + + {/* */} + + + + + + + + + + + + {/* BODY */} + + {sortedRepos.map((r: any) => { + const mergeRate = + r.prCreated > 0 + ? Math.round((r.prMerged / r.prCreated) * 100) + : 0; + + return ( + + + + + + + + + + + + ); + })} + + +
RepoPR CreatedPR MergedMerge %Issues Solved
Repo { + setSortKey("prCreated"); + setSortOrder(sortOrder === "asc" ? "desc" : "asc"); + }} + > + PR Created + { + setSortKey("prCreated"); + setSortOrder(sortOrder === "asc" ? "desc" : "asc"); + }} + > + PR {sortKey === "prCreated" ? (sortOrder === "asc" ? "↑" : "↓") : ""} + { + setSortKey("prMerged"); + setSortOrder(sortOrder === "asc" ? "desc" : "asc"); + }} + > + PR Merged {sortKey === "prMerged" ? (sortOrder === "asc" ? "↑" : "↓") : ""} + Merge % { + setSortKey("issuesSolved"); + setSortOrder(sortOrder === "asc" ? "desc" : "asc"); + }} + > + Issues Solved {sortKey === "issuesSolved" ? (sortOrder === "asc" ? "↑" : "↓") : ""} +
+ + {r.repo} + + {r.prCreated} + {r.prMerged} + + {mergeRate}% + + {r.issuesSolved} +
+
+ )} +
+ + {/* 💡 INSIGHTS */} +
+

Insights

+ +
    + {mergeRate > 70 && ( +
  • 🟢 High merge rate indicates quality work
  • + )} + {mergeRate < 40 && ( +
  • ⚠️ Low merge rate suggests PR issues
  • + )} + {totalPR < 5 && ( +
  • ⚠️ Limited contributions
  • + )} +
+
+ + + {/* 📅 Recent Activity */} +
+
+

📅 Recent Activity

+ + {/* Filter */} + +
+ + {/* Events */} + {filteredEvents.length === 0 ? ( +

No recent activity

+ ) : ( +
+ {filteredEvents.slice(0, 10).map((e: any, index: number) => { + const isLatest = index === 0; + + let text = ""; + let link = "#"; + + // 🔀 PR Created + if (e.type === "PullRequestEvent" && e.payload.action === "opened") { + const prNumber = e.payload?.pull_request?.number || ""; + text = `🔀 Created PR #${prNumber} in ${e.repo.name}`; + link = e.payload?.pull_request?.html_url; + } + + // ✅ PR Merged + else if (e.type === "PullRequestEvent" && e.payload?.pull_request?.merged) { + const prNumber = e.payload?.pull_request?.number || ""; + text = `✅ PR #${prNumber} merged in ${e.repo.name}`; + link = e.payload?.pull_request?.html_url; + } + + // 🆕 Issue Opened + else if (e.type === "IssuesEvent" && e.payload.action === "opened") { + const issueNumber = e.payload?.issue?.number || ""; + text = `🆕 Opened issue #${issueNumber} in ${e.repo.name}`; + link = e.payload?.issue?.html_url; + } + + // 🐞 Issue Closed + else if (e.type === "IssuesEvent" && e.payload.action === "closed") { + const issueNumber = e.payload?.issue?.number || ""; + text = `🐞 Closed issue #${issueNumber} in ${e.repo.name}`; + link = e.payload?.issue?.html_url; + } + + // 📦 Push + else if (e.type === "PushEvent") { + text = `📦 Pushed code to ${e.repo.name}`; + link = `https://github.com/${e.repo.name}`; + } + + else return null; + + return ( + + {text} + + ({timeAgo(e.created_at)}) + + + ); + })} +
+ )} +
+ + + + {/* 🔗 GITHUB */} + + View GitHub Profile → + + +
+ ); +} +===================================================================================================== +src/services/githubService.ts === + +const BASE = "https://api.github.com"; + +const TOKEN = import.meta.env.VITE_GITHUB_TOKEN; + +export const fetchOrgRepos = async (org: string) => { + const res = await fetch(`${BASE}/orgs/${org}/repos?per_page=100`, { + headers: { + Authorization: `token ${TOKEN}` + } + }); + + if (!res.ok) throw new Error("API Error"); + return res.json(); +}; + +export const fetchRepoContributors = async (url: string) => { + const res = await fetch(url, { + headers: { + Authorization: `token ${TOKEN}` + } + }); + + if (!res.ok) throw new Error("Contributor API Error"); + return res.json(); +}; + +export const fetchRepoIssues = async (owner: string, repo: string) => { + const res = await fetch( + `https://api.github.com/repos/${owner}/${repo}/issues?state=all&per_page=100`, + { + headers: { + Authorization: `token ${TOKEN}` + } + } + ); + + if (!res.ok) throw new Error("Issues API Error"); + return res.json(); +}; + +export const fetchRepoPRs = async (owner: string, repo: string) => { + const res = await fetch( + `${BASE}/repos/${owner}/${repo}/pulls?state=all&per_page=100`, + { + headers: { Authorization: `token ${TOKEN}` } + } + ); + + if (!res.ok) return []; + return res.json(); +}; +=============================================================================== +src/types/github.ts == +export interface Repo { + id: number; + name: string; + stargazers_count: number; + forks_count: number; + updated_at: string; +} + +export interface Insight { + totalRepos: number; + totalStars: number; + totalForks: number; + inactivePercent: string; + topRepos : Repo[]; +insights: string[]; +} +==================================================================================== +utils/calculateScore.ts == +export const calculateOrgHealthScore = (repos: any[]) => { + if (!repos || repos.length === 0) return { score: 0, label: "No Data" }; + + const totalRepos = repos.length; + + const activeRepos = repos.filter(repo => { + const days = + (Date.now() - new Date(repo.pushed_at).getTime()) / + (1000 * 60 * 60 * 24); + return days < 30; + }).length; + + const avgStars = + repos.reduce((sum, r) => sum + r.stargazers_count, 0) / totalRepos; + + const avgForks = + repos.reduce((sum, r) => sum + r.forks_count, 0) / totalRepos; + + const staleRepos = repos.filter(repo => { + const days = + (Date.now() - new Date(repo.pushed_at).getTime()) / + (1000 * 60 * 60 * 24); + return days > 180; + }).length; + + let score = 0; + + // 🚀 Activity score (40) + score += (activeRepos / totalRepos) * 40; + + // 🌟 Popularity score (30) + score += Math.min(avgStars, 100) * 0.3; + + // 🔁 Engagement score (20) + score += Math.min(avgForks, 50) * 0.4; + + // 🛑 Penalty (10) + score -= (staleRepos / totalRepos) * 10; + + score = Math.round(score); + + let label = "Poor"; + if (score > 75) label = "Excellent 🚀"; + else if (score > 50) label = "Good 👍"; + else if (score > 30) label = "Average ⚠️"; + + return { score, label }; +}; +====================================================== + +================================================================== + +utils/exportCSV.ts == + +export const exportCSV = (repos: any[]) => { + const rows = repos.map(r => + `${r.name},${r.stargazers_count},${r.forks_count}` + ); + + const csv = "Name,Stars,Forks\n" + rows.join("\n"); + + const blob = new Blob([csv]); + const url = URL.createObjectURL(blob); + + const a = document.createElement("a"); + a.href = url; + a.download = "repos.csv"; + a.click(); +}; +======================================================================= +utils/insightEngine.ts === +import type { Repo, Insight } from "../types/github"; + +export interface InsightResult extends Insight { + insights: string[]; +} + +export const getInsights = (repos: Repo[]): InsightResult => { + const now = new Date(); + + const inactive = repos.filter(r => + (now.getTime() - new Date(r.updated_at).getTime()) > + 90 * 24 * 60 * 60 * 1000 + ); + + const totalStars = repos.reduce((sum, r) => sum + r.stargazers_count, 0); + const totalForks = repos.reduce((s, r) => s + r.forks_count, 0); + + const topRepos = [...repos] + .sort((a, b) => b.stargazers_count - a.stargazers_count) + .slice(0, 5); + + const inactivePercent = (inactive.length / repos.length) * 100; + + // NEW: INSIGHT GENERATION + const insights: string[] = []; + + // Inactive repos insight + if (inactivePercent > 50) { + insights.push("⚠ More than 50% repositories are inactive → maintenance risk"); + } else if (inactivePercent > 30) { + insights.push("⚠ Significant number of repositories are inactive"); + } else { + insights.push("✅ Most repositories are actively maintained"); + } + + // Star concentration insight + if (topRepos.length > 0) { + const topStar = topRepos[0].stargazers_count; + + if (topStar > totalStars * 0.5) { + insights.push("⚡ One repository dominates more than 50% of total stars"); + } else if (topStar > totalStars * 0.3) { + insights.push("⚡ A few repositories dominate the ecosystem"); + } + } + + // Fork vs Star ratio insight + if (totalStars > 0) { + const ratio = totalForks / totalStars; + + if (ratio > 0.6) { + insights.push("🔁 High fork-to-star ratio → strong developer engagement"); + } else if (ratio < 0.2) { + insights.push("📉 Low fork activity compared to stars"); + } + } + + // Recently active repos insight + const recent = repos.filter(r => + (now.getTime() - new Date(r.updated_at).getTime()) < + 30 * 24 * 60 * 60 * 1000 + ); + + if (recent.length > repos.length * 0.5) { + insights.push("🚀 High recent activity across repositories"); + } else if (recent.length < repos.length * 0.2) { + insights.push("🐢 Low recent activity → possible slowdown"); + } + + // Repo size distribution insight + const lowStarRepos = repos.filter(r => r.stargazers_count < 10); + + if (lowStarRepos.length > repos.length * 0.6) { + insights.push("📦 Majority of repositories have low visibility (<10 stars)"); + } + + // Growth potential insight + if (totalStars > 1000 && inactivePercent < 30) { + insights.push("📈 Organization shows strong growth potential"); + } + + return { + totalRepos: repos.length, + totalStars, + totalForks, + inactivePercent: inactivePercent.toFixed(1), + topRepos, + insights + }; +}; +=========================================================================================== +utils/mergeOrgs.ts====== +import type { Repo } from "../types/github"; + +export const mergeRepos = (allRepos: Repo[][]): Repo[] => { + const map = new Map(); + + allRepos.flat().forEach(repo => { + if (!map.has(repo.id)) { + map.set(repo.id, repo); + } + }); + + return Array.from(map.values()); +}; +============================================================================================== +utils/insights.ts================================= +export function generateInsights(repos: any[]) { + if (!repos || repos.length === 0) return []; + + const insights: string[] = []; + + // Most starred repo + const topRepo = [...repos].sort( + (a, b) => b.stargazers_count - a.stargazers_count + )[0]; + + insights.push(`⭐ Most popular repo: ${topRepo.name}`); + + // Low activity repos + const lowActivity = repos.filter((r) => r.stargazers_count < 5); + if (lowActivity.length > 0) { + insights.push(`⚠️ ${lowActivity.length} repos have very low stars`); + } + + // Fork heavy repos + const forkHeavy = repos.filter((r) => r.forks_count > r.stargazers_count); + if (forkHeavy.length > 0) { + insights.push(`🍴 Some repos are fork-heavy but not popular`); + } + + // Recently updated + const recent = repos.filter((r) => { + const updated = new Date(r.updated_at); + const now = new Date(); + return (now.getTime() - updated.getTime()) / (1000 * 60 * 60 * 24) < 30; + }); + + if (recent.length > repos.length / 2) { + insights.push(`🚀 Org is actively maintained (many recent updates)`); + } + + // Stale repos + const stale = repos.filter((r) => { + const updated = new Date(r.updated_at); + const now = new Date(); + return (now.getTime() - updated.getTime()) / (1000 * 60 * 60 * 24) > 180; + }); + + if (stale.length > 0) { + insights.push(`💀 ${stale.length} repos are stale (>6 months no updates)`); + } + + // Language dominance + const langMap: any = {}; + repos.forEach((r) => { + if (!r.language) return; + langMap[r.language] = (langMap[r.language] || 0) + 1; + }); + + const topLang = Object.keys(langMap).sort( + (a, b) => langMap[b] - langMap[a] + )[0]; + + if (topLang) { + insights.push(`💻 Most used language: ${topLang}`); + } + + return insights; +} + +===================================================================================== +app.tsx === +import { useState } from "react"; +import { Routes, Route } from "react-router-dom"; + +import DashboardLayout from "./layout/DashboardLayout"; +import Overview from "./pages/Overview"; +import Repositories from "./pages/Repositories"; +import GraphPage from "./pages/GraphPage"; +import ContributorDetail from "./pages/ContributorDetail"; +import RepoDetails from "./pages/RepoDetails"; + +export default function App() { + const [orgInput, setOrgInput] = useState(""); + const [orgLogo, setOrgLogo] = useState(""); + + return ( + + + + } + /> + + } /> + } /> + } /> + } /> + + + + ); +} + diff --git a/note2.txt b/note2.txt new file mode 100644 index 0000000..f8fa9fc --- /dev/null +++ b/note2.txt @@ -0,0 +1,297 @@ +h2D from "react-force-graph-2d"; +import { useEffect, useState, useRef } from "react"; +import { fetchRepoContributors } from "../../services/githubService"; + +export default function NetworkGraph({ repos }: any) { + + const [graphData, setGraphData] = useState({ nodes: [], links: [] }); + const [selectedRepo, setSelectedRepo] = useState(null); + const [crossUsers, setCrossUsers] = useState([]); + const fgRef = useRef(null); + + useEffect(() => { + const buildGraph = async () => { + + const nodes: any[] = []; + const links: any[] = []; + + const userMap = new Map(); + const repoMap = new Map(); + const userOrgMap: any = {}; + + // 🔥 LIMIT for performance (important) + const selectedRepos = repos.slice(0, 8); + + for (let repo of selectedRepos) { + + const org = repo.full_name.split("/")[0]; + + // ✅ REPO NODE + if (!repoMap.has(repo.full_name)) { + repoMap.set(repo.full_name, true); + + nodes.push({ + id: repo.full_name, + type: "repo", + org, + stars: repo.stargazers_count, + size: Math.log(repo.stargazers_count + 1) * 3 + 8 + }); + } + + try { + const contributors = await fetchRepoContributors( + `${repo.contributors_url}?per_page=50` + ); + + contributors.slice(0, 20).forEach((c: any) => { + + // ✅ USER NODE + if (!userMap.has(c.login)) { + userMap.set(c.login, true); + + nodes.push({ + id: c.login, + type: "user", + img: c.avatar_url, + contributions: c.contributions, + size: Math.log(c.contributions + 1) * 2 + 5 + }); + } + + // 🔥 TRACK ORG RELATION + if (!userOrgMap[c.login]) { + userOrgMap[c.login] = new Set(); + } + userOrgMap[c.login].add(org); + + // ✅ LINK repo ↔ user + links.push({ + source: c.login, + target: repo.full_name, + weight: c.contributions + }); + + }); + + } catch (e) { + console.error(e); + } + } + + // 🔥 CROSS ORG USERS + const cross = Object.entries(userOrgMap) + .filter(([_, set]: any) => set.size > 1) + .map(([user, set]: any) => ({ + user, + orgCount: set.size + })) + .sort((a, b) => b.orgCount - a.orgCount) + .slice(0, 5); + + setCrossUsers(cross); + + setGraphData({ nodes, links }); + }; + + if (repos.length) buildGraph(); + + }, [repos]); + + // 🔥 FORCE SETTINGS (clean network look) + useEffect(() => { + if (!fgRef.current) return; + + const fg = fgRef.current; + + fg.d3Force("charge").strength(-150); + fg.d3Force("link").distance(100); + + }, [graphData]); + + // 🔥 INSIGHTS + const totalLinks = graphData.links.length; + const totalNodes = graphData.nodes.length; + + return ( +
+ + {/* 🔥 INSIGHT */} +

+ 🔥 {totalLinks} connections across {totalNodes} nodes +

+ +

+ {totalLinks > 150 + ? "🚀 High collaboration across organizations" + : "⚠️ Limited collaboration detected"} +

+ + {/* 🔥 CROSS ORG USERS */} +
+

+ 🔗 Top Cross-Org Contributors +

+ + {crossUsers.length === 0 && ( +

No cross-org contributors

+ )} + + {crossUsers.map((u: any) => ( +
+ {u.user} → {u.orgCount} orgs +
+ ))} +
+ + {/* 🔥 GRAPH */} +
+ + + node.type === "repo" + ? `📦 ${node.id} ⭐ ${node.stars}` + : `👤 ${node.id} (${node.contributions})` + } + + // 🔥 CLICK + onNodeClick={(node: any) => { + if (node.type === "user") { + window.open(`https://github.com/${node.id}`, "_blank"); + } + if (node.type === "repo") { + setSelectedRepo(node); + } + }} + + // 🔥 NODE DESIGN + nodeCanvasObject={(node: any, ctx, globalScale) => { + const size = node.size || 6; + + // USER IMAGE + if (node.type === "user" && node.img) { + const img = new Image(); + img.src = node.img; + + ctx.save(); + ctx.beginPath(); + ctx.arc(node.x, node.y, size, 0, 2 * Math.PI); + ctx.clip(); + ctx.drawImage(img, node.x - size, node.y - size, size * 2, size * 2); + ctx.restore(); + } + + // REPO NODE (colored by org) + else { + ctx.beginPath(); + ctx.arc(node.x, node.y, size, 0, 2 * Math.PI); + + if (node.org === "AOSSIE-Org") ctx.fillStyle = "#a855f7"; + else if (node.org === "StabilityNexus") ctx.fillStyle = "#22c55e"; + else ctx.fillStyle = "#facc15"; + + ctx.fill(); + } + + // LABEL + ctx.font = `${10 / globalScale}px Inter`; + ctx.fillStyle = "#e2e8f0"; + ctx.fillText(node.id.split("/")[1] || node.id, node.x + size + 2, node.y); + }} + + // 🔥 LINKS + linkWidth={(link: any) => Math.log(link.weight + 1)} + linkColor={() => "rgba(34,197,94,0.5)"} + + linkDirectionalParticles={2} + linkDirectionalParticleSpeed={0.003} + + onEngineStop={() => fgRef.current?.zoomToFit(400)} + /> +
+ + {/* 🔥 REPO POPUP */} + {selectedRepo && ( +
+
+ +

+ 📦 {selectedRepo.id} +

+ +

+ ⭐ Stars: {selectedRepo.stars} +

+ + + + + +
+
+ )} +
+ ); +} + + +
+ + {/* 🟢 Contributors */} +
+ 🟢 {stats.users} + Contributors +
+ + {/* 🟡 Repos */} +
+ 🟡 {stats.repos} + Repos +
+ + {/* ⚪ Edges */} +
+ ⚪ {stats.edges} + Links +
+ + {/* 🔥 Top Contributor */} +
+ Most Active Contributor : {topContributor?.label} +
+ + {/* 🚀 Most Active Repo */} +
+ Most Active Repo : {mostActiveRepo?.label} +
+ + {/* 🔗 Strongest Link */} +
+ Strongest : {getId(strongestLink?.source)} → {getId(strongestLink?.target)} +
+ +
\ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 63b60a4..4179a91 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "react": "^19.2.0", "react-dom": "^19.2.0", "react-force-graph-2d": "^1.29.1", + "react-icons": "^5.6.0", "react-router-dom": "^7.13.2", "recharts": "^3.8.1" }, @@ -3992,6 +3993,15 @@ "react": "*" } }, + "node_modules/react-icons": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.6.0.tgz", + "integrity": "sha512-RH93p5ki6LfOiIt0UtDyNg/cee+HLVR6cHHtW3wALfo+eOHTp8RnU2kRkI6E+H19zMIs03DyxUG/GfZMOGvmiA==", + "license": "MIT", + "peerDependencies": { + "react": "*" + } + }, "node_modules/react-is": { "version": "19.2.4", "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.4.tgz", diff --git a/package.json b/package.json index 230c781..7a383ea 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "react": "^19.2.0", "react-dom": "^19.2.0", "react-force-graph-2d": "^1.29.1", + "react-icons": "^5.6.0", "react-router-dom": "^7.13.2", "recharts": "^3.8.1" }, diff --git a/src/App.tsx b/src/App.tsx index 9b6a970..470c721 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -5,12 +5,15 @@ import DashboardLayout from "./layout/DashboardLayout"; import Overview from "./pages/Overview"; import Repositories from "./pages/Repositories"; import GraphPage from "./pages/GraphPage"; +import ContributorDetail from "./pages/ContributorDetail"; +import RepoDetails from "./pages/RepoDetails"; export default function App() { const [orgInput, setOrgInput] = useState(""); + const [orgLogo, setOrgLogo] = useState(""); return ( - + } /> } /> } /> + } /> + } /> + ); diff --git a/src/components/Charts/ActivityChart.tsx b/src/components/Charts/ActivityChart.tsx index 149bcaf..7a7069d 100644 --- a/src/components/Charts/ActivityChart.tsx +++ b/src/components/Charts/ActivityChart.tsx @@ -1,121 +1,313 @@ -import { useState, useMemo } from "react"; +import { useEffect, useState } from "react"; import { - LineChart, Line, - BarChart, Bar, - PieChart, Pie, Cell, LabelList, - XAxis, YAxis, Tooltip, Legend + AreaChart, Area, XAxis, YAxis, Tooltip, Legend, ResponsiveContainer, + BarChart, Bar } from "recharts"; +import { GoAlert, GoCheckbox } from "react-icons/go"; -const tooltipFormatter = ( - value?: number | string, - name?: string -): [string, string] => { - return [ - value !== undefined ? value.toString() : "-", - name !== undefined ? name : "-" - ]; -}; -export default function ActivityChart({ repos }: any) { - const [chartType, setChartType] = useState("line"); - const [filter, setFilter] = useState("top"); +const TOKEN = import.meta.env.VITE_GITHUB_TOKEN; - const COLORS = ["#22c55e", "#3b82f6", "#f59e0b", "#ef4444", "#8b5cf6", "#ec4899"]; +export default function ActivityChart({ orgs }: { orgs: string[] }) { - const data = useMemo(() => { - let processed = [...repos]; + const [data, setData] = useState([]); + const [filter, setFilter] = useState("30"); + const [loading, setLoading] = useState(false); - if (filter === "top") { - processed.sort((a, b) => b.stargazers_count - a.stargazers_count); + const getDays = () => (filter === "7" ? 7 : 30); + + useEffect(() => { + if (!orgs || orgs.length === 0) return; + + const timer = setTimeout(() => { + fetchData(); + }, 500); + + return () => clearTimeout(timer); + + }, [filter, orgs]); + + + const fetchAllPages = async (url: string) => { + let results: any[] = []; + let page = 1; + + while (page <= 3) { // limit pages (safe for rate limit) + const res = await fetch(`${url}&page=${page}`, { + headers: { Authorization: `token ${TOKEN}` } + }); + + const data = await res.json(); + + if (!data.items || data.items.length === 0) break; + + results.push(...data.items); + page++; } - if (filter === "inactive") { - const now = new Date().getTime(); - processed = processed.filter(r => - now - new Date(r.updated_at).getTime() > 90 * 24 * 60 * 60 * 1000 + + return results; + }; + + const updateChart = (prs: any[], issues: any[]) => { + const days = getDays(); + const now = new Date(); + + setData(prev => { + const map: any = {}; + + prev.forEach(d => { + map[d.date] = { ...d }; + }); + + prs.forEach((pr: any) => { + const date = new Date(pr.created_at); + const diff = (now.getTime() - date.getTime()) / (1000 * 60 * 60 * 24); + + if (diff <= days) { + const key = date.toISOString().split("T")[0]; + if (!map[key]) return; + + map[key].prCreated += 1; + if (pr.state === "closed") map[key].prMerged += 1; + } + }); + + issues.forEach((issue: any) => { + const date = new Date(issue.created_at); + const diff = (now.getTime() - date.getTime()) / (1000 * 60 * 60 * 24); + + if (diff <= days) { + const key = date.toISOString().split("T")[0]; + if (!map[key]) return; + + map[key].issuesCreated += 1; + if (issue.state === "closed") map[key].issuesClosed += 1; + } + }); + + return Object.values(map); + }); + }; + + const fetchData = async () => { + try { + setLoading(true); + setData( + Array.from({ length: getDays() + 1 }, (_, i) => { + const d = new Date(); + d.setDate(d.getDate() - (getDays() - i)); + const key = d.toISOString().split("T")[0]; + + return { + date: key, + prCreated: 0, + prMerged: 0, + issuesCreated: 0, + issuesClosed: 0 + }; + }) ); + + const days = getDays(); + const now = new Date(); + + let allPRs: any[] = []; + let allIssues: any[] = []; + + // MULTI ORG + PAGINATION + const orgPromises = orgs.map(async (org) => { + + const prs = await fetchAllPages( + `https://api.github.com/search/issues?q=org:${org}+type:pr&per_page=100` + ); + + const issues = await fetchAllPages( + `https://api.github.com/search/issues?q=org:${org}+type:issue&per_page=100` + ); + + return { prs, issues }; + }); + + // const results = await Promise.all(orgPromises); + + orgs.forEach(async (org) => { + try { + const prs = await fetchAllPages( + `https://api.github.com/search/issues?q=org:${org}+type:pr&per_page=100` + ); + + const issues = await fetchAllPages( + `https://api.github.com/search/issues?q=org:${org}+type:issue&per_page=100` + ); + + // update chart immediately for THIS org + updateChart(prs, issues); + + } catch (err) { + console.error(err); + } + }); + + // results.forEach(r => { + // allPRs.push(...r.prs); + // allIssues.push(...r.issues); + // }); + + const chartMap: any = {}; + + // PR DATA + allPRs.forEach((pr: any) => { + const date = new Date(pr.created_at); + const diff = (now.getTime() - date.getTime()) / (1000 * 60 * 60 * 24); + + if (diff <= days) { + const key = date.toISOString().split("T")[0]; + + if (!chartMap[key]) { + chartMap[key] = { + date: key, + prCreated: 0, + prMerged: 0, + issuesCreated: 0, + issuesClosed: 0 + }; + } + + chartMap[key].prCreated++; + if (pr.state === "closed") chartMap[key].prMerged++; + } + }); + + // ISSUE DATA + allIssues.forEach((issue: any) => { + const date = new Date(issue.created_at); + const diff = (now.getTime() - date.getTime()) / (1000 * 60 * 60 * 24); + + if (diff <= days) { + const key = date.toISOString().split("T")[0]; + + if (!chartMap[key]) { + chartMap[key] = { + date: key, + prCreated: 0, + prMerged: 0, + issuesCreated: 0, + issuesClosed: 0 + }; + } + + chartMap[key].issuesCreated++; + if (issue.state === "closed") chartMap[key].issuesClosed++; + } + }); + + // FILL DAYS (NO BREAK LINES) + const result: any[] = []; + + for (let i = days; i >= 0; i--) { + const d = new Date(); + d.setDate(d.getDate() - i); + + const key = d.toISOString().split("T")[0]; + + result.push({ + date: key, + prCreated: chartMap[key]?.prCreated || 0, + prMerged: chartMap[key]?.prMerged || 0, + issuesCreated: chartMap[key]?.issuesCreated || 0, + issuesClosed: chartMap[key]?.issuesClosed || 0 + }); + } + + console.log("FINAL COMBINED:", result); + + setData(result); + + } catch (err) { + console.error(err); + } finally { + setLoading(false); } + }; + const formatDate = (date: string) => { + const d = new Date(date); + return d.toLocaleDateString("en-US", { + month: "short", + day: "numeric", + }); + }; - return processed.slice(0, 8).map((r: any) => ({ - name: r.name, - stars: r.stargazers_count, - forks: r.forks_count - })); - }, [repos, filter]); + // INSIGHT + const totalPR = data.reduce((a, b) => a + b.prCreated, 0); + const mergedPR = data.reduce((a, b) => a + b.prMerged, 0); + const mergeRate = totalPR ? Math.round((mergedPR / totalPR) * 100) : 0; return ( -
- -
- {["line", "bar", "pie"].map(type => ( - - ))} -
+
+ + {/* TITLE */} +

+ {orgs.length > 1 + ? `Combined Analytics (${orgs.length} Organizations)` + : ` Activity (${orgs[0]})`} +

-
- - + {/* FILTER */} +
+ +
- {chartType === "line" && ( - - - - - - - - + {/* LOADING */} + {loading && ( +

Loading data...

)} - {chartType === "bar" && ( - - - - - - - - - - - - - )} + {/* INSIGHT */} +

+ {mergeRate > 70 + ? ( PR merge rate is strong) + : ( PR merge rate dropped recently)} +

- {chartType === "pie" && ( - - `${name}: ${value}`} - > - {data.map((_, index) => ( - - ))} - - - - - )} + {/* CHARTS */} +
+ {/* AREA CHART */} +
+

PR Trend

+ + + + + + + + + + + + +
+ + {/* BAR CHART */} +
+

Issues Activity

+ + + + + + + + + + + + +
+ +
); -} +} \ No newline at end of file diff --git a/src/components/Graph/NetworkGraph.tsx b/src/components/Graph/NetworkGraph.tsx index 5db2136..68fce2f 100644 --- a/src/components/Graph/NetworkGraph.tsx +++ b/src/components/Graph/NetworkGraph.tsx @@ -1,172 +1,549 @@ import ForceGraph2D from "react-force-graph-2d"; -import { useEffect, useState, useRef } from "react"; +import { useEffect, useRef, useState } from "react"; import { fetchRepoContributors } from "../../services/githubService"; +import { exportCSV } from "../../utils/exportCSV"; +import { GoPeople, GoRepo, GoGitBranch, GoLink } from "react-icons/go"; +import { FaLink } from "react-icons/fa"; +import { IoPeople } from "react-icons/io5"; -export default function NetworkGraph({ repos }: any) { - const [graphData, setGraphData] = useState({ - nodes: [], - links: [] - }); +type NodeType = { + id: string; + type: "repo" | "user"; + label: string; + img?: string; + + stars?: number; + forks?: number; + issues?: number; + + contributions?: number; + activity?: number; + + size?: number; + org?: string; +}; - const fgRef = useRef(null); // FIX +type LinkType = { + source: string; + target: string; + weight: number; +}; +export default function NetworkGraph({ repos }: any) { + const fgRef = useRef(null); + + const [graphData, setGraphData] = useState<{ + nodes: NodeType[]; + links: LinkType[]; + }>({ nodes: [], links: [] }); + + const [selectedNode, setSelectedNode] = useState(null); + const [stats, setStats] = useState({ + users: 0, + repos: 0, + edges: 0, + }); + const [hoverNode, setHoverNode] = useState(null); + const [focusNode, setFocusNode] = useState(null); + + /* ───────── BUILD GRAPH (MULTI ORG FIX) ───────── */ useEffect(() => { - const buildGraph = async () => { - const nodes: any[] = []; - const links: any[] = []; + if (!repos?.length) return; + + const build = async () => { + const nodes: NodeType[] = []; + const links: LinkType[] = []; const userMap = new Map(); - for (let repo of repos.slice(0, 10)) { + // GROUP BY ORG + const orgGrouped: Record = {}; + + repos.forEach((repo: any) => { + const org = repo.full_name.split("/")[0]; + if (!orgGrouped[org]) orgGrouped[org] = []; + orgGrouped[org].push(repo); + }); - // Repo node + // TAKE 4 REPOS PER ORG + const selectedRepos: any[] = []; + Object.values(orgGrouped).forEach((list: any[]) => { + selectedRepos.push(...list.slice(0, 4)); + }); + + for (const repo of selectedRepos) { + const org = repo.full_name.split("/")[0]; + + // REPO NODE nodes.push({ - id: repo.name, + id: repo.full_name, type: "repo", - stars: repo.stargazers_count + label: repo.name, + org, + stars: repo.stargazers_count, + forks: repo.forks_count, + issues: repo.open_issues_count, + activity: new Date(repo.updated_at).getTime(), + size: + Math.log((repo.stargazers_count || 1) + 1) * 4 + + Math.log((repo.forks_count || 1) + 1) * 2 + + 8, }); try { - const contributors = await fetchRepoContributors(repo.contributors_url); + const contributors = await fetchRepoContributors( + `${repo.contributors_url}?per_page=30` + ); - contributors.slice(0, 10).forEach((c: any) => { + contributors.slice(0, 15).forEach((c: any) => { + if (!c?.login) return; - // USER NODE (unique) if (!userMap.has(c.login)) { userMap.set(c.login, true); nodes.push({ id: c.login, type: "user", + label: c.login, img: c.avatar_url, - contributions: c.contributions + contributions: c.contributions, + activity: c.contributions, + size: Math.log((c.contributions || 1) + 1) * 3 + 6, }); } - //LINK repo-user links.push({ source: c.login, - target: repo.name, - weight: c.contributions + target: repo.full_name, + weight: c.contributions || 1, }); - - // USER ↔ USER CONNECTION (dense graph) - contributors.slice(0, 5).forEach((other: any) => { - if (other.login !== c.login) { - links.push({ - source: c.login, - target: other.login, - weight: 1 - }); - } - }); - }); - - } catch (e) { - console.error(e); + } catch (err) { + console.log(err); } } - + setStats({ + users: nodes.filter(n => n.type === "user").length, + repos: nodes.filter(n => n.type === "repo").length, + edges: links.length, + }); setGraphData({ nodes, links }); }; - if (repos.length) buildGraph(); + + build(); }, [repos]); + + /* ───────── FORCE LAYOUT (MULTI ORG FIXED) ───────── */ useEffect(() => { - if (!fgRef.current) return; + if (!fgRef.current) return; - const fg = fgRef.current; + const fg = fgRef.current; - // Charge force (node spread) - fg.d3Force("charge").strength(-120); + fg.d3Force("charge").strength(-320); - // Link distance - fg.d3Force("link").distance(80); + fg.d3Force("link").distance((l: any) => + Math.max(80, 200 - Math.log2(l.weight + 1) * 25) + ); - // Centering - fg.d3Force("center", null); - }, [graphData]); + // ORGS + const orgs: string[] = Array.from( + new Set( + graphData.nodes + .filter((n) => n.type === "repo" && n.org) + .map((n) => n.org as string) + ) + ); - return ( -
+ const orgMap: Record = {}; + const gap = 400; - { + orgMap[org] = (i - (orgs.length - 1) / 2) * gap; + }); - backgroundColor="#020617" + // X FORCE + fg.d3Force("x", (node: any) => { + if (node.type === "repo") { + return node.org ? orgMap[node.org] ?? 0 : 0; + } - // DISABLE DRAG (static feel) - enableNodeDrag={false} + const linked = graphData.links.find((l) => l.source === node.id); - // DISABLE PAN (no movement) - enablePanInteraction={false} + if (!linked) return 0; - // ONLY ZOOM allowed - enableZoomInteraction={true} + const repoNode = graphData.nodes.find( + (n) => n.id === linked.target + ) as NodeType | undefined; - // REMOVE AUTO MOVE - cooldownTicks={0} + if (!repoNode || !repoNode.org) return 0; - // STOP physics after load - onEngineStop={() => { - fgRef.current?.zoomToFit(400); - }} + return orgMap[repoNode.org] ?? 0; + }); - d3VelocityDecay={0.9} // fast stop - d3AlphaDecay={0.1} // instant stable + // Y FORCE (activity) + fg.d3Force("y", (node: any) => { + const act = node.activity || 1; + const norm = Math.min(1, Math.log(act + 1) / 10); - nodeLabel={(node: any) => - node.type === "repo" - ? `📦 ${node.id}` - : `👤 ${node.id}` - } + return node.type === "repo" + ? -300 * norm + : 300 * (1 - norm); + }); + }, [graphData]); + + // ======================================================================================== + const isConnectedToFocus = (nodeId: string) => { + if (!focusNode) return true; + + const focusId = focusNode.id; + + return graphData.links.some((l: any) => { + const s = typeof l.source === "object" ? l.source.id : l.source; + const t = typeof l.target === "object" ? l.target.id : l.target; + + return ( + (s === nodeId && t === focusId) || + (t === nodeId && s === focusId) + ); + }); + }; + // ======================================================= + /* ───────── NODE DRAW ───────── */ + + const drawNode = (node: any, ctx: CanvasRenderingContext2D, scale: number) => { + const padding = 6; + + // dynamic width based on text + const label = node.label || ""; + ctx.font = `${9 / scale}px Arial`; + const textWidth = ctx.measureText(label).width; + + const width = Math.max(80, textWidth + 40); // auto width + const height = 52; + + const x = node.x - width / 2; + const y = node.y - height / 2; + + // helper for id (IMPORTANT FIX) + const getId = (val: any) => + typeof val === "object" ? val.id : val; + + let opacity = 1; + + // PRIORITY: FOCUS MODE + if (focusNode) { + const isConnected = isConnectedToFocus(node.id); + + if (node.id !== focusNode.id && !isConnected) { + opacity = 0.1; + } + } - // NODE DRAW (DP + NAME) - nodeCanvasObject={(node: any, ctx, globalScale) => { - const size = - node.type === "repo" - ? 10 - : 6; - - const fontSize = 10 / globalScale; - - if (node.type === "user" && node.img) { - const img = new Image(); - img.src = node.img; - - ctx.save(); - ctx.beginPath(); - ctx.arc(node.x, node.y, size, 0, 2 * Math.PI); - ctx.closePath(); - ctx.clip(); - ctx.drawImage(img, node.x - size, node.y - size, size * 2, size * 2); - ctx.restore(); - } else { - ctx.beginPath(); - ctx.arc(node.x, node.y, size, 0, 2 * Math.PI); - ctx.fillStyle = "#22c55e"; - ctx.fill(); - } - - // 🏷 NAME - ctx.font = `${fontSize}px Inter`; - ctx.fillStyle = "#e2e8f0"; - ctx.fillText(node.id, node.x + size + 2, node.y); + // SECOND: HOVER MODE + else if (hoverNode) { + const getId = (val: any) => + typeof val === "object" ? val.id : val; + + const isConnected = graphData.links.some((l: any) => { + const s = getId(l.source); + const t = getId(l.target); + + return ( + (s === node.id && t === hoverNode.id) || + (t === node.id && s === hoverNode.id) + ); + }); + + if (node.id !== hoverNode.id && !isConnected) { + opacity = 0.1; + } + } + + ctx.globalAlpha = opacity; + + // CARD BACKGROUND (glass style) + ctx.fillStyle = "rgba(15, 23, 42, 0.9)"; + ctx.strokeStyle = node.type === "repo" ? "#facc15" : "#22c55e"; + ctx.lineWidth = 1.5; + + ctx.beginPath(); + ctx.roundRect(x, y, width, height, 10); + ctx.fill(); + ctx.stroke(); + + // avatar + if (node.img) { + const img = new Image(); + img.src = node.img; + + ctx.save(); + ctx.beginPath(); + ctx.roundRect(x + padding, y + padding, 22, 22, 4); + ctx.clip(); + ctx.drawImage(img, x + padding, y + padding, 22, 22); + ctx.restore(); + } + + // TEXT CLIP (IMPORTANT — no overflow) + ctx.save(); + ctx.beginPath(); + ctx.rect(x + 30, y + 5, width - 35, 20); + ctx.clip(); + + ctx.fillStyle = "#e5e7eb"; + ctx.fillText(label, x + 30, y + 18); + ctx.restore(); + + // 📊 contributions / stats + if (node.contributions) { + ctx.fillStyle = "#22c55e"; + ctx.font = `${8 / scale}px Arial`; + ctx.fillText(`${node.contributions} commits`, x + 30, y + 35); + } + + if (node.type === "repo") { + ctx.fillStyle = "#facc15"; + ctx.font = `${8 / scale}px Arial`; + ctx.fillText("repo", x + 30, y + 35); + } + + ctx.globalAlpha = 1; + + if (focusNode) { + const isConnected = isConnectedToFocus(node.id); + + if (node.id !== focusNode.id && !isConnected) { + opacity = 0.1; // dim others + } + } + }; + /* ───────── LINK DRAW ───────── */ + + const topContributor = graphData.nodes + .filter(n => n.type === "user") + .sort((a, b) => (b.contributions || 0) - (a.contributions || 0))[0]; + + const mostActiveRepo = graphData.nodes + .filter(n => n.type === "repo") + .sort((a, b) => (b.activity || 0) - (a.activity || 0))[0]; + + const strongestLink = graphData.links + .sort((a, b) => b.weight - a.weight)[0]; + + const getId = (val: any) => + typeof val === "object" ? val.id : val; + + // TOTAL UNIQUE CONTRIBUTORS + const totalUniqueContributors = new Set( + graphData.nodes + .filter(n => n.type === "user") + .map(n => n.id) + ).size; + + + // SHARED CONTRIBUTORS + const contributorOrgs: Record> = {}; + + graphData.links.forEach((l: any) => { + const user = typeof l.source === "object" ? l.source.id : l.source; + const repo = graphData.nodes.find(n => n.id === l.target); + + if (!repo?.org) return; + + if (!contributorOrgs[user]) { + contributorOrgs[user] = new Set(); + } + + contributorOrgs[user].add(repo.org); + }); + + const sharedContributors = Object.values(contributorOrgs).filter( + (orgSet) => orgSet.size > 1 + ).length; + + const drawLink = (link: any, ctx: CanvasRenderingContext2D) => { + const getId = (val: any) => + typeof val === "object" ? val.id : val; + + const s = link.source; + const t = link.target; + + if (!s?.x || !t?.x) return; + + let opacity = 0.3; + + if (focusNode) { + const sourceId = getId(link.source); + const targetId = getId(link.target); + + if (sourceId === focusNode.id || targetId === focusNode.id) { + opacity = 1; + } else { + opacity = 0.05; + } + } else if (hoverNode) { + const sourceId = getId(link.source); + const targetId = getId(link.target); + + if (sourceId === hoverNode.id || targetId === hoverNode.id) { + opacity = 1; + } else { + opacity = 0.05; + } + } + + ctx.beginPath(); + ctx.moveTo(s.x, s.y); + ctx.lineTo(t.x, t.y); + + ctx.strokeStyle = `rgba(34,197,94,${opacity})`; + ctx.lineWidth = Math.max(1, Math.log2(link.weight + 1)); + + ctx.stroke(); + }; + /* ───────── UI ───────── */ + + return ( +
+ +
+ + {/* LEFT SIDE (FULL WIDTH STATS) */} +
+ +
+ {stats.users} Contributors +
+ +
+ {stats.repos} Repos +
+ +
+ {stats.edges} Links +
+ +
+ {totalUniqueContributors} Total +
+ +
+ {sharedContributors} Shared +
+ +
+ Top Contributor : {topContributor?.label} +
+ +
+ Most Active Repo : {mostActiveRepo?.label} +
+ +
+ {getId(strongestLink?.source)} → {getId(strongestLink?.target)} +
+ +
+ + {/* RIGHT SIDE BUTTON */} + + +
+ + {selectedNode && ( +
+ + + +
+ {selectedNode.img && ( + + )} +
+

{selectedNode.label}

+

+ {selectedNode.type === "user" ? "Contributor" : "Repository"} +

+
+
+ + {selectedNode.type === "user" ? ( + <> +

Commits: {selectedNode.contributions}

+ + ) : ( + <> +

⭐ Stars: {selectedNode.stars}

+

🍴 Forks: {selectedNode.forks}

+ + )} + + + Open GitHub → + +
+ + + )} + + { + const width = 100; + const height = 60; + + ctx.fillStyle = color; + ctx.fillRect( + node.x - width / 2, + node.y - height / 2, + width, + height + ); }} - // 🔗 LINKS - linkWidth={(link: any) => Math.log(link.weight + 1)} + onNodeHover={(node: any) => { + setHoverNode(node); + document.body.style.cursor = node ? "pointer" : "default"; + }} + + onNodeClick={(node: any) => { + setSelectedNode(node); + setFocusNode(node); + }} - linkColor={() => "#22c55e"} + linkDirectionalParticles={2} + linkDirectionalParticleSpeed={0.004} - linkDirectionalParticles={1} - linkDirectionalParticleSpeed={0.002} - linkDirectionalParticleWidth={1} - linkDirectionalParticleColor={() => "#22c55e"} + onEngineStop={() => fgRef.current?.zoomToFit(400)} />
); -} \ No newline at end of file +} diff --git a/src/components/Input/OrgInput.tsx b/src/components/Input/OrgInput.tsx index 3418c63..8445dd6 100644 --- a/src/components/Input/OrgInput.tsx +++ b/src/components/Input/OrgInput.tsx @@ -1,14 +1,11 @@ -import { useState } from "react"; - -export default function OrgInput({ onSubmit }: any) { - const [value , setValue] = useState("Aossie-Org") +export default function OrgInput({ onSubmit, value, setValue }: any) { return (
setValue(e.target.value)} + onChange={(e) => setValue(e.target.value)} />
+ + {/* SORT BUTTONS */} +
+ - - + + -
+ > + Forks + + +
- {/* TABLE */} -
- + {/* TABLE */} +
+
- + - - + + + + + + - - {sorted.map((r: any) => ( - - - - - - ))} - + + {sorted.map((r: any) => ( + navigate(`/repo/${r.name}`, { state: r })} + className="border-t border-gray-700 hover:bg-gray-800 transition cursor-pointer" + > + {/* 1. REPO */} + + + {/* 2. STARS */} + + + {/* 3. FORKS */} + + + {/* 4. ISSUES */} + + + {/* 5. LANGUAGE */} + + + {/* 6. LAST UPDATED */} + + {/* 7. STATUS */} + + + ))} +
RepositoryStarsForks Stars Forks IssuesLanguageLast UpdatedStatus
{r.name} - {r.stargazers_count} - - {r.forks_count} -
+ {r.name} +
+ {r.description || "No description"} +
+
+
+ {r.stargazers_count} +
+
+
+ {r.forks_count} +
+
+
+ {r.open_issues_count || 0} +
+
+ + {r.language || "N/A"} + + + {new Date(r.updated_at).toLocaleDateString()} + + {getStatus(r.updated_at).includes("Active") ? ( + Active + ) : ( + Inactive + )} +
diff --git a/src/components/TopContributors.tsx b/src/components/TopContributors.tsx new file mode 100644 index 0000000..37c3a8d --- /dev/null +++ b/src/components/TopContributors.tsx @@ -0,0 +1,122 @@ +import { useEffect, useState } from "react"; +import { useNavigate } from "react-router-dom"; +import { FaTrophy } from "react-icons/fa"; + +const TOKEN = import.meta.env.VITE_GITHUB_TOKEN; + +export default function TopContributors({ repos }: any) { + const [contributors, setContributors] = useState([]); + const navigate = useNavigate(); + + useEffect(() => { + console.log("Fetching data for:", repos); + + const fetchData = async () => { + const map: any = {}; + + // GROUPING START + const groupedRepos: any = {}; + + repos.forEach((repo: any) => { + const org = repo.full_name.split("/")[0]; + + if (!groupedRepos[org]) { + groupedRepos[org] = []; + } + + groupedRepos[org].push(repo); + }); + + // EACH ORG SE LIMIT + let finalRepos: any[] = []; + + Object.values(groupedRepos).forEach((orgRepos: any) => { + finalRepos.push(...orgRepos.slice(0, 10)); + }); + // DEBUG + console.log( + "Repos being used:", + finalRepos.map((r: any) => r.full_name) + ); + + // MAIN LOOP (IMPORTANT) + for (let repo of finalRepos) { + try { + const res = await fetch(repo.contributors_url, { + headers: { + Authorization: `token ${TOKEN}` + } + }); + + const data = await res.json(); + + if (!Array.isArray(data)) { + console.error("GitHub API Error:", data); + continue; + } + + data.forEach((c: any) => { + if (!map[c.login]) { + map[c.login] = { + login: c.login, + avatar: c.avatar_url, + contributions: 0, + url: c.html_url + }; + } + + map[c.login].contributions += c.contributions; + }); + + } catch (e) { + console.error("Fetch error:", e); + } + } + + const sorted = Object.values(map) + .sort((a: any, b: any) => b.contributions - a.contributions) + .slice(0, 5); + + setContributors(sorted); + }; + + if (repos.length) fetchData(); + }, [repos]); + + return ( +
+

+ + Top Contributors +

+ + {/* Horizontal Scroll */} +
+ + {contributors.map((c, i) => ( +
+ navigate(`/contributor/${c.login}`, { state: c }) + } + className="min-w-[220px] bg-[#111827] p-4 rounded-lg cursor-pointer hover:bg-gray-800 transition border border-gray-700" + > + + +

+ #{i + 1} {c.login} +

+ +

+ {c.contributions} contributions +

+
+ ))} + +
+
+ ); +} \ No newline at end of file diff --git a/src/layout/DashboardLayout.tsx b/src/layout/DashboardLayout.tsx index 8353d79..e642122 100644 --- a/src/layout/DashboardLayout.tsx +++ b/src/layout/DashboardLayout.tsx @@ -1,7 +1,7 @@ import Sidebar from "./Sidebar"; import Topbar from "./Topbar"; -export default function DashboardLayout({ children, orgInput }: any) { +export default function DashboardLayout({ children, orgInput, orgLogo }: any) { return (
@@ -11,9 +11,9 @@ export default function DashboardLayout({ children, orgInput }: any) { {/* Main */}
- + -
+
{children}
diff --git a/src/layout/Topbar.tsx b/src/layout/Topbar.tsx index d8095f1..3ec5e2f 100644 --- a/src/layout/Topbar.tsx +++ b/src/layout/Topbar.tsx @@ -1,138 +1,33 @@ -import { useEffect, useState } from "react"; +import { useLocation } from "react-router-dom"; -export default function Topbar({ orgInput }: any) { - const [orgName, setOrgName] = useState(""); - const [logo, setLogo] = useState(""); - const [debouncedInput, setDebouncedInput] = useState(""); +export default function Topbar({ orgInput, logo }: any) { + const location = useLocation(); - // Debounce - useEffect(() => { - const timer = setTimeout(() => { - setDebouncedInput(orgInput); - }, 500); // 500ms delay + // Only dashboard + if (location.pathname !== "/") return null; - return () => clearTimeout(timer); - }, [orgInput]); - - // API call only after debounce - useEffect(() => { - if (!debouncedInput) return; - - const orgs = debouncedInput.split(",").map((o: string) => o.trim()); - - // name update - setOrgName(orgs.join(" + ")); - - // fetch only first org logo - fetch(`https://api.github.com/orgs/${orgs[0]}`) - .then((res) => { - if (!res.ok) { - throw new Error("Org not found"); - } - return res.json(); - }) - .then((data) => { - setLogo(data.avatar_url); - }) - .catch((err) => { - console.error("Error fetching org:", err); - setLogo(""); - }); - - }, [debouncedInput]); + const orgs = orgInput + ? orgInput.split(",").map((o: string) => o.trim()) + : []; return ( -
+
- {/* LEFT */} -
+
{logo ? ( ) : ( -
+
)} -

- {orgName || "OrgExplorer"} +

+ {orgs.join(" + ") || "OrgExplorer"}

+
); -} - -// import { useEffect, useState } from "react"; - -// export default function Topbar({ orgInput }: any) { -// const [orgName, setOrgName] = useState(""); -// const [logo, setLogo] = useState(""); -// const [debouncedInput, setDebouncedInput] = useState(""); - -// // ✅ STEP 1: Debounce (IMPORTANT) -// useEffect(() => { -// const timer = setTimeout(() => { -// setDebouncedInput(orgInput); -// }, 500); // 500ms delay - -// return () => clearTimeout(timer); -// }, [orgInput]); - -// // ✅ STEP 2: API call only after debounce -// useEffect(() => { -// if (!debouncedInput) return; - -// const orgs = debouncedInput.split(",").map((o: string) => o.trim()); - -// // 👉 name update -// setOrgName(orgs.join(" + ")); - -// // 👉 fetch only first org logo -// fetch(`https://api.github.com/orgs/${orgs[0]}`) -// .then((res) => { -// if (!res.ok) { -// throw new Error("Org not found"); -// } -// return res.json(); -// }) -// .then((data) => { -// setLogo(data.avatar_url); -// }) -// .catch((err) => { -// console.error("Error fetching org:", err); -// setLogo(""); // reset if error -// }); - -// }, [debouncedInput]); - -// return ( -//
- -// {/* LEFT */} -//
-// {logo ? ( -// -// ) : ( -//
-// )} - -//

-// {orgName || "OrgExplorer"} -//

-//
- -// {/* RIGHT */} -//
-// 🔔 -//
-//
-// Tom Cook -//
-//
-//
-// ); -// } +} \ No newline at end of file diff --git a/src/main.tsx b/src/main.tsx index 735710d..f40b154 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,14 +1,3 @@ -// import { StrictMode } from 'react' -// import { createRoot } from 'react-dom/client' -// import './index.css' -// import App from './App.tsx' - -// createRoot(document.getElementById('root')!).render( -// -// -// , -// ) - import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' import './index.css' diff --git a/src/pages/ContributorDetail.tsx b/src/pages/ContributorDetail.tsx new file mode 100644 index 0000000..109cdd4 --- /dev/null +++ b/src/pages/ContributorDetail.tsx @@ -0,0 +1,508 @@ +import { useEffect, useState } from "react"; +import { useParams } from "react-router-dom"; +import { + AreaChart, Area, XAxis, YAxis, Tooltip, CartesianGrid, ResponsiveContainer, Legend +} from "recharts"; +import { FaCodeBranch, FaGithub } from "react-icons/fa"; +import { FaCodeMerge } from "react-icons/fa6"; +import { GoIssueOpened, GoIssueClosed,GoCheckCircleFill, GoXCircle, GoAlert, GoStar, GoGitMerge, GoTrophy, GoPeople } from "react-icons/go"; +// import { MdOutlineReportGmailerrorred } from "react-icons/md"; + + +const TOKEN = import.meta.env.VITE_GITHUB_TOKEN; + +export default function ContributorDetail() { + const { username } = useParams(); + + const [user, setUser] = useState(null); + const [prs, setPrs] = useState([]); + const [issues, setIssues] = useState([]); + const [sortKey, setSortKey] = useState("prCreated"); + const [sortOrder, setSortOrder] = useState("desc"); + const [events, setEvents] = useState([]); + const [filter, setFilter] = useState("7"); + + useEffect(() => { + + if (!username) return; + + const fetchData = async () => { + // console.log("Repos coming:", repos.map(r => r.full_name)); + try { + // USER + const userRes = await fetch(`https://api.github.com/users/${username}`, { + headers: { Authorization: `token ${TOKEN}` } + }); + const userData = await userRes.json(); + setUser(userData); + + // PRs + const prRes = await fetch( + `https://api.github.com/search/issues?q=author:${username}+type:pr`, + { headers: { Authorization: `token ${TOKEN}` } } + ); + const prData = await prRes.json(); + setPrs(prData.items || []); + + // Issues + const issueRes = await fetch( + `https://api.github.com/search/issues?q=author:${username}+type:issue`, + { headers: { Authorization: `token ${TOKEN}` } } + ); + const issueData = await issueRes.json(); + setIssues(issueData.items || []); + + // EVENTS (Recent Activity) + const eventRes = await fetch( + `https://api.github.com/users/${username}/events`, + { headers: { Authorization: `token ${TOKEN}` } } + ); + const eventData = await eventRes.json(); + setEvents(eventData || []); + console.log("events", eventData); + + } catch (err) { + console.error(err); + } + }; + + fetchData(); + + + }, [username]); + + function timeAgo(date: string) { + const seconds = Math.floor((new Date().getTime() - new Date(date).getTime()) / 1000); + + const intervals: any = { + year: 31536000, + month: 2592000, + week: 604800, + day: 86400, + hour: 3600, + minute: 60, + }; + + for (let key in intervals) { + const interval = Math.floor(seconds / intervals[key]); + if (interval > 1) return `${interval} ${key}s ago`; + if (interval === 1) return `1 ${key} ago`; + } + + return "just now"; + } + + const filteredEvents = events.filter((e: any) => { + const days = filter === "7" ? 7 : 30; + const eventDate = new Date(e.created_at); + const now = new Date(); + + return (now.getTime() - eventDate.getTime()) / (1000 * 60 * 60 * 24) <= days; + }); + + if (!user) return

Loading...

; + + // CALCULATIONS + const totalPR = prs.length; + const mergedPR = prs.filter(p => p.pull_request?.merged_at).length; + const mergeRate = totalPR ? Math.round((mergedPR / totalPR) * 100) : 0; + const totalIssues = issues.length; + + const score = Math.round( + 0.5 * mergeRate + + 0.3 * Math.min(totalIssues * 5, 100) + + 0.2 * 70 + ); + + // REPO-WISE STATS + const repoStats: any = {}; + + // PR data + prs.forEach((pr: any) => { + // const repo = pr.repository_url.split("/").pop(); + const parts = pr.repository_url.split("/"); + const repo = parts[parts.length - 2] + "/" + parts[parts.length - 1]; + + if (!repoStats[repo]) { + repoStats[repo] = { + repo, + prCreated: 0, + prMerged: 0, + issuesSolved: 0, + }; + } + + repoStats[repo].prCreated++; + + if (pr.pull_request?.merged_at) { + repoStats[repo].prMerged++; + } + }); + + // Issue data + issues.forEach((issue: any) => { + // const repo = issue.repository_url.split("/").pop(); + const parts = issue.repository_url.split("/"); + const repo = parts[parts.length - 2] + "/" + parts[parts.length - 1]; + + if (!repoStats[repo]) { + repoStats[repo] = { + repo, + prCreated: 0, + prMerged: 0, + issuesSolved: 0, + }; + } + + repoStats[repo].issuesSolved++; + }); + + const repoList = Object.values(repoStats); + const sortedRepos = [...repoList].sort((a: any, b: any) => { + const valA = a[sortKey]; + const valB = b[sortKey]; + + if (sortOrder === "asc") return valA - valB; + return valB - valA; + }); + + const chartData = repoList.map((r: any) => ({ + name: r.repo.split("/")[1], // sirf repo name + created: r.prCreated, + merged: r.prMerged + })); + + return ( +
+ + {/* TOP SECTION */} +
+ + + +
+

{user.login}

+ +

+ Quality Score: {score}/100 +

+ +

+ Merge Rate: {mergeRate}% +

+ +

80 ? "text-green-400" : + score > 50 ? "text-yellow-400" : + "text-red-400" + }`}> + {score > 80 ? ( High Quality Contributor) : + score > 50 ? ( Medium Quality Contributor) : + (Low Quality Contributor)} +

+
+
+ + {/* STATS CARDS */} +
+
+
+ + PR +
+

{totalPR}

+
+ +
+
+ + PR Merged +
+ +

{mergedPR}

+
+ +
+
+ +

Issues Solved

+
+

{totalIssues}

+
+ +
+
+ +

Issues Created

+
+

{totalIssues}

+
+
+ +
+ +

+ PR Activity (Created vs Merged) +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + {/* REPOSITORIES TABLE */} +
+ +

+ Repository Contributions +

+ + {repoList.length === 0 ? ( +

No repository data found

+ ) : ( +
+ + + + + + + + + + + + + + + + + + {/* BODY */} + + {sortedRepos.map((r: any) => { + const mergeRate = + r.prCreated > 0 + ? Math.round((r.prMerged / r.prCreated) * 100) + : 0; + + return ( + + + + + + + + + + + + ); + })} + + +
Repo { + setSortKey("prCreated"); + setSortOrder(sortOrder === "asc" ? "desc" : "asc"); + }} + > + PR {sortKey === "prCreated" ? (sortOrder === "asc" ? "↑" : "↓") : ""} + { + setSortKey("prMerged"); + setSortOrder(sortOrder === "asc" ? "desc" : "asc"); + }} + > + PR Merged {sortKey === "prMerged" ? (sortOrder === "asc" ? "↑" : "↓") : ""} + Merge % { + setSortKey("issuesSolved"); + setSortOrder(sortOrder === "asc" ? "desc" : "asc"); + }} + > + Issues Solved {sortKey === "issuesSolved" ? (sortOrder === "asc" ? "↑" : "↓") : ""} +
+ + {r.repo} + + {r.prCreated} + {r.prMerged} + + {mergeRate}% + + {r.issuesSolved} +
+
+ )} +
+ + {/* INSIGHTS */} +
+

Insights

+ +
    + {mergeRate > 70 && ( +
  • High merge rate indicates quality work
  • + )} + {mergeRate < 40 && ( +
  • Low merge rate suggests PR issues
  • + )} + {totalPR < 5 && ( +
  • Limited contributions
  • + )} +
+
+ + + {/* Recent Activity */} +
+
+

Recent Activity

+ + {/* Filter */} + +
+ + {/* Events */} + {filteredEvents.length === 0 ? ( +

No recent activity

+ ) : ( +
+ {filteredEvents.slice(0, 10).map((e: any, index: number) => { + const isLatest = index === 0; + + let text = ""; + let link = "#"; + + // PR Created + if (e.type === "PullRequestEvent" && e.payload.action === "opened") { + const prNumber = e.payload?.pull_request?.number || ""; + text = ` Created PR #${prNumber} in ${e.repo.name}`; + link = e.payload?.pull_request?.html_url; + } + + // PR Merged + else if (e.type === "PullRequestEvent" && e.payload?.pull_request?.merged) { + const prNumber = e.payload?.pull_request?.number || ""; + text = ` PR #${prNumber} merged in ${e.repo.name}`; + link = e.payload?.pull_request?.html_url; + } + + // Issue Opened + else if (e.type === "IssuesEvent" && e.payload.action === "opened") { + const issueNumber = e.payload?.issue?.number || ""; + text = ` Opened issue #${issueNumber} in ${e.repo.name}`; + link = e.payload?.issue?.html_url; + } + + // Issue Closed + else if (e.type === "IssuesEvent" && e.payload.action === "closed") { + const issueNumber = e.payload?.issue?.number || ""; + text = ` Closed issue #${issueNumber} in ${e.repo.name}`; + link = e.payload?.issue?.html_url; + } + // Push + else if (e.type === "PushEvent") { + text = ` Pushed code to ${e.repo.name}`; + link = `https://github.com/${e.repo.name}`; + } + + else return null; + + return ( + + {text} + + ({timeAgo(e.created_at)}) + + + ); + })} +
+ )} +
+ + + + {/* GITHUB */} + + Open On Github + + +
+ ); +} \ No newline at end of file diff --git a/src/pages/GraphPage.tsx b/src/pages/GraphPage.tsx index 129df77..228c543 100644 --- a/src/pages/GraphPage.tsx +++ b/src/pages/GraphPage.tsx @@ -6,7 +6,7 @@ export default function GraphPage() { if (!repos.length) { return (
- No data found 🚫
+ No data found
Please analyze organizations first.
); @@ -16,14 +16,14 @@ export default function GraphPage() {

- Contributor Collaboration Network 🌐 + Contributor Collaboration Network

-

+ {/*

Visualizes relationships between repositories and contributors across multiple organizations. -

+

*/}
); } diff --git a/src/pages/Overview.tsx b/src/pages/Overview.tsx index fbcb747..9d8d0d7 100644 --- a/src/pages/Overview.tsx +++ b/src/pages/Overview.tsx @@ -8,16 +8,16 @@ import { getInsights } from "../utils/insightEngine"; import HealthScore from "../components/HealthScore"; import { calculateOrgHealthScore } from "../utils/calculateScore"; import type { Repo, Insight } from "../types/github"; -import { generateInsights } from "../utils/insights"; +// import { generateInsights } from "../utils/insights"; +import TopContributors from "../components/TopContributors"; - -// PROPS TYPE type Props = { orgInput: string; setOrgInput: React.Dispatch>; + setOrgLogo: React.Dispatch>; }; -export default function Overview({ orgInput, setOrgInput }: Props) { +export default function Overview({ orgInput, setOrgInput, setOrgLogo }: Props) { const [repos, setRepos] = useState([]); const [data, setData] = useState(null); @@ -25,10 +25,16 @@ export default function Overview({ orgInput, setOrgInput }: Props) { const { score, label } = calculateOrgHealthScore(repos); - // RESTORE DATA + // DEFAULT LOAD useEffect(() => { const savedRepos = localStorage.getItem("repos"); const savedInput = localStorage.getItem("orgInput"); + const savedLogo = localStorage.getItem("orgLogo"); + + if (!savedInput) { + handleSubmit("AOSSIE-Org"); // default + return; + } if (savedRepos) { const parsed: Repo[] = JSON.parse(savedRepos); @@ -37,23 +43,37 @@ export default function Overview({ orgInput, setOrgInput }: Props) { } if (savedInput) { - setOrgInput(savedInput); // sync with topbar + setOrgInput(savedInput); + } + + if (savedLogo) { + setOrgLogo(savedLogo); // GLOBAL SET } - }, [setOrgInput]); - // ANALYZE + }, []); + + // ANALYZE const handleSubmit = async (input: string) => { setLoading(true); + try { - setOrgInput(input); // GLOBAL UPDATE + setOrgInput(input); - const orgs = input.split(",").map(o => o.trim()); + const orgs = input.split(",").map(o => o.trim()).filter(Boolean); + // FAST LOGO (NO WAIT) + fetch(`https://api.github.com/orgs/${orgs[0]}`) + .then(res => res.json()) + .then(data => { + setOrgLogo(data.avatar_url); + localStorage.setItem("orgLogo", data.avatar_url); + }) + .catch(() => setOrgLogo("")); + + // MULTI ORG DATA const results = await Promise.all(orgs.map(fetchOrgRepos)); const merged: Repo[] = mergeRepos(results); - console.log("TOTAL REPOS:", merged.length); - setRepos(merged); localStorage.setItem("repos", JSON.stringify(merged)); @@ -69,50 +89,35 @@ export default function Overview({ orgInput, setOrgInput }: Props) { setLoading(false); }; - const insights = generateInsights(repos); - return (
- {/* INPUT */} - {/* LOADING */} {loading &&

Loading...

} {data && } -
-

- Insights -

- -
    - {insights.map((insight, i) => ( -
  • • {insight}
  • - ))} -
-
-
- - {/* CHART + EXPORT */} + {/* CHART */} {repos.length > 0 && ( -
- - {/* CHART */} - + o.trim()).filter(Boolean)} + /> + )} -
+ {/* CONTRIBUTORS */} + {repos.length > 0 && ( + )}
); -} +} \ No newline at end of file diff --git a/src/pages/RepoDetails.tsx b/src/pages/RepoDetails.tsx new file mode 100644 index 0000000..b4f70eb --- /dev/null +++ b/src/pages/RepoDetails.tsx @@ -0,0 +1,182 @@ +import { useLocation } from "react-router-dom"; +import { useEffect, useState } from "react"; +import { + AreaChart, + Area, + XAxis, + YAxis, + Tooltip, + ResponsiveContainer +} from "recharts"; +import { FaCodeBranch, FaGithub } from "react-icons/fa"; +import { IoIosStarOutline } from "react-icons/io"; +import { MdOutlineReportGmailerrorred } from "react-icons/md"; +; + +const TOKEN = import.meta.env.VITE_GITHUB_TOKEN; + +export default function RepoDetails() { + const { state } = useLocation(); + const repo = state; + + const [contributors, setContributors] = useState([]); + const [chartData, setChartData] = useState([]); + + useEffect(() => { + if (!repo) return; + + fetchContributors(); + fetchActivity(); + }, []); + + // CONTRIBUTORS + const fetchContributors = async () => { + const res = await fetch(repo.contributors_url, { + headers: { Authorization: `token ${TOKEN}` } + }); + const data = await res.json(); + setContributors(data.slice(0, 4)); + }; + + // ACTIVITY CHART + const fetchActivity = async () => { + const res = await fetch( + `https://api.github.com/repos/${repo.full_name}/issues?per_page=100`, + { headers: { Authorization: `token ${TOKEN}` } } + ); + + const data = await res.json(); + + const map: any = {}; + + data.forEach((item: any) => { + const d = new Date(item.created_at) + .toISOString() + .split("T")[0]; + + if (!map[d]) map[d] = { date: d, issues: 0 }; + + map[d].issues++; + }); + + const result = Object.values(map); + setChartData(result); + }; + + if (!repo) { + return

No repo data

; + } + + const formatDate = (date: string) => { + const d = new Date(date); + return d.toLocaleDateString("en-US", { + month: "short", + day: "numeric", + }); + }; + + return ( +
+ + {/* TITLE */} +

+ {repo.name} +

+ + {/* STAT CARDS */} +
+ + {/* STARS */} +
+ +
+

Stars

+

{repo.stargazers_count}

+
+
+ + {/* FORKS */} +
+ +
+

Forks

+

{repo.forks_count}

+
+
+ + {/* ISSUES */} +
+ window.open(repo.html_url + "/issues", "_blank") + } + > + +
+

Issues

+

{repo.open_issues_count}

+
+
+ +
+ + {/* AREA CHART */} +
+

Issues Activity

+ + + + + + + + + + +
+ + {/* CONTRIBUTORS */} +
+

Top Contributors

+ +
+ {contributors.map((c: any) => ( + +
window.open(c.html_url, "_blank")} + className="flex items-center gap-3 bg-gray-800 p-3 rounded cursor-pointer hover:bg-gray-700 transition" + > + + +
+

{c.login}

+

+ Contributions: {c.contributions} +

+
+
+ ))} +
+
+ + {/* GITHUB LINK */} + + +
+ ); +} \ No newline at end of file diff --git a/src/pages/Repositories.tsx b/src/pages/Repositories.tsx index 7fa2352..718bf04 100644 --- a/src/pages/Repositories.tsx +++ b/src/pages/Repositories.tsx @@ -1,5 +1,4 @@ import RepoTable from "../components/Tables/RepoTable"; -import { exportCSV } from "../utils/exportCSV"; export default function Repositories() { const repos = JSON.parse(localStorage.getItem("repos") || "[]"); @@ -7,9 +6,9 @@ export default function Repositories() { return ( <>
-

+ {/*

Repositories -

+ */} {!repos.length ? (

@@ -19,14 +18,7 @@ export default function Repositories() { )}

- {/* EXPORT */} - - + ); } \ No newline at end of file diff --git a/src/services/githubService.ts b/src/services/githubService.ts index 287508d..0f2c54e 100644 --- a/src/services/githubService.ts +++ b/src/services/githubService.ts @@ -22,4 +22,32 @@ export const fetchRepoContributors = async (url: string) => { if (!res.ok) throw new Error("Contributor API Error"); return res.json(); -}; \ No newline at end of file +}; + +export const fetchRepoIssues = async (owner: string, repo: string) => { + const res = await fetch( + `https://api.github.com/repos/${owner}/${repo}/issues?state=all&per_page=100`, + { + headers: { + Authorization: `token ${TOKEN}` + } + } + ); + + if (!res.ok) throw new Error("Issues API Error"); + return res.json(); +}; + +// Fetch PRs of repo +export const fetchRepoPRs = async (owner: string, repo: string) => { + const res = await fetch( + `${BASE}/repos/${owner}/${repo}/pulls?state=all&per_page=100`, + { + headers: { Authorization: `token ${TOKEN}` } + } + ); + + if (!res.ok) return []; + return res.json(); +}; + diff --git a/src/utils/calculateScore.ts b/src/utils/calculateScore.ts index ccc87bb..69a71b6 100644 --- a/src/utils/calculateScore.ts +++ b/src/utils/calculateScore.ts @@ -40,9 +40,9 @@ export const calculateOrgHealthScore = (repos: any[]) => { score = Math.round(score); let label = "Poor"; - if (score > 75) label = "Excellent 🚀"; - else if (score > 50) label = "Good 👍"; - else if (score > 30) label = "Average ⚠️"; + if (score > 75) label = "Excellent "; + else if (score > 50) label = "Good "; + else if (score > 30) label = "Average "; return { score, label }; }; \ No newline at end of file diff --git a/src/utils/exportCSV.ts b/src/utils/exportCSV.ts index 573190b..80b1441 100644 --- a/src/utils/exportCSV.ts +++ b/src/utils/exportCSV.ts @@ -1,15 +1,57 @@ -export const exportCSV = (repos: any[]) => { - const rows = repos.map(r => - `${r.name},${r.stargazers_count},${r.forks_count}` +export const exportCSV = (graphData: any) => { + const { nodes, links } = graphData; + + // Repos + const repoRows = nodes + .filter((n: any) => n.type === "repo") + .map((r: any) => + `${r.label},${r.stars || 0},${r.forks || 0},${r.issues || 0}` + ); + + // Contributors + const userRows = nodes + .filter((n: any) => n.type === "user") + .map((u: any) => + `${u.label},${u.contributions || 0}` + ); + + // Connections + const getId = (val: any) => + typeof val === "object" ? val.id : val; + + const linkRows = links.map((l: any) => + `${getId(l.source)},${getId(l.target)},${l.weight}` ); - const csv = "Name,Stars,Forks\n" + rows.join("\n"); + const parsedRows: string[][] = userRows.map((r: string) => r.split(",")); + + const topContributor: string[] | undefined = parsedRows + .sort((a: string[], b: string[]) => Number(b[1]) - Number(a[1]))[0]; + const csvContent = + // REPO SECTION + "=== REPOSITORIES ===\n" + + "Name,Stars,Forks,Issues\n" + + repoRows.join("\n") + - const blob = new Blob([csv]); + "\n\n=== CONTRIBUTORS ===\n" + + "Name,Contributions\n" + + userRows.join("\n") + + + "\n\n=== CONNECTIONS ===\n" + + "Contributor,Repository,Weight\n" + + linkRows.join("\n") + + + "\n\n=== INSIGHTS ===\n" + + `Top Contributor,${topContributor?.[0]}\n`; + + // Download + const blob = new Blob([csvContent], { type: "text/csv;charset=utf-8;" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; - a.download = "repos.csv"; + a.download = "github-analysis.csv"; a.click(); + + URL.revokeObjectURL(url); }; \ No newline at end of file diff --git a/src/utils/insightEngine.ts b/src/utils/insightEngine.ts index 3848389..5ea30c4 100644 --- a/src/utils/insightEngine.ts +++ b/src/utils/insightEngine.ts @@ -49,9 +49,9 @@ export const getInsights = (repos: Repo[]): InsightResult => { const ratio = totalForks / totalStars; if (ratio > 0.6) { - insights.push("🔁 High fork-to-star ratio → strong developer engagement"); + insights.push(" High fork-to-star ratio → strong developer engagement"); } else if (ratio < 0.2) { - insights.push("📉 Low fork activity compared to stars"); + insights.push(" Low fork activity compared to stars"); } } @@ -62,9 +62,9 @@ export const getInsights = (repos: Repo[]): InsightResult => { ); if (recent.length > repos.length * 0.5) { - insights.push("🚀 High recent activity across repositories"); + insights.push(" High recent activity across repositories"); } else if (recent.length < repos.length * 0.2) { - insights.push("🐢 Low recent activity → possible slowdown"); + insights.push(" Low recent activity → possible slowdown"); } // Repo size distribution insight diff --git a/src/utils/insights.ts b/src/utils/insights.ts index 2565c00..7696436 100644 --- a/src/utils/insights.ts +++ b/src/utils/insights.ts @@ -1,63 +1,63 @@ -export function generateInsights(repos: any[]) { - if (!repos || repos.length === 0) return []; - - const insights: string[] = []; - - // Most starred repo - const topRepo = [...repos].sort( - (a, b) => b.stargazers_count - a.stargazers_count - )[0]; - - insights.push(`⭐ Most popular repo: ${topRepo.name}`); - - // Low activity repos - const lowActivity = repos.filter((r) => r.stargazers_count < 5); - if (lowActivity.length > 0) { - insights.push(`⚠️ ${lowActivity.length} repos have very low stars`); - } - - // Fork heavy repos - const forkHeavy = repos.filter((r) => r.forks_count > r.stargazers_count); - if (forkHeavy.length > 0) { - insights.push(`🍴 Some repos are fork-heavy but not popular`); - } - - // Recently updated - const recent = repos.filter((r) => { - const updated = new Date(r.updated_at); - const now = new Date(); - return (now.getTime() - updated.getTime()) / (1000 * 60 * 60 * 24) < 30; - }); - - if (recent.length > repos.length / 2) { - insights.push(`🚀 Org is actively maintained (many recent updates)`); - } - - // Stale repos - const stale = repos.filter((r) => { - const updated = new Date(r.updated_at); - const now = new Date(); - return (now.getTime() - updated.getTime()) / (1000 * 60 * 60 * 24) > 180; - }); - - if (stale.length > 0) { - insights.push(`💀 ${stale.length} repos are stale (>6 months no updates)`); - } - - // Language dominance - const langMap: any = {}; - repos.forEach((r) => { - if (!r.language) return; - langMap[r.language] = (langMap[r.language] || 0) + 1; - }); - - const topLang = Object.keys(langMap).sort( - (a, b) => langMap[b] - langMap[a] - )[0]; - - if (topLang) { - insights.push(`💻 Most used language: ${topLang}`); - } - - return insights; -} \ No newline at end of file +// export function generateInsights(repos: any[]) { +// if (!repos || repos.length === 0) return []; + +// const insights: string[] = []; + +// // Most starred repo +// const topRepo = [...repos].sort( +// (a, b) => b.stargazers_count - a.stargazers_count +// )[0]; + +// insights.push(` Most popular repo: ${topRepo.name}`); + +// // Low activity repos +// const lowActivity = repos.filter((r) => r.stargazers_count < 5); +// if (lowActivity.length > 0) { +// insights.push(` ${lowActivity.length} repos have very low stars`); +// } + +// // Fork heavy repos +// const forkHeavy = repos.filter((r) => r.forks_count > r.stargazers_count); +// if (forkHeavy.length > 0) { +// insights.push(` Some repos are fork-heavy but not popular`); +// } + +// // Recently updated +// const recent = repos.filter((r) => { +// const updated = new Date(r.updated_at); +// const now = new Date(); +// return (now.getTime() - updated.getTime()) / (1000 * 60 * 60 * 24) < 30; +// }); + +// if (recent.length > repos.length / 2) { +// insights.push(` Org is actively maintained (many recent updates)`); +// } + +// // Stale repos +// const stale = repos.filter((r) => { +// const updated = new Date(r.updated_at); +// const now = new Date(); +// return (now.getTime() - updated.getTime()) / (1000 * 60 * 60 * 24) > 180; +// }); + +// if (stale.length > 0) { +// insights.push(` ${stale.length} repos are stale (>6 months no updates)`); +// } + +// // Language dominance +// const langMap: any = {}; +// repos.forEach((r) => { +// if (!r.language) return; +// langMap[r.language] = (langMap[r.language] || 0) + 1; +// }); + +// const topLang = Object.keys(langMap).sort( +// (a, b) => langMap[b] - langMap[a] +// )[0]; + +// if (topLang) { +// insights.push(` Most used language: ${topLang}`); +// } + +// return insights; +// } \ No newline at end of file From f94934806811dc11434bfd7fef28537fbfabf57b Mon Sep 17 00:00:00 2001 From: yanamavani Date: Mon, 20 Apr 2026 05:10:15 +0530 Subject: [PATCH 4/5] remove unused sidebar menu option --- src/layout/Sidebar.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/layout/Sidebar.tsx b/src/layout/Sidebar.tsx index 451c955..acb1063 100644 --- a/src/layout/Sidebar.tsx +++ b/src/layout/Sidebar.tsx @@ -49,9 +49,6 @@ export default function Sidebar() {
-
- ⚙ Settings -
); From 059dc85c75ce118a5d570780f2bbed71a900e59a Mon Sep 17 00:00:00 2001 From: yanamavani Date: Tue, 21 Apr 2026 01:30:48 +0530 Subject: [PATCH 5/5] removed unused commented lines --- note.txt | 2836 ----------------------------------------- note2.txt | 297 ----- src/utils/insights.ts | 63 - 3 files changed, 3196 deletions(-) delete mode 100644 note.txt delete mode 100644 note2.txt delete mode 100644 src/utils/insights.ts diff --git a/note.txt b/note.txt deleted file mode 100644 index b943938..0000000 --- a/note.txt +++ /dev/null @@ -1,2836 +0,0 @@ -components/Cards/statCard.tsx = -interface Props { - title: string; - value: string | number; -} - -export default function StatCard({ title, value }: Props) { - return ( -
-

{title}

-

{value}

-
- ); -} -components/Charts/ActivityChart.tsx = - - -import { useEffect, useState } from "react"; -import { - LineChart, - Line, - XAxis, - YAxis, - Tooltip, - Legend, - ResponsiveContainer, - CartesianGrid, -} from "recharts"; - -let timeout: any; -const TOKEN = import.meta.env.VITE_GITHUB_TOKEN; - -export default function ActivityChart({ orgs }: { orgs: string[] }) { - const [data, setData] = useState([]); - const [filter, setFilter] = useState("30"); - - const getDays = () => { - return filter === "7" ? 7 : 30; - }; - - // useEffect(() => { - // if (!orgs || orgs.length === 0) return; - // fetchData(); - // }, [orgs, filter]); - useEffect(() => { - if (!orgs || orgs.length === 0) return; - - clearTimeout(timeout); - - timeout = setTimeout(() => { - fetchData(); - }, 800); // wait 0.8 sec - - }, [filter, orgs]); - - const fetchData = async () => { - try { - const days = getDays(); - const now = new Date(); - - // 🔥 PARALLEL FETCH (FAST) - const requests = orgs.flatMap(org => [ - fetch(`https://api.github.com/search/issues?q=org:${org}+type:pr&per_page=100`, { - headers: { Authorization: `token ${TOKEN}` } - }).then(res => res.json()), - - fetch(`https://api.github.com/search/issues?q=org:${org}+type:issue&per_page=100`, { - headers: { Authorization: `token ${TOKEN}` } - }).then(res => res.json()) - ]); - - const results = await Promise.all(requests); - - let allPRs: any[] = []; - let allIssues: any[] = []; - - // 🔥 SPLIT RESULTS - // results.forEach((res, i) => { - // if (i % 2 === 0) { - // if (Array.isArray(res.items)) allPRs.push(...res.items); - // } else { - // if (Array.isArray(res.items)) allIssues.push(...res.items); - // } - // }); - results.forEach((res, i) => { - if (res && res.items && Array.isArray(res.items)) { - if (i % 2 === 0) { - allPRs.push(...res.items); - } else { - allIssues.push(...res.items); - } - } else { - console.warn("Invalid org or API error", res); - } - }); - const chartMap: any = {}; - - // PRs - allPRs.forEach((pr: any) => { - const date = new Date(pr.created_at); - const diff = (now.getTime() - date.getTime()) / (1000 * 60 * 60 * 24); - - if (diff <= days) { - const key = date.toLocaleDateString("en-CA"); // YYYY-MM-DD format - - if (!chartMap[key]) { - chartMap[key] = { - date: key, - prCreated: 0, - prMerged: 0, - issuesCreated: 0, - issuesClosed: 0 - }; - } - - chartMap[key].prCreated++; - - if (pr.state === "closed") { - chartMap[key].prMerged++; - } - } - }); - - // Issues - allIssues.forEach((issue: any) => { - const date = new Date(issue.created_at); - const diff = (now.getTime() - date.getTime()) / (1000 * 60 * 60 * 24); - - if (diff <= days) { - // const key = date.toISOString().split("T")[0]; - const key = date.toLocaleDateString("en-CA"); - - if (!chartMap[key]) { - chartMap[key] = { - date: key, - prCreated: 0, - prMerged: 0, - issuesCreated: 0, - issuesClosed: 0 - }; - } - - chartMap[key].issuesCreated++; - - if (issue.state === "closed") { - chartMap[key].issuesClosed++; - } - } - }); - - // const finalData = Object.values(chartMap).sort( - // (a: any, b: any) => - // new Date(a.date).getTime() - new Date(b.date).getTime() - // ); - - // setData(finalData); - const result: any[] = []; - - for (let i = days; i >= 0; i--) { - const d = new Date(); - d.setDate(d.getDate() - i); - - // const key = d.toISOString().split("T")[0]; - const key = d.toLocaleDateString("en-CA"); - - result.push({ - date: key, - prCreated: chartMap[key]?.prCreated || 0, - prMerged: chartMap[key]?.prMerged || 0, - issuesCreated: chartMap[key]?.issuesCreated || 0, - issuesClosed: chartMap[key]?.issuesClosed || 0 - }); - } - - setData(result); - - } catch (err) { - console.error(err); - } - }; - - const formatDate = (date: string) => { - const d = new Date(date); - return d.toLocaleDateString("en-US", { - month: "short", - day: "numeric", - }); - }; - const orgCount = orgs.length; - return ( -
- - {/* FILTER */} -
- {/* - - */} - - - - - {/* */} -
- - {/* CHART TITLE */} - {/*

- 📊 Combined Activity (All Organizations) -

*/} -

- {orgCount > 1 - ? `📊 Combined Activity (${orgCount} Organizations)` - : `📊 Activity (${orgs[0] || "Organization"})`} -

- {/* AREA CHART */} - - - - - - - - - - - - - - - - - - {/* */} - - - - - {/* SIMPLE CLEAN LINES */} - - - - - - - - - - - -
- ); -} -============================================================================================= -components/Graph/NetworkGraph.tsx = -import ForceGraph2D from "react-force-graph-2d"; -import { useEffect, useRef, useState } from "react"; -import { fetchRepoContributors } from "../../services/githubService"; -import { exportCSV } from "../../utils/exportCSV"; -import { GoPeople, GoRepo, GoGitBranch, GoLink } from "react-icons/go"; -import { FaLink } from "react-icons/fa"; -import { IoPeople } from "react-icons/io5"; - - -type NodeType = { - id: string; - type: "repo" | "user"; - label: string; - img?: string; - - stars?: number; - forks?: number; - issues?: number; - - contributions?: number; - activity?: number; - - size?: number; - org?: string; -}; - -type LinkType = { - source: string; - target: string; - weight: number; -}; - -export default function NetworkGraph({ repos }: any) { - const fgRef = useRef(null); - - const [graphData, setGraphData] = useState<{ - nodes: NodeType[]; - links: LinkType[]; - }>({ nodes: [], links: [] }); - - const [selectedNode, setSelectedNode] = useState(null); - const [stats, setStats] = useState({ - users: 0, - repos: 0, - edges: 0, - }); - const [hoverNode, setHoverNode] = useState(null); - const [focusNode, setFocusNode] = useState(null); - - /* ───────── BUILD GRAPH (MULTI ORG FIX) ───────── */ - useEffect(() => { - if (!repos?.length) return; - - const build = async () => { - const nodes: NodeType[] = []; - const links: LinkType[] = []; - const userMap = new Map(); - - // GROUP BY ORG - const orgGrouped: Record = {}; - - repos.forEach((repo: any) => { - const org = repo.full_name.split("/")[0]; - if (!orgGrouped[org]) orgGrouped[org] = []; - orgGrouped[org].push(repo); - }); - - // TAKE 4 REPOS PER ORG - const selectedRepos: any[] = []; - Object.values(orgGrouped).forEach((list: any[]) => { - selectedRepos.push(...list.slice(0, 4)); - }); - - for (const repo of selectedRepos) { - const org = repo.full_name.split("/")[0]; - - // REPO NODE - nodes.push({ - id: repo.full_name, - type: "repo", - label: repo.name, - org, - stars: repo.stargazers_count, - forks: repo.forks_count, - issues: repo.open_issues_count, - activity: new Date(repo.updated_at).getTime(), - size: - Math.log((repo.stargazers_count || 1) + 1) * 4 + - Math.log((repo.forks_count || 1) + 1) * 2 + - 8, - }); - - try { - const contributors = await fetchRepoContributors( - `${repo.contributors_url}?per_page=30` - ); - - contributors.slice(0, 15).forEach((c: any) => { - if (!c?.login) return; - - if (!userMap.has(c.login)) { - userMap.set(c.login, true); - - nodes.push({ - id: c.login, - type: "user", - label: c.login, - img: c.avatar_url, - contributions: c.contributions, - activity: c.contributions, - size: Math.log((c.contributions || 1) + 1) * 3 + 6, - }); - } - - links.push({ - source: c.login, - target: repo.full_name, - weight: c.contributions || 1, - }); - }); - } catch (err) { - console.log(err); - } - } - setStats({ - users: nodes.filter(n => n.type === "user").length, - repos: nodes.filter(n => n.type === "repo").length, - edges: links.length, - }); - setGraphData({ nodes, links }); - }; - - - build(); - }, [repos]); - - - /* ───────── FORCE LAYOUT (MULTI ORG FIXED) ───────── */ - useEffect(() => { - if (!fgRef.current) return; - - const fg = fgRef.current; - - fg.d3Force("charge").strength(-320); - - fg.d3Force("link").distance((l: any) => - Math.max(80, 200 - Math.log2(l.weight + 1) * 25) - ); - - // ORGS - const orgs: string[] = Array.from( - new Set( - graphData.nodes - .filter((n) => n.type === "repo" && n.org) - .map((n) => n.org as string) - ) - ); - - const orgMap: Record = {}; - const gap = 400; - - orgs.forEach((org, i) => { - orgMap[org] = (i - (orgs.length - 1) / 2) * gap; - }); - - // X FORCE - fg.d3Force("x", (node: any) => { - if (node.type === "repo") { - return node.org ? orgMap[node.org] ?? 0 : 0; - } - - const linked = graphData.links.find((l) => l.source === node.id); - - if (!linked) return 0; - - const repoNode = graphData.nodes.find( - (n) => n.id === linked.target - ) as NodeType | undefined; - - if (!repoNode || !repoNode.org) return 0; - - return orgMap[repoNode.org] ?? 0; - }); - - // Y FORCE (activity) - fg.d3Force("y", (node: any) => { - const act = node.activity || 1; - const norm = Math.min(1, Math.log(act + 1) / 10); - - return node.type === "repo" - ? -300 * norm - : 300 * (1 - norm); - }); - }, [graphData]); - - // ======================================================================================== - const isConnectedToFocus = (nodeId: string) => { - if (!focusNode) return true; - - const focusId = focusNode.id; - - return graphData.links.some((l: any) => { - const s = typeof l.source === "object" ? l.source.id : l.source; - const t = typeof l.target === "object" ? l.target.id : l.target; - - return ( - (s === nodeId && t === focusId) || - (t === nodeId && s === focusId) - ); - }); - }; - // ======================================================= - /* ───────── NODE DRAW ───────── */ - - const drawNode = (node: any, ctx: CanvasRenderingContext2D, scale: number) => { - const padding = 6; - - // dynamic width based on text - const label = node.label || ""; - ctx.font = `${9 / scale}px Arial`; - const textWidth = ctx.measureText(label).width; - - const width = Math.max(80, textWidth + 40); // auto width - const height = 52; - - const x = node.x - width / 2; - const y = node.y - height / 2; - - // helper for id (IMPORTANT FIX) - const getId = (val: any) => - typeof val === "object" ? val.id : val; - - let opacity = 1; - - // PRIORITY: FOCUS MODE - if (focusNode) { - const isConnected = isConnectedToFocus(node.id); - - if (node.id !== focusNode.id && !isConnected) { - opacity = 0.1; - } - } - - // SECOND: HOVER MODE - else if (hoverNode) { - const getId = (val: any) => - typeof val === "object" ? val.id : val; - - const isConnected = graphData.links.some((l: any) => { - const s = getId(l.source); - const t = getId(l.target); - - return ( - (s === node.id && t === hoverNode.id) || - (t === node.id && s === hoverNode.id) - ); - }); - - if (node.id !== hoverNode.id && !isConnected) { - opacity = 0.1; - } - } - - ctx.globalAlpha = opacity; - - // CARD BACKGROUND (glass style) - ctx.fillStyle = "rgba(15, 23, 42, 0.9)"; - ctx.strokeStyle = node.type === "repo" ? "#facc15" : "#22c55e"; - ctx.lineWidth = 1.5; - - ctx.beginPath(); - ctx.roundRect(x, y, width, height, 10); - ctx.fill(); - ctx.stroke(); - - // avatar - if (node.img) { - const img = new Image(); - img.src = node.img; - - ctx.save(); - ctx.beginPath(); - ctx.roundRect(x + padding, y + padding, 22, 22, 4); - ctx.clip(); - ctx.drawImage(img, x + padding, y + padding, 22, 22); - ctx.restore(); - } - - // TEXT CLIP (IMPORTANT — no overflow) - ctx.save(); - ctx.beginPath(); - ctx.rect(x + 30, y + 5, width - 35, 20); - ctx.clip(); - - ctx.fillStyle = "#e5e7eb"; - ctx.fillText(label, x + 30, y + 18); - ctx.restore(); - - // 📊 contributions / stats - if (node.contributions) { - ctx.fillStyle = "#22c55e"; - ctx.font = `${8 / scale}px Arial`; - ctx.fillText(`${node.contributions} commits`, x + 30, y + 35); - } - - if (node.type === "repo") { - ctx.fillStyle = "#facc15"; - ctx.font = `${8 / scale}px Arial`; - ctx.fillText("repo", x + 30, y + 35); - } - - ctx.globalAlpha = 1; - - if (focusNode) { - const isConnected = isConnectedToFocus(node.id); - - if (node.id !== focusNode.id && !isConnected) { - opacity = 0.1; // dim others - } - } - }; - /* ───────── LINK DRAW ───────── */ - - const topContributor = graphData.nodes - .filter(n => n.type === "user") - .sort((a, b) => (b.contributions || 0) - (a.contributions || 0))[0]; - - const mostActiveRepo = graphData.nodes - .filter(n => n.type === "repo") - .sort((a, b) => (b.activity || 0) - (a.activity || 0))[0]; - - const strongestLink = graphData.links - .sort((a, b) => b.weight - a.weight)[0]; - - const getId = (val: any) => - typeof val === "object" ? val.id : val; - - // TOTAL UNIQUE CONTRIBUTORS - const totalUniqueContributors = new Set( - graphData.nodes - .filter(n => n.type === "user") - .map(n => n.id) - ).size; - - - // SHARED CONTRIBUTORS - const contributorOrgs: Record> = {}; - - graphData.links.forEach((l: any) => { - const user = typeof l.source === "object" ? l.source.id : l.source; - const repo = graphData.nodes.find(n => n.id === l.target); - - if (!repo?.org) return; - - if (!contributorOrgs[user]) { - contributorOrgs[user] = new Set(); - } - - contributorOrgs[user].add(repo.org); - }); - - const sharedContributors = Object.values(contributorOrgs).filter( - (orgSet) => orgSet.size > 1 - ).length; - - const drawLink = (link: any, ctx: CanvasRenderingContext2D) => { - const getId = (val: any) => - typeof val === "object" ? val.id : val; - - const s = link.source; - const t = link.target; - - if (!s?.x || !t?.x) return; - - let opacity = 0.3; - - if (focusNode) { - const sourceId = getId(link.source); - const targetId = getId(link.target); - - if (sourceId === focusNode.id || targetId === focusNode.id) { - opacity = 1; - } else { - opacity = 0.05; - } - } else if (hoverNode) { - const sourceId = getId(link.source); - const targetId = getId(link.target); - - if (sourceId === hoverNode.id || targetId === hoverNode.id) { - opacity = 1; - } else { - opacity = 0.05; - } - } - - ctx.beginPath(); - ctx.moveTo(s.x, s.y); - ctx.lineTo(t.x, t.y); - - ctx.strokeStyle = `rgba(34,197,94,${opacity})`; - ctx.lineWidth = Math.max(1, Math.log2(link.weight + 1)); - - ctx.stroke(); - }; - /* ───────── UI ───────── */ - - return ( -
- -
- - {/* LEFT SIDE (FULL WIDTH STATS) */} -
- -
- {stats.users} Contributors -
- -
- {stats.repos} Repos -
- -
- {stats.edges} Links -
- -
- {totalUniqueContributors} Total -
- -
- {sharedContributors} Shared -
- -
- Top Contributor : {topContributor?.label} -
- -
- Most Active Repo : {mostActiveRepo?.label} -
- -
- {getId(strongestLink?.source)} → {getId(strongestLink?.target)} -
- -
- - {/* RIGHT SIDE BUTTON */} - - -
- - {selectedNode && ( -
- - - -
- {selectedNode.img && ( - - )} -
-

{selectedNode.label}

-

- {selectedNode.type === "user" ? "Contributor" : "Repository"} -

-
-
- - {selectedNode.type === "user" ? ( - <> -

Commits: {selectedNode.contributions}

- - ) : ( - <> -

⭐ Stars: {selectedNode.stars}

-

🍴 Forks: {selectedNode.forks}

- - )} - - - Open GitHub → - -
- - - )} - - { - const width = 100; - const height = 60; - - ctx.fillStyle = color; - ctx.fillRect( - node.x - width / 2, - node.y - height / 2, - width, - height - ); - }} - - onNodeHover={(node: any) => { - setHoverNode(node); - document.body.style.cursor = node ? "pointer" : "default"; - }} - - onNodeClick={(node: any) => { - setSelectedNode(node); - setFocusNode(node); - }} - - linkDirectionalParticles={2} - linkDirectionalParticleSpeed={0.004} - - onEngineStop={() => fgRef.current?.zoomToFit(400)} - /> -
- ); -} - -============================================================================================ -components/Input/OrgInput.tsx == -export default function OrgInput({ onSubmit, value, setValue }: any) { - return ( -
- setValue(e.target.value)} - /> - - -
- ); -} -============================================================================================== -components/Insights/Insightpanel.tsx == - -import StatCard from "../Cards/statCard"; -import { IoIosStarOutline } from "react-icons/io" -import { GoRepo } from "react-icons/go"; -import { useNavigate } from "react-router-dom"; - -export default function InsightPanel({ data }: { data: any }) { - const navigate = useNavigate(); - return ( - <> -
- - - - -
-
-

High Activity Repositories

- -
- - - {data.topRepos.map((repo: any, i: number) => ( -
navigate(`/repo/${repo.name}`, { state: repo })} - className="flex justify-between items-center p-2 rounded hover:bg-gray-800 transition cursor-pointer" - > - - {repo.name} - - - - {repo.stargazers_count} - -
- ))} -
-
- -
-

- Key Insights -

- -
- {data.insights.map((insight: string, i: number) => ( -
- {insight} -
- ))} -
-
- - ); -} - - - -=========================================================================== -components/Tables/RepoTable.tsx == -import { useState } from "react"; -import { FaCodeBranch } from "react-icons/fa"; -import { MdOutlineReportGmailerrorred } from "react-icons/md"; -import { useNavigate } from "react-router-dom"; -import { IoIosStarOutline } from "react-icons/io"; - -console.log("Rendering RepoTable component..."); -export default function RepoTable({ repos }: any) { - const [sortKey, setSortKey] = useState("stars"); - const [search, setSearch] = useState(""); - const [language, setLanguage] = useState("all"); - - const navigate = useNavigate(); - - // 🔥 UNIQUE LANGUAGES - const languages = [ - "all", - ...new Set(repos.map((r: any) => r.language).filter(Boolean)), - ]; - - // 🔥 FILTER + SEARCH - const filtered = repos.filter((r: any) => { - const matchSearch = - r.name.toLowerCase().includes(search.toLowerCase()) || - (r.language || "").toLowerCase().includes(search.toLowerCase()); - - const matchLang = - language === "all" || r.language === language; - - return matchSearch && matchLang; - }); - - // 🔥 SORT - const sorted = [...filtered].sort((a, b) => { - if (sortKey === "stars") return b.stargazers_count - a.stargazers_count; - if (sortKey === "forks") return b.forks_count - a.forks_count; - if (sortKey === "issues") return b.open_issues_count - a.open_issues_count; - return 0; - }); - - // 🔥 STATUS - const getStatus = (updated_at: string) => { - const days = - (Date.now() - new Date(updated_at).getTime()) / - (1000 * 60 * 60 * 24); - - return days < 30 ? "Active" : "Inactive"; - }; - - return ( -
- - {/* 🔥 TOP BAR */} -
- - {/* TITLE */} -

- Repositories ({sorted.length}) -

- - {/* SEARCH */} - setSearch(e.target.value)} - className="px-3 py-2 rounded bg-gray-800 text-white border border-gray-600" - /> - - {/* LANGUAGE FILTER */} - -
- - {/* 🔥 SORT BUTTONS */} -
- - - - - -
- - {/* 🔥 TABLE */} -
- - - - - - - - - - - - - - - - {sorted.map((r: any) => ( - navigate(`/repo/${r.name}`, { state: r })} - className="border-t border-gray-700 hover:bg-gray-800 transition cursor-pointer" - > - {/* 1. REPO */} - - - {/* 2. STARS */} - - - {/* 3. FORKS */} - - - {/* 4. ISSUES */} - - - {/* 5. LANGUAGE */} - - - {/* 6. LAST UPDATED */} - - - {/* 7. STATUS */} - - - ))} - -
Repository Stars Forks IssuesLanguageLast UpdatedStatus
- {r.name} -
- {r.description || "No description"} -
-
-
- {r.stargazers_count} -
-
-
- {r.forks_count} -
-
-
- {r.open_issues_count || 0} -
-
- - {r.language || "N/A"} - - - {new Date(r.updated_at).toLocaleDateString()} - - {getStatus(r.updated_at).includes("Active") ? ( - 🟢 Active - ) : ( - 🔴 Inactive - )} -
-
-
- ); -} -========================================================= -src/components/HealthScore.tsx -export default function HealthScore({ score, label }: any) { - return ( -
- -

- Organization Health Score -

- -
- - {score}/100 - - - {label} - -
- - {/* Progress bar */} -
-
-
- -
- ); -} -================================================================== -src/components/TopContributors.tsx - - -import { useEffect, useState } from "react"; -import { useNavigate } from "react-router-dom"; -import { FaTrophy } from "react-icons/fa"; - -const TOKEN = import.meta.env.VITE_GITHUB_TOKEN; - -export default function TopContributors({ repos }: any) { - const [contributors, setContributors] = useState([]); - const navigate = useNavigate(); - - useEffect(() => { - const fetchData = async () => { - const map: any = {}; - - for (let repo of repos.slice(0, 5)) { - try { - const res = await fetch(repo.contributors_url, { - headers: { - Authorization: `token ${TOKEN}` - } - }); - - const data = await res.json(); - - // ✅ ERROR HANDLE - if (!Array.isArray(data)) { - console.error("GitHub API Error:", data); - continue; - } - - data.forEach((c: any) => { - if (!map[c.login]) { - map[c.login] = { - login: c.login, - avatar: c.avatar_url, - contributions: 0, - url: c.html_url - }; - } - - map[c.login].contributions += c.contributions; - }); - - } catch (e) { - console.error("Fetch error:", e); - } - } - - const sorted = Object.values(map) - .sort((a: any, b: any) => b.contributions - a.contributions) - .slice(0, 5); - - setContributors(sorted); - }; - - if (repos.length) fetchData(); - }, [repos]); - - return ( -
-

- - Top Contributors -

- - {/* 🔥 Horizontal Scroll */} -
- - {contributors.map((c, i) => ( -
- navigate(`/contributor/${c.login}`, { state: c }) - } - className="min-w-[220px] bg-[#111827] p-4 rounded-lg cursor-pointer hover:bg-gray-800 transition border border-gray-700" - > - - -

- #{i + 1} {c.login} -

- -

- {c.contributions} contributions -

-
- ))} - -
-
- ); -} -============================================================== -src/layout/DashboardLayout.tsx == -import Sidebar from "./Sidebar"; -import Topbar from "./Topbar"; - -export default function DashboardLayout({ children, orgInput, orgLogo }: any) { - return ( -
- - {/* Sidebar */} - - - {/* Main */} -
- - - -
- {children} -
- -
-
- ); -} -====================================================================== -src/layout/Sidebar.tsx === - - -import { Link, useLocation } from "react-router-dom"; - - -export default function Sidebar() { - - const location = useLocation(); - - return ( -
- -
-
- Logo -
- - -
- -
- ⚙ Settings -
- -
- ); -} -================================================================================== -src/layout/Topbar.tsx == - -export default function Topbar({ orgInput, logo }: any) { - - const orgs = orgInput - ? orgInput.split(",").map((o: string) => o.trim()) - : []; - - return ( -
- -
- - {logo ? ( - - ) : ( -
- )} - -

- {orgs.join(" + ") || "OrgExplorer"} -

- -
-
- ); -} -================================== -src/pages/RepoDetails.tsx============= -import { useLocation } from "react-router-dom"; -import { useEffect, useState } from "react"; -import { - AreaChart, - Area, - XAxis, - YAxis, - Tooltip, - ResponsiveContainer -} from "recharts"; -import { FaCodeBranch, FaGithub } from "react-icons/fa"; -import { IoIosStarOutline } from "react-icons/io"; -import { MdOutlineReportGmailerrorred } from "react-icons/md"; -; - -const TOKEN = import.meta.env.VITE_GITHUB_TOKEN; - -export default function RepoDetails() { - const { state } = useLocation(); - const repo = state; - - const [contributors, setContributors] = useState([]); - const [chartData, setChartData] = useState([]); - - useEffect(() => { - if (!repo) return; - - fetchContributors(); - fetchActivity(); - }, []); - - // 👨‍💻 CONTRIBUTORS - const fetchContributors = async () => { - const res = await fetch(repo.contributors_url, { - headers: { Authorization: `token ${TOKEN}` } - }); - const data = await res.json(); - setContributors(data.slice(0, 4)); - }; - - // 📊 ACTIVITY CHART - const fetchActivity = async () => { - const res = await fetch( - `https://api.github.com/repos/${repo.full_name}/issues?per_page=100`, - { headers: { Authorization: `token ${TOKEN}` } } - ); - - const data = await res.json(); - - const map: any = {}; - - data.forEach((item: any) => { - const d = new Date(item.created_at) - .toISOString() - .split("T")[0]; - - if (!map[d]) map[d] = { date: d, issues: 0 }; - - map[d].issues++; - }); - - const result = Object.values(map); - setChartData(result); - }; - - if (!repo) { - return

No repo data

; - } - - const formatDate = (date: string) => { - const d = new Date(date); - return d.toLocaleDateString("en-US", { - month: "short", - day: "numeric", - }); -}; - - return ( -
- - {/* TITLE */} -

- {repo.name} -

- - {/* ⭐ STAT CARDS */} -
- - {/* STARS */} -
- -
-

Stars

-

{repo.stargazers_count}

-
-
- - {/* FORKS */} -
- -
-

Forks

-

{repo.forks_count}

-
-
- - {/* ISSUES */} -
- window.open(repo.html_url + "/issues", "_blank") - } - > - -
-

Issues

-

{repo.open_issues_count}

-
-
- -
- - {/* 📊 AREA CHART */} -
-

📊 Issues Activity

- - - - - - - - - - -
- - {/* 👨‍💻 CONTRIBUTORS */} -
-

👨‍💻 Top Contributors

- -
- {contributors.map((c: any) => ( - //
-
window.open(c.html_url, "_blank")} - className="flex items-center gap-3 bg-gray-800 p-3 rounded cursor-pointer hover:bg-gray-700 transition" - > - - -
-

{c.login}

-

- Contributions: {c.contributions} -

-
-
- ))} -
-
- - {/* 🔗 GITHUB LINK */} - - -
- ); -} - -==================================================================================== -src/pages/GraphPage.tsx === - -import NetworkGraph from "../components/Graph/NetworkGraph"; - -export default function GraphPage() { - const repos = JSON.parse(localStorage.getItem("repos") || "[]"); - - if (!repos.length) { - return ( -
- No data found 🚫
- Please analyze organizations first. -
- ); - } - - return ( -
- -

- Contributor Collaboration Network 🌐 -

- - - -

- Visualizes relationships between repositories and contributors across multiple organizations. -

-
- ); -} - -=================================================================== -src/pages/Overview.tsx == - -import { useState, useEffect } from "react"; -import OrgInput from "../components/Input/OrgInput"; -import InsightPanel from "../components/Insights/Insightpanel"; -import ActivityChart from "../components/Charts/ActivityChart"; -import { fetchOrgRepos } from "../services/githubService"; -import { mergeRepos } from "../utils/mergeOrgs"; -import { getInsights } from "../utils/insightEngine"; -import HealthScore from "../components/HealthScore"; -import { calculateOrgHealthScore } from "../utils/calculateScore"; -import type { Repo, Insight } from "../types/github"; -import { generateInsights } from "../utils/insights"; -import TopContributors from "../components/TopContributors"; - -type Props = { - orgInput: string; - setOrgInput: React.Dispatch>; - setOrgLogo : React.Dispatch>; -}; -export default function Overview({ orgInput, setOrgInput, setOrgLogo }: Props) { - - const [repos, setRepos] = useState([]); - const [data, setData] = useState(null); - const [loading, setLoading] = useState(false); - - const { score, label } = calculateOrgHealthScore(repos); - - // 🔥 DEFAULT LOAD - useEffect(() => { - const savedRepos = localStorage.getItem("repos"); - const savedInput = localStorage.getItem("orgInput"); - const savedLogo = localStorage.getItem("orgLogo"); - - if (!savedInput) { - handleSubmit("AOSSIE-Org"); // default - return; - } - - if (savedRepos) { - const parsed: Repo[] = JSON.parse(savedRepos); - setRepos(parsed); - setData(getInsights(parsed)); - } - - if (savedInput) { - setOrgInput(savedInput); - } - - if (savedLogo) { - setOrgLogo(savedLogo); // 🔥 GLOBAL SET - } - - }, []); - - // 🔥 ANALYZE - const handleSubmit = async (input: string) => { - setLoading(true); - - try { - setOrgInput(input); - - const orgs = input.split(",").map(o => o.trim()).filter(Boolean); - - // 🔥 FAST LOGO (NO WAIT) - fetch(`https://api.github.com/orgs/${orgs[0]}`) - .then(res => res.json()) - .then(data => { - setOrgLogo(data.avatar_url); // 🔥 GLOBAL - localStorage.setItem("orgLogo", data.avatar_url); - }) - .catch(() => setOrgLogo("")); - - // 🔥 MULTI ORG DATA - const results = await Promise.all(orgs.map(fetchOrgRepos)); - const merged: Repo[] = mergeRepos(results); - - setRepos(merged); - - localStorage.setItem("repos", JSON.stringify(merged)); - localStorage.setItem("orgInput", input); - - const insights: Insight = getInsights(merged); - setData(insights); - - } catch (e) { - console.error(e); - } - - setLoading(false); - }; - - const insights = generateInsights(repos); - - return ( -
- - - - {loading &&

Loading...

} - - {data && } - - {/* INSIGHTS */} -
-

- Insights -

- -
    - {insights.map((insight, i) => ( -
  • • {insight}
  • - ))} -
-
- -
- -
- - {/* CHART */} - {repos.length > 0 && ( - o.trim()).filter(Boolean)} - /> - )} - - {/* CONTRIBUTORS */} - {repos.length > 0 && ( - - )} - -
- ); -} -======================================================================================== -src/pages/Repositories.tsx === -import { useLocation } from "react-router-dom"; -import { useEffect, useState } from "react"; -import { - AreaChart, - Area, - XAxis, - YAxis, - Tooltip, - ResponsiveContainer -} from "recharts"; -import { FaCodeBranch, FaGithub } from "react-icons/fa"; -import { IoIosStarOutline } from "react-icons/io"; -import { MdOutlineReportGmailerrorred } from "react-icons/md"; -; - -const TOKEN = import.meta.env.VITE_GITHUB_TOKEN; - -export default function RepoDetails() { - const { state } = useLocation(); - const repo = state; - - const [contributors, setContributors] = useState([]); - const [chartData, setChartData] = useState([]); - - useEffect(() => { - if (!repo) return; - - fetchContributors(); - fetchActivity(); - }, []); - - // CONTRIBUTORS - const fetchContributors = async () => { - const res = await fetch(repo.contributors_url, { - headers: { Authorization: `token ${TOKEN}` } - }); - const data = await res.json(); - setContributors(data.slice(0, 4)); - }; - - // ACTIVITY CHART - const fetchActivity = async () => { - const res = await fetch( - `https://api.github.com/repos/${repo.full_name}/issues?per_page=100`, - { headers: { Authorization: `token ${TOKEN}` } } - ); - - const data = await res.json(); - - const map: any = {}; - - data.forEach((item: any) => { - const d = new Date(item.created_at) - .toISOString() - .split("T")[0]; - - if (!map[d]) map[d] = { date: d, issues: 0 }; - - map[d].issues++; - }); - - const result = Object.values(map); - setChartData(result); - }; - - if (!repo) { - return

No repo data

; - } - - const formatDate = (date: string) => { - const d = new Date(date); - return d.toLocaleDateString("en-US", { - month: "short", - day: "numeric", - }); - }; - - return ( -
- - {/* TITLE */} -

- {repo.name} -

- - {/* STAT CARDS */} -
- - {/* STARS */} -
- -
-

Stars

-

{repo.stargazers_count}

-
-
- - {/* FORKS */} -
- -
-

Forks

-

{repo.forks_count}

-
-
- - {/* ISSUES */} -
- window.open(repo.html_url + "/issues", "_blank") - } - > - -
-

Issues

-

{repo.open_issues_count}

-
-
- -
- - {/* AREA CHART */} -
-

Issues Activity

- - - - - - - - - - -
- - {/* CONTRIBUTORS */} -
-

Top Contributors

- -
- {contributors.map((c: any) => ( - -
window.open(c.html_url, "_blank")} - className="flex items-center gap-3 bg-gray-800 p-3 rounded cursor-pointer hover:bg-gray-700 transition" - > - - -
-

{c.login}

-

- Contributions: {c.contributions} -

-
-
- ))} -
-
- - {/* GITHUB LINK */} - - -
- ); -} - -=========================================================================== -src/pages/ContributorDetail.tsx - -import { useEffect, useState } from "react"; -import { useParams } from "react-router-dom"; -import { - AreaChart, Area, XAxis, YAxis, Tooltip, CartesianGrid, ResponsiveContainer, Legend -} from "recharts"; -import { FaCodeBranch } from "react-icons/fa"; - - -const TOKEN = import.meta.env.VITE_GITHUB_TOKEN; - -export default function ContributorDetail() { - const { username } = useParams(); - - const [user, setUser] = useState(null); - const [prs, setPrs] = useState([]); - const [issues, setIssues] = useState([]); - const [sortKey, setSortKey] = useState("prCreated"); - const [sortOrder, setSortOrder] = useState("desc"); - const [events, setEvents] = useState([]); - const [filter, setFilter] = useState("7"); - - useEffect(() => { - if (!username) return; - - const fetchData = async () => { - try { - // 👤 USER - const userRes = await fetch(`https://api.github.com/users/${username}`, { - headers: { Authorization: `token ${TOKEN}` } - }); - const userData = await userRes.json(); - setUser(userData); - - // 🔀 PRs - const prRes = await fetch( - `https://api.github.com/search/issues?q=author:${username}+type:pr`, - { headers: { Authorization: `token ${TOKEN}` } } - ); - const prData = await prRes.json(); - setPrs(prData.items || []); - - // 🐞 Issues - const issueRes = await fetch( - `https://api.github.com/search/issues?q=author:${username}+type:issue`, - { headers: { Authorization: `token ${TOKEN}` } } - ); - const issueData = await issueRes.json(); - setIssues(issueData.items || []); - - // 📅 EVENTS (Recent Activity) - const eventRes = await fetch( - `https://api.github.com/users/${username}/events`, - { headers: { Authorization: `token ${TOKEN}` } } - ); - const eventData = await eventRes.json(); - setEvents(eventData || []); - console.log("events", eventData); - - } catch (err) { - console.error(err); - } - }; - - fetchData(); - - - }, [username]); - - // const timeAgo = (date: string) => { - // const diff = (new Date().getTime() - new Date(date).getTime()) / 1000; - - // const days = Math.floor(diff / 86400); - // if (days > 0) return `${days} day${days > 1 ? "s" : ""} ago`; - - // const hours = Math.floor(diff / 3600); - // if (hours > 0) return `${hours} hour${hours > 1 ? "s" : ""} ago`; - - // const mins = Math.floor(diff / 60); - // return `${mins} min ago`; - // }; - - function timeAgo(date: string) { - const seconds = Math.floor((new Date().getTime() - new Date(date).getTime()) / 1000); - - const intervals: any = { - year: 31536000, - month: 2592000, - week: 604800, - day: 86400, - hour: 3600, - minute: 60, - }; - - for (let key in intervals) { - const interval = Math.floor(seconds / intervals[key]); - if (interval > 1) return `${interval} ${key}s ago`; - if (interval === 1) return `1 ${key} ago`; - } - - return "just now"; - } - - const filteredEvents = events.filter((e: any) => { - const days = filter === "7" ? 7 : 30; - const eventDate = new Date(e.created_at); - const now = new Date(); - - return (now.getTime() - eventDate.getTime()) / (1000 * 60 * 60 * 24) <= days; - }); - - if (!user) return

Loading...

; - - // 📊 CALCULATIONS - const totalPR = prs.length; - const mergedPR = prs.filter(p => p.pull_request?.merged_at).length; - const mergeRate = totalPR ? Math.round((mergedPR / totalPR) * 100) : 0; - const totalIssues = issues.length; - - const score = Math.round( - 0.5 * mergeRate + - 0.3 * Math.min(totalIssues * 5, 100) + - 0.2 * 70 - ); - - // 📁 REPO-WISE STATS - const repoStats: any = {}; - - // PR data - prs.forEach((pr: any) => { - // const repo = pr.repository_url.split("/").pop(); - const parts = pr.repository_url.split("/"); - const repo = parts[parts.length - 2] + "/" + parts[parts.length - 1]; - - if (!repoStats[repo]) { - repoStats[repo] = { - repo, - prCreated: 0, - prMerged: 0, - issuesSolved: 0, - }; - } - - repoStats[repo].prCreated++; - - if (pr.pull_request?.merged_at) { - repoStats[repo].prMerged++; - } - }); - - // Issue data - issues.forEach((issue: any) => { - // const repo = issue.repository_url.split("/").pop(); - const parts = issue.repository_url.split("/"); - const repo = parts[parts.length - 2] + "/" + parts[parts.length - 1]; - - if (!repoStats[repo]) { - repoStats[repo] = { - repo, - prCreated: 0, - prMerged: 0, - issuesSolved: 0, - }; - } - - repoStats[repo].issuesSolved++; - }); - - const repoList = Object.values(repoStats); - const sortedRepos = [...repoList].sort((a: any, b: any) => { - const valA = a[sortKey]; - const valB = b[sortKey]; - - if (sortOrder === "asc") return valA - valB; - return valB - valA; - }); - - const chartData = repoList.map((r: any) => ({ - name: r.repo.split("/")[1], // sirf repo name - created: r.prCreated, - merged: r.prMerged - })); - - - return ( -
- - {/* 🔥 TOP SECTION */} -
- - - -
-

{user.login}

- -

- ⭐ Quality Score: {score}/100 -

- -

- 📊 Merge Rate: {mergeRate}% -

- -

80 ? "text-green-400" : - score > 50 ? "text-yellow-400" : - "text-red-400" - }`}> - {score > 80 ? "🟢 High Quality Contributor" : - score > 50 ? "🟡 Medium Quality" : - "🔴 Low Quality"} -

-
-
- - {/* 📊 STATS CARDS */} -
-
- PR -

{totalPR}

-
- -
-

PR Merged

-

{mergedPR}

-
- -
-

Issues Solved

-

{totalIssues}

-
- -
-

Issues Created

-

{totalIssues}

-
-
- -
- -

- PR Activity (Created vs Merged) -

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - {/* 📁 REPOSITORIES TABLE */} -
- -

- Repository Contributions -

- - {repoList.length === 0 ? ( -

No repository data found

- ) : ( -
- - - {/* HEADER */} - {/* - - - - - - - - */} - - - - - - - {/* */} - - - - - - - - - - - - {/* BODY */} - - {sortedRepos.map((r: any) => { - const mergeRate = - r.prCreated > 0 - ? Math.round((r.prMerged / r.prCreated) * 100) - : 0; - - return ( - - - - - - - - - - - - ); - })} - - -
RepoPR CreatedPR MergedMerge %Issues Solved
Repo { - setSortKey("prCreated"); - setSortOrder(sortOrder === "asc" ? "desc" : "asc"); - }} - > - PR Created - { - setSortKey("prCreated"); - setSortOrder(sortOrder === "asc" ? "desc" : "asc"); - }} - > - PR {sortKey === "prCreated" ? (sortOrder === "asc" ? "↑" : "↓") : ""} - { - setSortKey("prMerged"); - setSortOrder(sortOrder === "asc" ? "desc" : "asc"); - }} - > - PR Merged {sortKey === "prMerged" ? (sortOrder === "asc" ? "↑" : "↓") : ""} - Merge % { - setSortKey("issuesSolved"); - setSortOrder(sortOrder === "asc" ? "desc" : "asc"); - }} - > - Issues Solved {sortKey === "issuesSolved" ? (sortOrder === "asc" ? "↑" : "↓") : ""} -
- - {r.repo} - - {r.prCreated} - {r.prMerged} - - {mergeRate}% - - {r.issuesSolved} -
-
- )} -
- - {/* 💡 INSIGHTS */} -
-

Insights

- -
    - {mergeRate > 70 && ( -
  • 🟢 High merge rate indicates quality work
  • - )} - {mergeRate < 40 && ( -
  • ⚠️ Low merge rate suggests PR issues
  • - )} - {totalPR < 5 && ( -
  • ⚠️ Limited contributions
  • - )} -
-
- - - {/* 📅 Recent Activity */} -
-
-

📅 Recent Activity

- - {/* Filter */} - -
- - {/* Events */} - {filteredEvents.length === 0 ? ( -

No recent activity

- ) : ( -
- {filteredEvents.slice(0, 10).map((e: any, index: number) => { - const isLatest = index === 0; - - let text = ""; - let link = "#"; - - // 🔀 PR Created - if (e.type === "PullRequestEvent" && e.payload.action === "opened") { - const prNumber = e.payload?.pull_request?.number || ""; - text = `🔀 Created PR #${prNumber} in ${e.repo.name}`; - link = e.payload?.pull_request?.html_url; - } - - // ✅ PR Merged - else if (e.type === "PullRequestEvent" && e.payload?.pull_request?.merged) { - const prNumber = e.payload?.pull_request?.number || ""; - text = `✅ PR #${prNumber} merged in ${e.repo.name}`; - link = e.payload?.pull_request?.html_url; - } - - // 🆕 Issue Opened - else if (e.type === "IssuesEvent" && e.payload.action === "opened") { - const issueNumber = e.payload?.issue?.number || ""; - text = `🆕 Opened issue #${issueNumber} in ${e.repo.name}`; - link = e.payload?.issue?.html_url; - } - - // 🐞 Issue Closed - else if (e.type === "IssuesEvent" && e.payload.action === "closed") { - const issueNumber = e.payload?.issue?.number || ""; - text = `🐞 Closed issue #${issueNumber} in ${e.repo.name}`; - link = e.payload?.issue?.html_url; - } - - // 📦 Push - else if (e.type === "PushEvent") { - text = `📦 Pushed code to ${e.repo.name}`; - link = `https://github.com/${e.repo.name}`; - } - - else return null; - - return ( - - {text} - - ({timeAgo(e.created_at)}) - - - ); - })} -
- )} -
- - - - {/* 🔗 GITHUB */} - - View GitHub Profile → - - -
- ); -} -===================================================================================================== -src/services/githubService.ts === - -const BASE = "https://api.github.com"; - -const TOKEN = import.meta.env.VITE_GITHUB_TOKEN; - -export const fetchOrgRepos = async (org: string) => { - const res = await fetch(`${BASE}/orgs/${org}/repos?per_page=100`, { - headers: { - Authorization: `token ${TOKEN}` - } - }); - - if (!res.ok) throw new Error("API Error"); - return res.json(); -}; - -export const fetchRepoContributors = async (url: string) => { - const res = await fetch(url, { - headers: { - Authorization: `token ${TOKEN}` - } - }); - - if (!res.ok) throw new Error("Contributor API Error"); - return res.json(); -}; - -export const fetchRepoIssues = async (owner: string, repo: string) => { - const res = await fetch( - `https://api.github.com/repos/${owner}/${repo}/issues?state=all&per_page=100`, - { - headers: { - Authorization: `token ${TOKEN}` - } - } - ); - - if (!res.ok) throw new Error("Issues API Error"); - return res.json(); -}; - -export const fetchRepoPRs = async (owner: string, repo: string) => { - const res = await fetch( - `${BASE}/repos/${owner}/${repo}/pulls?state=all&per_page=100`, - { - headers: { Authorization: `token ${TOKEN}` } - } - ); - - if (!res.ok) return []; - return res.json(); -}; -=============================================================================== -src/types/github.ts == -export interface Repo { - id: number; - name: string; - stargazers_count: number; - forks_count: number; - updated_at: string; -} - -export interface Insight { - totalRepos: number; - totalStars: number; - totalForks: number; - inactivePercent: string; - topRepos : Repo[]; -insights: string[]; -} -==================================================================================== -utils/calculateScore.ts == -export const calculateOrgHealthScore = (repos: any[]) => { - if (!repos || repos.length === 0) return { score: 0, label: "No Data" }; - - const totalRepos = repos.length; - - const activeRepos = repos.filter(repo => { - const days = - (Date.now() - new Date(repo.pushed_at).getTime()) / - (1000 * 60 * 60 * 24); - return days < 30; - }).length; - - const avgStars = - repos.reduce((sum, r) => sum + r.stargazers_count, 0) / totalRepos; - - const avgForks = - repos.reduce((sum, r) => sum + r.forks_count, 0) / totalRepos; - - const staleRepos = repos.filter(repo => { - const days = - (Date.now() - new Date(repo.pushed_at).getTime()) / - (1000 * 60 * 60 * 24); - return days > 180; - }).length; - - let score = 0; - - // 🚀 Activity score (40) - score += (activeRepos / totalRepos) * 40; - - // 🌟 Popularity score (30) - score += Math.min(avgStars, 100) * 0.3; - - // 🔁 Engagement score (20) - score += Math.min(avgForks, 50) * 0.4; - - // 🛑 Penalty (10) - score -= (staleRepos / totalRepos) * 10; - - score = Math.round(score); - - let label = "Poor"; - if (score > 75) label = "Excellent 🚀"; - else if (score > 50) label = "Good 👍"; - else if (score > 30) label = "Average ⚠️"; - - return { score, label }; -}; -====================================================== - -================================================================== - -utils/exportCSV.ts == - -export const exportCSV = (repos: any[]) => { - const rows = repos.map(r => - `${r.name},${r.stargazers_count},${r.forks_count}` - ); - - const csv = "Name,Stars,Forks\n" + rows.join("\n"); - - const blob = new Blob([csv]); - const url = URL.createObjectURL(blob); - - const a = document.createElement("a"); - a.href = url; - a.download = "repos.csv"; - a.click(); -}; -======================================================================= -utils/insightEngine.ts === -import type { Repo, Insight } from "../types/github"; - -export interface InsightResult extends Insight { - insights: string[]; -} - -export const getInsights = (repos: Repo[]): InsightResult => { - const now = new Date(); - - const inactive = repos.filter(r => - (now.getTime() - new Date(r.updated_at).getTime()) > - 90 * 24 * 60 * 60 * 1000 - ); - - const totalStars = repos.reduce((sum, r) => sum + r.stargazers_count, 0); - const totalForks = repos.reduce((s, r) => s + r.forks_count, 0); - - const topRepos = [...repos] - .sort((a, b) => b.stargazers_count - a.stargazers_count) - .slice(0, 5); - - const inactivePercent = (inactive.length / repos.length) * 100; - - // NEW: INSIGHT GENERATION - const insights: string[] = []; - - // Inactive repos insight - if (inactivePercent > 50) { - insights.push("⚠ More than 50% repositories are inactive → maintenance risk"); - } else if (inactivePercent > 30) { - insights.push("⚠ Significant number of repositories are inactive"); - } else { - insights.push("✅ Most repositories are actively maintained"); - } - - // Star concentration insight - if (topRepos.length > 0) { - const topStar = topRepos[0].stargazers_count; - - if (topStar > totalStars * 0.5) { - insights.push("⚡ One repository dominates more than 50% of total stars"); - } else if (topStar > totalStars * 0.3) { - insights.push("⚡ A few repositories dominate the ecosystem"); - } - } - - // Fork vs Star ratio insight - if (totalStars > 0) { - const ratio = totalForks / totalStars; - - if (ratio > 0.6) { - insights.push("🔁 High fork-to-star ratio → strong developer engagement"); - } else if (ratio < 0.2) { - insights.push("📉 Low fork activity compared to stars"); - } - } - - // Recently active repos insight - const recent = repos.filter(r => - (now.getTime() - new Date(r.updated_at).getTime()) < - 30 * 24 * 60 * 60 * 1000 - ); - - if (recent.length > repos.length * 0.5) { - insights.push("🚀 High recent activity across repositories"); - } else if (recent.length < repos.length * 0.2) { - insights.push("🐢 Low recent activity → possible slowdown"); - } - - // Repo size distribution insight - const lowStarRepos = repos.filter(r => r.stargazers_count < 10); - - if (lowStarRepos.length > repos.length * 0.6) { - insights.push("📦 Majority of repositories have low visibility (<10 stars)"); - } - - // Growth potential insight - if (totalStars > 1000 && inactivePercent < 30) { - insights.push("📈 Organization shows strong growth potential"); - } - - return { - totalRepos: repos.length, - totalStars, - totalForks, - inactivePercent: inactivePercent.toFixed(1), - topRepos, - insights - }; -}; -=========================================================================================== -utils/mergeOrgs.ts====== -import type { Repo } from "../types/github"; - -export const mergeRepos = (allRepos: Repo[][]): Repo[] => { - const map = new Map(); - - allRepos.flat().forEach(repo => { - if (!map.has(repo.id)) { - map.set(repo.id, repo); - } - }); - - return Array.from(map.values()); -}; -============================================================================================== -utils/insights.ts================================= -export function generateInsights(repos: any[]) { - if (!repos || repos.length === 0) return []; - - const insights: string[] = []; - - // Most starred repo - const topRepo = [...repos].sort( - (a, b) => b.stargazers_count - a.stargazers_count - )[0]; - - insights.push(`⭐ Most popular repo: ${topRepo.name}`); - - // Low activity repos - const lowActivity = repos.filter((r) => r.stargazers_count < 5); - if (lowActivity.length > 0) { - insights.push(`⚠️ ${lowActivity.length} repos have very low stars`); - } - - // Fork heavy repos - const forkHeavy = repos.filter((r) => r.forks_count > r.stargazers_count); - if (forkHeavy.length > 0) { - insights.push(`🍴 Some repos are fork-heavy but not popular`); - } - - // Recently updated - const recent = repos.filter((r) => { - const updated = new Date(r.updated_at); - const now = new Date(); - return (now.getTime() - updated.getTime()) / (1000 * 60 * 60 * 24) < 30; - }); - - if (recent.length > repos.length / 2) { - insights.push(`🚀 Org is actively maintained (many recent updates)`); - } - - // Stale repos - const stale = repos.filter((r) => { - const updated = new Date(r.updated_at); - const now = new Date(); - return (now.getTime() - updated.getTime()) / (1000 * 60 * 60 * 24) > 180; - }); - - if (stale.length > 0) { - insights.push(`💀 ${stale.length} repos are stale (>6 months no updates)`); - } - - // Language dominance - const langMap: any = {}; - repos.forEach((r) => { - if (!r.language) return; - langMap[r.language] = (langMap[r.language] || 0) + 1; - }); - - const topLang = Object.keys(langMap).sort( - (a, b) => langMap[b] - langMap[a] - )[0]; - - if (topLang) { - insights.push(`💻 Most used language: ${topLang}`); - } - - return insights; -} - -===================================================================================== -app.tsx === -import { useState } from "react"; -import { Routes, Route } from "react-router-dom"; - -import DashboardLayout from "./layout/DashboardLayout"; -import Overview from "./pages/Overview"; -import Repositories from "./pages/Repositories"; -import GraphPage from "./pages/GraphPage"; -import ContributorDetail from "./pages/ContributorDetail"; -import RepoDetails from "./pages/RepoDetails"; - -export default function App() { - const [orgInput, setOrgInput] = useState(""); - const [orgLogo, setOrgLogo] = useState(""); - - return ( - - - - } - /> - - } /> - } /> - } /> - } /> - - - - ); -} - diff --git a/note2.txt b/note2.txt deleted file mode 100644 index f8fa9fc..0000000 --- a/note2.txt +++ /dev/null @@ -1,297 +0,0 @@ -h2D from "react-force-graph-2d"; -import { useEffect, useState, useRef } from "react"; -import { fetchRepoContributors } from "../../services/githubService"; - -export default function NetworkGraph({ repos }: any) { - - const [graphData, setGraphData] = useState({ nodes: [], links: [] }); - const [selectedRepo, setSelectedRepo] = useState(null); - const [crossUsers, setCrossUsers] = useState([]); - const fgRef = useRef(null); - - useEffect(() => { - const buildGraph = async () => { - - const nodes: any[] = []; - const links: any[] = []; - - const userMap = new Map(); - const repoMap = new Map(); - const userOrgMap: any = {}; - - // 🔥 LIMIT for performance (important) - const selectedRepos = repos.slice(0, 8); - - for (let repo of selectedRepos) { - - const org = repo.full_name.split("/")[0]; - - // ✅ REPO NODE - if (!repoMap.has(repo.full_name)) { - repoMap.set(repo.full_name, true); - - nodes.push({ - id: repo.full_name, - type: "repo", - org, - stars: repo.stargazers_count, - size: Math.log(repo.stargazers_count + 1) * 3 + 8 - }); - } - - try { - const contributors = await fetchRepoContributors( - `${repo.contributors_url}?per_page=50` - ); - - contributors.slice(0, 20).forEach((c: any) => { - - // ✅ USER NODE - if (!userMap.has(c.login)) { - userMap.set(c.login, true); - - nodes.push({ - id: c.login, - type: "user", - img: c.avatar_url, - contributions: c.contributions, - size: Math.log(c.contributions + 1) * 2 + 5 - }); - } - - // 🔥 TRACK ORG RELATION - if (!userOrgMap[c.login]) { - userOrgMap[c.login] = new Set(); - } - userOrgMap[c.login].add(org); - - // ✅ LINK repo ↔ user - links.push({ - source: c.login, - target: repo.full_name, - weight: c.contributions - }); - - }); - - } catch (e) { - console.error(e); - } - } - - // 🔥 CROSS ORG USERS - const cross = Object.entries(userOrgMap) - .filter(([_, set]: any) => set.size > 1) - .map(([user, set]: any) => ({ - user, - orgCount: set.size - })) - .sort((a, b) => b.orgCount - a.orgCount) - .slice(0, 5); - - setCrossUsers(cross); - - setGraphData({ nodes, links }); - }; - - if (repos.length) buildGraph(); - - }, [repos]); - - // 🔥 FORCE SETTINGS (clean network look) - useEffect(() => { - if (!fgRef.current) return; - - const fg = fgRef.current; - - fg.d3Force("charge").strength(-150); - fg.d3Force("link").distance(100); - - }, [graphData]); - - // 🔥 INSIGHTS - const totalLinks = graphData.links.length; - const totalNodes = graphData.nodes.length; - - return ( -
- - {/* 🔥 INSIGHT */} -

- 🔥 {totalLinks} connections across {totalNodes} nodes -

- -

- {totalLinks > 150 - ? "🚀 High collaboration across organizations" - : "⚠️ Limited collaboration detected"} -

- - {/* 🔥 CROSS ORG USERS */} -
-

- 🔗 Top Cross-Org Contributors -

- - {crossUsers.length === 0 && ( -

No cross-org contributors

- )} - - {crossUsers.map((u: any) => ( -
- {u.user} → {u.orgCount} orgs -
- ))} -
- - {/* 🔥 GRAPH */} -
- - - node.type === "repo" - ? `📦 ${node.id} ⭐ ${node.stars}` - : `👤 ${node.id} (${node.contributions})` - } - - // 🔥 CLICK - onNodeClick={(node: any) => { - if (node.type === "user") { - window.open(`https://github.com/${node.id}`, "_blank"); - } - if (node.type === "repo") { - setSelectedRepo(node); - } - }} - - // 🔥 NODE DESIGN - nodeCanvasObject={(node: any, ctx, globalScale) => { - const size = node.size || 6; - - // USER IMAGE - if (node.type === "user" && node.img) { - const img = new Image(); - img.src = node.img; - - ctx.save(); - ctx.beginPath(); - ctx.arc(node.x, node.y, size, 0, 2 * Math.PI); - ctx.clip(); - ctx.drawImage(img, node.x - size, node.y - size, size * 2, size * 2); - ctx.restore(); - } - - // REPO NODE (colored by org) - else { - ctx.beginPath(); - ctx.arc(node.x, node.y, size, 0, 2 * Math.PI); - - if (node.org === "AOSSIE-Org") ctx.fillStyle = "#a855f7"; - else if (node.org === "StabilityNexus") ctx.fillStyle = "#22c55e"; - else ctx.fillStyle = "#facc15"; - - ctx.fill(); - } - - // LABEL - ctx.font = `${10 / globalScale}px Inter`; - ctx.fillStyle = "#e2e8f0"; - ctx.fillText(node.id.split("/")[1] || node.id, node.x + size + 2, node.y); - }} - - // 🔥 LINKS - linkWidth={(link: any) => Math.log(link.weight + 1)} - linkColor={() => "rgba(34,197,94,0.5)"} - - linkDirectionalParticles={2} - linkDirectionalParticleSpeed={0.003} - - onEngineStop={() => fgRef.current?.zoomToFit(400)} - /> -
- - {/* 🔥 REPO POPUP */} - {selectedRepo && ( -
-
- -

- 📦 {selectedRepo.id} -

- -

- ⭐ Stars: {selectedRepo.stars} -

- - - - - -
-
- )} -
- ); -} - - -
- - {/* 🟢 Contributors */} -
- 🟢 {stats.users} - Contributors -
- - {/* 🟡 Repos */} -
- 🟡 {stats.repos} - Repos -
- - {/* ⚪ Edges */} -
- ⚪ {stats.edges} - Links -
- - {/* 🔥 Top Contributor */} -
- Most Active Contributor : {topContributor?.label} -
- - {/* 🚀 Most Active Repo */} -
- Most Active Repo : {mostActiveRepo?.label} -
- - {/* 🔗 Strongest Link */} -
- Strongest : {getId(strongestLink?.source)} → {getId(strongestLink?.target)} -
- -
\ No newline at end of file diff --git a/src/utils/insights.ts b/src/utils/insights.ts deleted file mode 100644 index 7696436..0000000 --- a/src/utils/insights.ts +++ /dev/null @@ -1,63 +0,0 @@ -// export function generateInsights(repos: any[]) { -// if (!repos || repos.length === 0) return []; - -// const insights: string[] = []; - -// // Most starred repo -// const topRepo = [...repos].sort( -// (a, b) => b.stargazers_count - a.stargazers_count -// )[0]; - -// insights.push(` Most popular repo: ${topRepo.name}`); - -// // Low activity repos -// const lowActivity = repos.filter((r) => r.stargazers_count < 5); -// if (lowActivity.length > 0) { -// insights.push(` ${lowActivity.length} repos have very low stars`); -// } - -// // Fork heavy repos -// const forkHeavy = repos.filter((r) => r.forks_count > r.stargazers_count); -// if (forkHeavy.length > 0) { -// insights.push(` Some repos are fork-heavy but not popular`); -// } - -// // Recently updated -// const recent = repos.filter((r) => { -// const updated = new Date(r.updated_at); -// const now = new Date(); -// return (now.getTime() - updated.getTime()) / (1000 * 60 * 60 * 24) < 30; -// }); - -// if (recent.length > repos.length / 2) { -// insights.push(` Org is actively maintained (many recent updates)`); -// } - -// // Stale repos -// const stale = repos.filter((r) => { -// const updated = new Date(r.updated_at); -// const now = new Date(); -// return (now.getTime() - updated.getTime()) / (1000 * 60 * 60 * 24) > 180; -// }); - -// if (stale.length > 0) { -// insights.push(` ${stale.length} repos are stale (>6 months no updates)`); -// } - -// // Language dominance -// const langMap: any = {}; -// repos.forEach((r) => { -// if (!r.language) return; -// langMap[r.language] = (langMap[r.language] || 0) + 1; -// }); - -// const topLang = Object.keys(langMap).sort( -// (a, b) => langMap[b] - langMap[a] -// )[0]; - -// if (topLang) { -// insights.push(` Most used language: ${topLang}`); -// } - -// return insights; -// } \ No newline at end of file