From 28f3fa9a2c09e48b55775ce7cb606c0726deddfc Mon Sep 17 00:00:00 2001 From: vee1e Date: Fri, 13 Mar 2026 04:21:41 +0530 Subject: [PATCH] add better pb and sane defaultsz --- src/App.tsx | 96 +++-------- src/components/LoadingProgressPanel.tsx | 213 ++++++++++++++++++++++++ src/index.css | 60 +++++++ src/utils/github.ts | 105 ++++++++---- 4 files changed, 366 insertions(+), 108 deletions(-) create mode 100644 src/components/LoadingProgressPanel.tsx diff --git a/src/App.tsx b/src/App.tsx index 7850b7f..56b6a62 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -36,6 +36,7 @@ import type { } from "./types"; import { UserStatsModal } from "./components/UserStatsModal"; import { TokenSettings } from "./components/TokenSettings"; +import { LoadingProgressPanel } from "./components/LoadingProgressPanel"; import { hasGitHubToken } from "./utils/env"; import { ThemeProvider } from "./contexts/ThemeContext"; @@ -44,7 +45,7 @@ function AppContent() { const [stats, setStats] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); - const [timeFilter, setTimeFilter] = useState("1m"); + const [timeFilter, setTimeFilter] = useState("6m"); const [selectedUser, setSelectedUser] = useState(null); const [loadingUser, setLoadingUser] = useState(false); const [showPRs, setShowPRs] = useState(false); @@ -999,65 +1000,17 @@ function AppContent() { border: "1px solid var(--border)", }} > -
-
- -

- {loadingProgress} -

- - {structuredProgress && ( -
-
- - {structuredProgress.stage} - - - {structuredProgress.percentage}% - -
-
-
-
- {structuredProgress.total > 1 && ( -
- {structuredProgress.completed} /{" "} - {structuredProgress.total} units -
- )} -
- )} - - {!structuredProgress && ( -

- This may take a moment for large repositories -

- )} -
+
+
)} @@ -1068,22 +1021,13 @@ function AppContent() { className="fixed inset-0 flex items-center justify-center z-50" style={{ background: "rgba(0,0,0,0.8)" }} > -
-
- -

- Loading user statistics... -

-
+
+
)} diff --git a/src/components/LoadingProgressPanel.tsx b/src/components/LoadingProgressPanel.tsx new file mode 100644 index 0000000..9187af7 --- /dev/null +++ b/src/components/LoadingProgressPanel.tsx @@ -0,0 +1,213 @@ +import { Check, Circle, Loader2 } from "lucide-react"; +import type { LoadingProgressState } from "../utils/github"; + +type LoadingMode = "repo" | "org" | "member"; + +interface LoadingStep { + key: string; + label: string; +} + +interface LoadingProgressPanelProps { + mode: LoadingMode; + progress: LoadingProgressState | null; + fallbackMessage: string; + compact?: boolean; +} + +const MODE_STEPS: Record = { + repo: [ + { key: "repo", label: "Repository" }, + { key: "collaborators", label: "Collaborators" }, + { key: "repo-prs", label: "Pull Requests" }, + { key: "processing", label: "Contributors" }, + { key: "completed", label: "Completed" }, + ], + org: [ + { key: "org-repos", label: "Repositories" }, + { key: "org-scan", label: "Organization Scan" }, + { key: "finalizing", label: "Finalizing" }, + { key: "completed", label: "Completed" }, + ], + member: [ + { key: "user", label: "Profile" }, + { key: "user-prs", label: "Pull Requests" }, + { key: "user-processing", label: "Repositories" }, + { key: "maintainer-check", label: "Maintainer Check" }, + { key: "completed", label: "Completed" }, + ], +}; + +const MODE_LABEL: Record = { + repo: "Repository Analysis", + org: "Organization Analysis", + member: "Member Analysis", +}; + +const prettifyStage = (stage: string): string => + stage + .split("-") + .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) + .join(" "); + +const getActiveIndex = ( + steps: LoadingStep[], + stage: string | undefined, + percentage: number, +): number => { + if (stage) { + const exactMatchIndex = steps.findIndex((step) => step.key === stage); + if (exactMatchIndex >= 0) { + return exactMatchIndex; + } + } + + if (percentage >= 100) { + return steps.length - 1; + } + + if (percentage <= 0) { + return 0; + } + + const estimatedIndex = Math.floor((percentage / 100) * steps.length); + return Math.min(steps.length - 1, Math.max(0, estimatedIndex)); +}; + +const getStepStatus = ( + index: number, + activeIndex: number, + isComplete: boolean, +): "pending" | "active" | "completed" => { + if (isComplete && index <= activeIndex) { + return "completed"; + } + if (index < activeIndex) { + return "completed"; + } + if (index === activeIndex) { + return "active"; + } + return "pending"; +}; + +export function LoadingProgressPanel({ + mode, + progress, + fallbackMessage, + compact = false, +}: LoadingProgressPanelProps) { + const steps = MODE_STEPS[mode]; + const percentage = progress?.percentage ?? 0; + const safePercentage = Math.min(100, Math.max(0, percentage)); + const activeIndex = getActiveIndex(steps, progress?.stage, safePercentage); + const isComplete = progress?.stage === "completed" || safePercentage >= 100; + const stageLabel = + steps.find((step) => step.key === progress?.stage)?.label ?? + (progress?.stage ? prettifyStage(progress.stage) : "Initializing"); + + return ( +
+
+ +
+
+ + + {MODE_LABEL[mode]} + +
+
+ {safePercentage}% +
+
+ +

+ {progress?.message || fallbackMessage} +

+ +
+
+
+
+
+ {stageLabel} + + {progress && progress.total > 1 + ? `${Math.min(progress.completed, progress.total)}/${progress.total}` + : isComplete + ? "Done" + : "In progress"} + +
+
+ +
+ {steps.map((step, index) => { + const status = getStepStatus(index, activeIndex, isComplete); + const isActive = status === "active"; + const isCompleted = status === "completed"; + + return ( +
+ + {isCompleted ? ( + + ) : isActive ? ( + + ) : ( + + )} + + + {step.label} + +
+ ); + })} +
+
+ ); +} diff --git a/src/index.css b/src/index.css index e569231..6459a44 100644 --- a/src/index.css +++ b/src/index.css @@ -157,6 +157,66 @@ body { animation: fadeIn 0.2s ease-out; } +@keyframes loadingSweep { + 0% { + transform: translateX(-120%); + } + 100% { + transform: translateX(220%); + } +} + +@keyframes loadingPulse { + 0%, + 100% { + opacity: 0.45; + } + 50% { + opacity: 1; + } +} + +.loading-signal-dot { + width: 7px; + height: 7px; + border-radius: 9999px; + background: var(--text-muted); + animation: loadingPulse 1.15s ease-in-out infinite; +} + +.loading-progress-track { + background: linear-gradient(180deg, var(--bg) 0%, var(--bg-secondary) 100%); + border: 1px solid var(--border-subtle); +} + +.loading-progress-fill { + position: relative; + border-radius: 9999px; + background: linear-gradient( + 90deg, + var(--text-dim) 0%, + var(--text-muted) 45%, + var(--primary) 100% + ); + box-shadow: 0 0 14px rgba(160, 160, 160, 0.2); + transition: width 280ms cubic-bezier(0.2, 0.9, 0.2, 1); + overflow: hidden; +} + +.loading-progress-fill::after { + content: ""; + position: absolute; + inset: 0; + width: 36%; + background: linear-gradient( + 110deg, + transparent 0%, + rgba(255, 255, 255, 0.34) 50%, + transparent 100% + ); + animation: loadingSweep 1.4s linear infinite; +} + /* Scrollbar */ ::-webkit-scrollbar { width: 8px; diff --git a/src/utils/github.ts b/src/utils/github.ts index b50a31f..ab9ab57 100644 --- a/src/utils/github.ts +++ b/src/utils/github.ts @@ -130,7 +130,7 @@ async function fetchMaintainers( if (!quiet) { emitLoadingProgress("Fetching collaborators...", { stage: "collaborators", - percentage: 5, + percentage: 18, completed: 0, total: 1, }); @@ -311,7 +311,7 @@ export const fetchRepoStats = async ( // Use optimized maintainer fetching emitLoadingProgress("Fetching repository information...", { stage: "repo", - percentage: 5, + percentage: 8, completed: 0, total: 1, }); @@ -327,17 +327,17 @@ export const fetchRepoStats = async ( maintainers = new Set(); } - // Single-repo pull request fetch (repo mode) - emitLoadingProgress("Fetching pull requests...", { - stage: "repo-prs", - percentage: 10, - completed: 0, - total: 1, - }); const pullRequests: PullRequest[] = []; let page = 1; let hasMore = true; const MAX_PAGES = 5; + // Single-repo pull request fetch (repo mode) + emitLoadingProgress("Fetching pull requests...", { + stage: "repo-prs", + percentage: 26, + completed: 0, + total: MAX_PAGES, + }); try { while (hasMore && page <= MAX_PAGES) { @@ -389,12 +389,16 @@ export const fetchRepoStats = async ( })); pullRequests.push(...mappedPRs); + const pagesFetched = page; page++; - const pagePct = Math.min(85, 10 + Math.floor((page / MAX_PAGES) * 70)); - emitLoadingProgress(`Fetched page ${page - 1} of pull requests...`, { + const pagePct = Math.min( + 82, + 26 + Math.floor((pagesFetched / MAX_PAGES) * 56), + ); + emitLoadingProgress(`Fetched page ${pagesFetched} of pull requests...`, { stage: "repo-prs", percentage: pagePct, - completed: page - 1, + completed: pagesFetched, total: MAX_PAGES, }); } @@ -404,7 +408,7 @@ export const fetchRepoStats = async ( emitLoadingProgress("Processing contributor data...", { stage: "processing", - percentage: 92, + percentage: 90, completed: 1, total: 1, }); @@ -439,7 +443,10 @@ export const fetchRepoStats = async ( }); const contributors = Array.from(contributorMap.values()).sort( - (a, b) => b.totalPRs - a.totalPRs, + (a, b) => + b.mergedPRs - a.mergedPRs || + b.totalPRs - a.totalPRs || + b.openPRs - a.openPRs, ); for (const contributor of contributors) { @@ -450,6 +457,13 @@ export const fetchRepoStats = async ( } } + emitLoadingProgress("Repository scan completed", { + stage: "completed", + percentage: 100, + completed: 1, + total: 1, + }); + return { totalPRs: pullRequests.length, contributors, @@ -474,7 +488,7 @@ export const fetchOrganizationActiveUsers = async ( emitLoadingProgress(`Fetching repositories for ${normalizedOrg}...`, { stage: "org-repos", - percentage: 3, + percentage: 8, completed: 0, total: 1, }); @@ -483,6 +497,15 @@ export const fetchOrganizationActiveUsers = async ( const reposToScan = Array.from(new Set(firstPageRepos)); const MAX_REPOS = 50; const selectedRepos = reposToScan.slice(0, MAX_REPOS); + emitLoadingProgress( + `Preparing organization scan across ${selectedRepos.length} repositories...`, + { + stage: "org-repos", + percentage: 18, + completed: selectedRepos.length, + total: Math.max(1, selectedRepos.length), + }, + ); const MAX_PAGES_PER_REPO = 5; const pullRequests: PullRequest[] = []; @@ -511,8 +534,8 @@ export const fetchOrganizationActiveUsers = async ( ); const currentUnit = repoIndex * MAX_PAGES_PER_REPO + page; const scanPct = Math.min( - 95, - 5 + Math.floor((currentUnit / totalUnits) * 85), + 92, + 20 + Math.floor((currentUnit / totalUnits) * 72), ); emitLoadingProgress( `Org scan ${repoIndex + 1}/${selectedRepos.length} • ${repoName} • page ${page}`, @@ -601,9 +624,9 @@ export const fetchOrganizationActiveUsers = async ( emitLoadingProgress("Finalizing org activity results...", { stage: "finalizing", - percentage: 98, + percentage: 96, completed: selectedRepos.length, - total: selectedRepos.length, + total: Math.max(1, selectedRepos.length), }); const users = aggregateOrganizationActiveUsers(pullRequests, maintainersMap); @@ -611,8 +634,8 @@ export const fetchOrganizationActiveUsers = async ( emitLoadingProgress("Org scan completed", { stage: "completed", percentage: 100, - completed: selectedRepos.length, - total: selectedRepos.length, + completed: selectedRepos.length > 0 ? selectedRepos.length : 1, + total: Math.max(1, selectedRepos.length), }); return { @@ -679,7 +702,7 @@ export const fetchUserStats = async ( emitLoadingProgress(`Fetching user data for ${username}...`, { stage: "user", - percentage: 5, + percentage: 8, completed: 0, total: 1, }); @@ -698,7 +721,7 @@ export const fetchUserStats = async ( // Fetch pull requests emitLoadingProgress(`Fetching ${username}'s pull requests...`, { stage: "user-prs", - percentage: 10, + percentage: 20, completed: 0, total: MAX_PAGES, }); @@ -755,15 +778,16 @@ export const fetchUserStats = async ( })), ); + const pagesFetched = page; page++; const userPagePct = Math.min( - 85, - 10 + Math.floor((page / MAX_PAGES) * 70), + 82, + 22 + Math.floor((pagesFetched / MAX_PAGES) * 60), ); - emitLoadingProgress(`Fetched page ${page - 1} of pull requests...`, { + emitLoadingProgress(`Fetched page ${pagesFetched} of pull requests...`, { stage: "user-prs", percentage: userPagePct, - completed: page - 1, + completed: pagesFetched, total: MAX_PAGES, }); } @@ -774,7 +798,7 @@ export const fetchUserStats = async ( emitLoadingProgress(`Processing repositories for ${username}...`, { stage: "user-processing", - percentage: 90, + percentage: 86, completed: 1, total: 1, }); @@ -828,22 +852,24 @@ export const fetchUserStats = async ( .sort(([, a], [, b]) => b.totalPRs - a.totalPRs) .slice(0, 3); // Top 3 repos + const totalChecks = Math.max(1, repoEntries.length); emitLoadingProgress(`Checking maintainer status...`, { stage: "maintainer-check", - percentage: 95, - completed: 0, - total: repoEntries.length || 1, + percentage: repoEntries.length > 0 ? 90 : 97, + completed: repoEntries.length > 0 ? 0 : 1, + total: totalChecks, }); // Use Promise.all instead of Promise.any for better performance if (repoEntries.length > 0) { + let checksCompleted = 0; const checkResults = await Promise.all( repoEntries.map(async ([repoName]) => { try { const [owner, repo] = repoName.split("/"); if (!owner || !repo) return false; - const maintainers = await getMaintainers(octokit, owner, repo); + const maintainers = await getMaintainers(octokit, owner, repo, true); const isMaintainerForRepo = maintainers.has(username); console.log( @@ -852,6 +878,21 @@ export const fetchUserStats = async ( return isMaintainerForRepo; } catch { return false; + } finally { + checksCompleted += 1; + const checkPct = Math.min( + 98, + 90 + Math.floor((checksCompleted / totalChecks) * 8), + ); + emitLoadingProgress( + `Checking maintainer status... (${checksCompleted}/${totalChecks})`, + { + stage: "maintainer-check", + percentage: checkPct, + completed: checksCompleted, + total: totalChecks, + }, + ); } }), );