Files
3x-ui/docs/lib/github-stats.ts
T
MHSanaei 9b91f0f42e docs: vendor the documentation site into the monorepo
Fold the standalone 3x-ui-docs project (Next.js 16 + Fumadocs, deployed to
docs.sanaei.dev) into docs/ so the panel and its documentation share a single
source of truth, the way sing-box keeps its docs in-tree. The old repo becomes
redundant and can be retired.

- Import the full site under docs/ (app, components, content, lib, public,
  scripts, config). The self-contained pnpm project sits alongside the existing
  engineering notes with no filename collisions.
- Re-point "Edit on GitHub" links from MHSanaei/3x-ui-docs to this repo's
  docs/content/docs path (docs/lib/shared.ts, docs/app/.../page.tsx).
- Add docs-ci.yml and docs-deploy.yml under .github/workflows/, scoped to
  docs/** and run with working-directory: docs, since GitHub only runs
  workflows from the repo-root .github/. deploy-static.yml's GitHub Pages
  publish (CNAME docs.sanaei.dev) carries over unchanged.

Follow-up (outside this commit): attach the docs.sanaei.dev custom domain to
this repository's Pages (or set the Vercel project's root directory to docs),
confirm the site is live from the monorepo, then delete MHSanaei/3x-ui-docs.
2026-07-07 23:07:14 +02:00

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