add better pb and sane defaultsz

This commit is contained in:
vee1e 2026-03-13 04:21:41 +05:30
parent 635dc571d5
commit 28f3fa9a2c
4 changed files with 366 additions and 108 deletions

View file

@ -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<RepoStats | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [timeFilter, setTimeFilter] = useState<TimeFilter>("1m");
const [timeFilter, setTimeFilter] = useState<TimeFilter>("6m");
const [selectedUser, setSelectedUser] = useState<UserStats | null>(null);
const [loadingUser, setLoadingUser] = useState(false);
const [showPRs, setShowPRs] = useState(false);
@ -999,65 +1000,17 @@ function AppContent() {
border: "1px solid var(--border)",
}}
>
<div className="flex items-center justify-center py-12">
<div className="text-center max-w-md w-full px-4">
<Loader2
className="w-8 h-8 animate-spin mx-auto mb-4"
style={{ color: "var(--text-muted)" }}
/>
<p
className="text-sm font-medium mb-3"
style={{ color: "var(--text)" }}
>
{loadingProgress}
</p>
{structuredProgress && (
<div className="w-full">
<div className="flex justify-between text-[10px] mb-1.5">
<span
className="uppercase"
style={{ color: "var(--text-muted)" }}
>
{structuredProgress.stage}
</span>
<span style={{ color: "var(--text-dim)" }}>
{structuredProgress.percentage}%
</span>
</div>
<div
className="w-full h-1.5 rounded-full overflow-hidden"
style={{ background: "var(--bg-tertiary)" }}
>
<div
className="h-full rounded-full transition-all duration-300 ease-out"
style={{
background: "var(--primary)",
width: `${structuredProgress.percentage}%`,
}}
/>
</div>
{structuredProgress.total > 1 && (
<div
className="text-[10px] mt-1.5 text-right"
style={{ color: "var(--text-dim)" }}
>
{structuredProgress.completed} /{" "}
{structuredProgress.total} units
</div>
)}
</div>
)}
{!structuredProgress && (
<p
className="text-xs mt-1"
style={{ color: "var(--text-dim)" }}
>
This may take a moment for large repositories
</p>
)}
</div>
<div className="max-w-3xl mx-auto py-8 px-2">
<LoadingProgressPanel
mode={searchMode === "org" ? "org" : "repo"}
progress={structuredProgress}
fallbackMessage={
loadingProgress ||
(searchMode === "org"
? "Preparing organization-wide analysis..."
: "Preparing repository analysis...")
}
/>
</div>
</div>
)}
@ -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)" }}
>
<div
className="p-6 rounded-lg"
style={{
background: "var(--bg-secondary)",
border: "1px solid var(--border)",
}}
>
<div className="flex items-center gap-3">
<Loader2
className="w-5 h-5 animate-spin"
style={{ color: "var(--text-muted)" }}
/>
<p style={{ color: "var(--text)" }}>
Loading user statistics...
</p>
</div>
<div className="w-full max-w-2xl px-4">
<LoadingProgressPanel
mode="member"
progress={structuredProgress}
fallbackMessage={loadingProgress || "Loading user statistics..."}
compact
/>
</div>
</div>
)}

View file

@ -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<LoadingMode, LoadingStep[]> = {
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<LoadingMode, string> = {
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 (
<div
className={`rounded-lg border relative overflow-hidden ${compact ? "p-4" : "p-5"}`}
style={{
background: "linear-gradient(180deg, var(--bg-secondary), var(--bg-tertiary))",
borderColor: "var(--border)",
}}
>
<div
className="absolute top-0 left-0 h-px w-full"
style={{
background:
"linear-gradient(90deg, transparent 0%, var(--text-muted) 30%, var(--primary) 50%, var(--text-muted) 70%, transparent 100%)",
}}
/>
<div className="flex items-center justify-between gap-3 mb-2">
<div className="flex items-center gap-2 min-w-0">
<span className="loading-signal-dot" />
<span
className="text-[10px] uppercase tracking-[0.12em] truncate"
style={{ color: "var(--text-dim)" }}
>
{MODE_LABEL[mode]}
</span>
</div>
<div className="text-xs font-medium" style={{ color: "var(--text-muted)" }}>
{safePercentage}%
</div>
</div>
<p
className={`${compact ? "text-sm" : "text-base"} font-medium leading-snug`}
style={{ color: "var(--text)" }}
>
{progress?.message || fallbackMessage}
</p>
<div className="mt-3">
<div className="h-2 rounded-full overflow-hidden loading-progress-track">
<div
className="h-full loading-progress-fill"
style={{
width: `${Math.max(3, safePercentage)}%`,
}}
/>
</div>
<div
className="mt-1.5 text-[10px] flex items-center justify-between"
style={{ color: "var(--text-dim)" }}
>
<span>{stageLabel}</span>
<span>
{progress && progress.total > 1
? `${Math.min(progress.completed, progress.total)}/${progress.total}`
: isComplete
? "Done"
: "In progress"}
</span>
</div>
</div>
<div
className={`mt-4 grid gap-2 ${compact ? "grid-cols-2" : mode === "org" ? "grid-cols-2 md:grid-cols-4" : "grid-cols-2 md:grid-cols-5"}`}
>
{steps.map((step, index) => {
const status = getStepStatus(index, activeIndex, isComplete);
const isActive = status === "active";
const isCompleted = status === "completed";
return (
<div
key={step.key}
className="rounded-md px-2.5 py-2 flex items-center gap-2 border min-w-0"
style={{
background: isActive ? "var(--bg-secondary)" : "var(--bg)",
borderColor: isCompleted || isActive ? "var(--text-muted)" : "var(--border-subtle)",
opacity: isCompleted || isActive ? 1 : 0.72,
}}
>
<span
className="flex-shrink-0"
style={{ color: isCompleted || isActive ? "var(--text-muted)" : "var(--text-dim)" }}
>
{isCompleted ? (
<Check className="w-3.5 h-3.5" />
) : isActive ? (
<Loader2 className="w-3.5 h-3.5 animate-spin" />
) : (
<Circle className="w-3.5 h-3.5" />
)}
</span>
<span
className="text-[10px] uppercase tracking-wide truncate"
style={{ color: isCompleted || isActive ? "var(--text)" : "var(--text-dim)" }}
>
{step.label}
</span>
</div>
);
})}
</div>
</div>
);
}

View file

@ -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;

View file

@ -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,
},
);
}
}),
);