CI / go-test (push) Canceled after 0s
CI / postgres-durable-first (push) Canceled after 0s
CI / codegen (push) Canceled after 0s
CI / govulncheck (push) Canceled after 0s
CI / race (push) Canceled after 0s
CI / fuzz-smoke (push) Canceled after 0s
CI / golangci (push) Canceled after 0s
CI / frontend (push) Canceled after 0s
CodeQL Advanced / Analyze (go) (push) Canceled after 0s
CodeQL Advanced / Analyze (actions) (push) Canceled after 0s
CodeQL Advanced / Analyze (javascript-typescript) (push) Canceled after 0s
Docs CI / build (push) Canceled after 0s
Docs Deploy (GitHub Pages) / build (push) Canceled after 0s
Release 3X-UI / build (386) (push) Canceled after 0s
Release 3X-UI / build (amd64) (push) Canceled after 0s
Release 3X-UI / build (arm64) (push) Canceled after 0s
Release 3X-UI / build (armv5) (push) Canceled after 0s
Release 3X-UI / build (armv6) (push) Canceled after 0s
Release 3X-UI / build (armv7) (push) Canceled after 0s
Release 3X-UI / build (s390x) (push) Canceled after 0s
Release 3X-UI / Build for Windows (amd64) (push) Canceled after 0s
Deploy Smoke Tests / noninteractive-install (ubuntu-24.04-arm) (push) Canceled after 0s
Deploy Smoke Tests / noninteractive-install (ubuntu-latest) (push) Canceled after 0s
Deploy Smoke Tests / release-tag-install (ubuntu-24.04-arm) (push) Canceled after 0s
Deploy Smoke Tests / release-tag-install (ubuntu-latest) (push) Canceled after 0s
Docs Deploy (GitHub Pages) / deploy (push) Canceled after 0s
Release 3X-UI / Publish rolling dev release (push) Canceled after 0s
73 lines
2.5 KiB
TypeScript
73 lines
2.5 KiB
TypeScript
import { productRepo } from './shared';
|
|
|
|
export interface GitHubStats {
|
|
stars: number;
|
|
forks: number;
|
|
latestVersion: string;
|
|
}
|
|
|
|
// Real, recent numbers used as a fallback when the GitHub API is unavailable
|
|
// at build time (offline CI, rate limit). Update periodically.
|
|
const FALLBACK: GitHubStats = {
|
|
stars: 41500,
|
|
forks: 7700,
|
|
latestVersion: 'v3.x',
|
|
};
|
|
|
|
const API_BASE = `https://api.github.com/repos/${productRepo.user}/${productRepo.repo}`;
|
|
|
|
/**
|
|
* Fetch live repo stats. Runs both at build time (Node) and in the browser —
|
|
* api.github.com sends CORS headers, and the unauthenticated limit (60 req/h
|
|
* per client IP) is plenty for one call per landing-page visit.
|
|
* Returns null on any failure so callers keep the numbers they already have;
|
|
* `latestVersion` is '' when the release lookup alone fails.
|
|
*/
|
|
export async function fetchGitHubStats(init?: RequestInit): Promise<GitHubStats | null> {
|
|
try {
|
|
const [repoRes, releaseRes] = await Promise.all([
|
|
fetch(API_BASE, init),
|
|
fetch(`${API_BASE}/releases/latest`, init),
|
|
]);
|
|
|
|
if (!repoRes.ok) return null;
|
|
const repo = (await repoRes.json()) as { stargazers_count?: number; forks_count?: number };
|
|
if (typeof repo.stargazers_count !== 'number' || typeof repo.forks_count !== 'number') {
|
|
return null;
|
|
}
|
|
|
|
let latestVersion = '';
|
|
if (releaseRes.ok) {
|
|
const release = (await releaseRes.json()) as { tag_name?: string };
|
|
if (release.tag_name) latestVersion = release.tag_name;
|
|
}
|
|
|
|
return { stars: repo.stargazers_count, forks: repo.forks_count, latestVersion };
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Build-time stats used as the initial render (no layout shift, works without
|
|
* JS). The client refreshes them via fetchGitHubStats() after hydration.
|
|
* Always resolves; on any error it returns the hardcoded fallback.
|
|
*/
|
|
export async function getGitHubStats(): Promise<GitHubStats> {
|
|
const headers: Record<string, string> = {
|
|
'User-Agent': '3x-ui-docs',
|
|
Accept: 'application/vnd.github+json',
|
|
};
|
|
if (process.env.GITHUB_TOKEN) headers.Authorization = `Bearer ${process.env.GITHUB_TOKEN}`;
|
|
|
|
const live = await fetchGitHubStats({ headers, next: { revalidate: 3600 } });
|
|
if (!live) return FALLBACK;
|
|
return { ...live, latestVersion: live.latestVersion || FALLBACK.latestVersion };
|
|
}
|
|
|
|
/** Compact display, e.g. 41523 -> "41.5k". */
|
|
export function formatCount(n: number): string {
|
|
if (n >= 1000) return `${(n / 1000).toFixed(1)}k`;
|
|
return String(n);
|
|
}
|