Files
3x-ui/docs/components/home/github-stats.tsx
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

68 lines
1.9 KiB
TypeScript

'use client';
import { useEffect, useState } from 'react';
import { GitFork, Star, Tag } from 'lucide-react';
import { fetchGitHubStats, formatCount, type GitHubStats } from '@/lib/github-stats';
/**
* Stars / forks / latest-release row. Renders the build-time numbers
* immediately (no layout shift, works without JS), then swaps in live ones
* from the GitHub API after hydration.
*/
export function GitHubStatsRow({
initial,
labels,
}: {
initial: GitHubStats;
labels: { stars: string; forks: string; latest: string };
}) {
const [stats, setStats] = useState(initial);
useEffect(() => {
let cancelled = false;
// Plain fetch, no custom headers — keeps the request preflight-free.
void fetchGitHubStats().then((live) => {
if (cancelled || !live) return;
setStats((prev) => ({ ...live, latestVersion: live.latestVersion || prev.latestVersion }));
});
return () => {
cancelled = true;
};
}, []);
return (
<dl className="mt-8 flex flex-wrap items-center justify-center gap-x-8 gap-y-3 text-sm">
<Stat icon={<Star className="size-4" aria-hidden />} label={labels.stars}>
{formatCount(stats.stars)}
</Stat>
<Stat icon={<GitFork className="size-4" aria-hidden />} label={labels.forks}>
{formatCount(stats.forks)}
</Stat>
<Stat icon={<Tag className="size-4" aria-hidden />} label={labels.latest}>
{stats.latestVersion}
</Stat>
</dl>
);
}
function Stat({
icon,
label,
children,
}: {
icon: React.ReactNode;
label: string;
children: React.ReactNode;
}) {
return (
<div className="inline-flex items-center gap-2">
<span className="text-brand">{icon}</span>
<dt className="sr-only">{label}</dt>
<dd>
<span className="font-semibold">{children}</span>{' '}
<span className="text-fd-muted-foreground">{label}</span>
</dd>
</div>
);
}