diff --git a/src/components/feed/feed-view.tsx b/src/components/feed/feed-view.tsx index b923d6e..8420f3c 100644 --- a/src/components/feed/feed-view.tsx +++ b/src/components/feed/feed-view.tsx @@ -11,6 +11,7 @@ import { getLatestNodes } from "@/lib/graph-api" import { schemaTypeComparator } from "@/lib/node-schema-utils" import { FeedCard } from "./feed-card" import { HotTakes } from "./hot-takes" +import { GraphStats } from "./graph-stats" import { cn } from "@/lib/utils" export function FeedView() { @@ -91,6 +92,7 @@ export function FeedView() { return (
+ {showHotTakes && } {showHotTakes && } {(showHotTakes || typeCounts.length > 1) && ( diff --git a/src/components/feed/graph-stats.tsx b/src/components/feed/graph-stats.tsx new file mode 100644 index 0000000..ee1be68 --- /dev/null +++ b/src/components/feed/graph-stats.tsx @@ -0,0 +1,58 @@ +"use client" + +import { useEffect, useState } from "react" +import { getStats } from "@/lib/graph-api" +import { isMocksEnabled } from "@/lib/mock-data" +import { formatStatNumber, getDisplayStats, type GraphStat } from "@/lib/graph-stats" + +const MOCK_STATS: GraphStat[] = [{ label: "Nodes", value: 1234 }] + +export function GraphStats() { + const mocks = isMocksEnabled() + const [stats, setStats] = useState(mocks ? MOCK_STATS : null) + const [error, setError] = useState(false) + + useEffect(() => { + if (mocks) return + const controller = new AbortController() + getStats(controller.signal) + .then((res) => setStats(getDisplayStats(res))) + .catch((err) => { + if (controller.signal.aborted) return + console.error("[graph-stats] getStats failed:", err) + setError(true) + }) + return () => controller.abort() + }, [mocks]) + + if (error) { + return ( +
+ Stats unavailable +
+ ) + } + + if (stats === null) { + return ( +
+
+
+ ) + } + + if (stats.length === 0) return null + + return ( +
+ {stats.map((s) => ( +
+
+ {s.label} +
+
{formatStatNumber(s.value)}
+
+ ))} +
+ ) +} diff --git a/src/lib/__tests__/graph-stats.test.ts b/src/lib/__tests__/graph-stats.test.ts new file mode 100644 index 0000000..7ee925d --- /dev/null +++ b/src/lib/__tests__/graph-stats.test.ts @@ -0,0 +1,25 @@ +import { describe, it, expect } from "vitest" +import { getDisplayStats, formatStatNumber } from "../graph-stats" + +describe("getDisplayStats", () => { + it("returns nodes and CVEs when cve_count present", () => { + expect(getDisplayStats({ node_count: 10, cve_count: 3 })).toEqual([ + { label: "Nodes", value: 10 }, + { label: "CVEs", value: 3 }, + ]) + }) + it("returns only nodes when cve_count absent or 0", () => { + expect(getDisplayStats({ node_count: 10 })).toEqual([{ label: "Nodes", value: 10 }]) + expect(getDisplayStats({ node_count: 10, cve_count: 0 })).toEqual([{ label: "Nodes", value: 10 }]) + }) + it("uses node_count, not num_nodes", () => { + expect(getDisplayStats({ node_count: 5, num_nodes: 0 })[0].value).toBe(5) + }) + it("returns nothing without node_count", () => { + expect(getDisplayStats({ num_nodes: 7 })).toEqual([]) + expect(getDisplayStats(null)).toEqual([]) + }) + it("formats with grouping", () => { + expect(formatStatNumber(1776798)).toBe("1,776,798") + }) +}) diff --git a/src/lib/graph-api.ts b/src/lib/graph-api.ts index 88abd8c..f434a69 100644 --- a/src/lib/graph-api.ts +++ b/src/lib/graph-api.ts @@ -33,7 +33,8 @@ interface NodesListResponse { totalCount?: number } -interface StatsResponse { +export interface StatsResponse { + node_count: number num_nodes: number num_episodes: number num_people: number diff --git a/src/lib/graph-stats.ts b/src/lib/graph-stats.ts new file mode 100644 index 0000000..b76e166 --- /dev/null +++ b/src/lib/graph-stats.ts @@ -0,0 +1,26 @@ +import type { StatsResponse } from "@/lib/graph-api" + +export interface GraphStat { + label: string + value: number +} + +// Per-type counts shown only when the graph reports a positive count. +// Add entries here to surface more node types. +export const TYPE_STATS: readonly { key: string; label: string }[] = [ + { key: "cve_count", label: "CVEs" }, +] + +export function getDisplayStats(stats: Partial | null | undefined): GraphStat[] { + if (!stats || typeof stats.node_count !== "number") return [] + const result: GraphStat[] = [{ label: "Nodes", value: stats.node_count }] + for (const { key, label } of TYPE_STATS) { + const value = (stats as Record)[key] + if (typeof value === "number" && value > 0) result.push({ label, value }) + } + return result +} + +export function formatStatNumber(n: number): string { + return n.toLocaleString("en-US") +}