diff --git a/src/components/RepoHealthPanel.tsx b/src/components/RepoHealthPanel.tsx new file mode 100644 index 00000000..36b10d9c --- /dev/null +++ b/src/components/RepoHealthPanel.tsx @@ -0,0 +1,100 @@ +"use client"; + +import { useEffect } from "react"; +import type { RepoHealthScore } from "@/types/repo-health"; +interface Props { + health: RepoHealthScore; + isOpen: boolean; + onClose: () => void; +} + +function ScoreBar({ score, maxScore }: { score: number; maxScore: number }) { + const pct = Math.round((score / maxScore) * 100); + const color = pct >= 70 ? "bg-[var(--accent)]" : pct >= 40 ? "bg-[var(--warning)]" : "bg-[var(--destructive)]"; + return ( +
+
+
+ ); +} + +export default function RepoHealthPanel({ health, isOpen, onClose }: Props) { + useEffect(() => { + const handler = (e: KeyboardEvent) => { + if (e.key === "Escape") { + onClose(); + } + }; + + document.addEventListener("keydown", handler); + + return () => { + document.removeEventListener("keydown", handler); + }; + }, [onClose]); + + if (!isOpen) return null; + const s = health.signals; + const shortName = health.repo.split("/")[1] ?? health.repo; + + const commitScore = Math.min(20, Math.round((s.commitFrequency / 10) * 20)); + const prScore = Math.min(20, Math.round(s.prMergeRate * 25)); + const prTimeScore = s.avgPrOpenTimeHours === 0 ? 20 : Math.max(0, Math.round(20 - (s.avgPrOpenTimeHours / 168) * 20)); + const issueScore = Math.max(0, Math.round(20 - s.openIssuesCount * 2)); + const recencyScore = Math.max(0, Math.round(20 - (s.daysSinceLastCommit / 30) * 20)); + + const dimensions = [ + { label: "Commit Activity", score: commitScore, tip: commitScore < 10 ? "Aim for at least 10 commits per month." : "Good commit frequency!" }, + { label: "PR Merge Rate", score: prScore, tip: prScore < 10 ? "Review and close stale pull requests." : "Healthy merge rate." }, + { label: "PR Turnaround", score: prTimeScore, tip: prTimeScore < 10 ? "Try to review and merge PRs faster." : "PRs are moving quickly." }, + { label: "Issue Load", score: issueScore, tip: issueScore < 10 ? "Triage and close outdated issues." : "Issue backlog looks manageable." }, + { label: "Recent Activity", score: recencyScore, tip: recencyScore < 10 ? `Last commit was ${s.daysSinceLastCommit} days ago.` : "Repo is actively maintained." }, + ]; + + return ( +
+ + ); +} diff --git a/src/components/TopRepos.tsx b/src/components/TopRepos.tsx index a7b96ea7..4f7a5915 100644 --- a/src/components/TopRepos.tsx +++ b/src/components/TopRepos.tsx @@ -3,6 +3,7 @@ import { useCallback, useEffect, useState } from "react"; import { useAccount } from "@/components/AccountContext"; import type { RepoHealthScore } from "@/types/repo-health"; +import RepoHealthPanel from "@/components/RepoHealthPanel"; interface RepoLanguage { name: string; @@ -84,7 +85,7 @@ export default function TopRepos() { const [sortDirection, setSortDirection] = useState<"asc" | "desc">("desc"); const [pinnedRepos, setPinnedRepos] = useState([]); const [pinError, setPinError] = useState(null); - + const [activeHealthRepo, setActiveHealthRepo] = useState(null); useEffect(() => { fetch("/api/user/settings") .then((r) => r.json()) @@ -326,12 +327,15 @@ export default function TopRepos() { {healthLoading ? (
) : health ? ( - setActiveHealthRepo(repo.name)} + className={`inline-flex items-center rounded-full px-2 py-0.5 text-xs font-semibold cursor-pointer ${badgeClass}`} title={badgeTitle} + aria-label={`View health breakdown for ${shortName}`} > {health.score} - + ) : ( )} + {activeHealthRepo && healthScores[activeHealthRepo] && ( + setActiveHealthRepo(null)} + /> + )}
); }