Skip to content

Commit 00bc1e9

Browse files
committed
feat(docs): add GitHub stats widget to homepage
- Create GitHubStats component that fetches and displays stars, open issues, and open PRs from GitHub API - Add responsive CSS styles for the stats widget - Position the widget in the top-right corner of homepage - Remove old GitHub star component from navbar - Replace with comprehensive stats widget showing repository metrics - Implement automatic refresh every 5 minutes - Add accessibility features with keyboard navigation support
1 parent 112359b commit 00bc1e9

7 files changed

Lines changed: 181 additions & 18 deletions

File tree

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import React, { useState, useEffect, useCallback } from 'react';
2+
import styles from './styles.module.css';
3+
4+
const OWNER = 'vectorlessflow';
5+
const REPO = 'vectorless';
6+
7+
function formatNumber(num: number | null): string {
8+
if (num === null) return '—';
9+
return num.toLocaleString();
10+
}
11+
12+
async function fetchRepoBasics(): Promise<number> {
13+
const resp = await fetch(`https://api.github.com/repos/${OWNER}/${REPO}`, {
14+
headers: { Accept: 'application/vnd.github.v3+json' },
15+
});
16+
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
17+
const data = await resp.json();
18+
return data.stargazers_count ?? 0;
19+
}
20+
21+
async function fetchOpenIssues(): Promise<number> {
22+
const resp = await fetch(
23+
`https://api.github.com/search/issues?q=repo:${OWNER}/${REPO}+type:issue+state:open&per_page=1`,
24+
{ headers: { Accept: 'application/vnd.github.v3+json' } },
25+
);
26+
if (!resp.ok) throw new Error(`Issues: ${resp.status}`);
27+
const data = await resp.json();
28+
return data.total_count ?? 0;
29+
}
30+
31+
async function fetchOpenPRs(): Promise<number> {
32+
const resp = await fetch(
33+
`https://api.github.com/search/issues?q=repo:${OWNER}/${REPO}+type:pr+state:open&per_page=1`,
34+
{ headers: { Accept: 'application/vnd.github.v3+json' } },
35+
);
36+
if (!resp.ok) throw new Error(`PR: ${resp.status}`);
37+
const data = await resp.json();
38+
return data.total_count ?? 0;
39+
}
40+
41+
export default function GitHubStats(): React.ReactElement {
42+
const [stars, setStars] = useState<number | null>(null);
43+
const [issues, setIssues] = useState<number | null>(null);
44+
const [prs, setPrs] = useState<number | null>(null);
45+
const [error, setError] = useState(false);
46+
47+
const load = useCallback(async () => {
48+
setStars(null);
49+
setIssues(null);
50+
setPrs(null);
51+
setError(false);
52+
try {
53+
const [s, i, p] = await Promise.all([fetchRepoBasics(), fetchOpenIssues(), fetchOpenPRs()]);
54+
setStars(s);
55+
setIssues(i);
56+
setPrs(p);
57+
} catch {
58+
setError(true);
59+
}
60+
}, []);
61+
62+
useEffect(() => {
63+
load();
64+
const t = setInterval(load, 300000);
65+
return () => clearInterval(t);
66+
}, [load]);
67+
68+
return (
69+
<div
70+
className={styles.widget}
71+
onClick={() => window.open('https://github.com/vectorlessflow/vectorless', '_blank', 'noopener,noreferrer')}
72+
role="link"
73+
tabIndex={0}
74+
onKeyDown={(e) => { if (e.key === 'Enter') window.open('https://github.com/vectorlessflow/vectorless', '_blank', 'noopener,noreferrer'); }}>
75+
<div className={styles.statList}>
76+
<div className={styles.statRow}>
77+
<div className={styles.statLabel}>
78+
<svg width="12" height="12" viewBox="0 0 24 24" fill="var(--primary)"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg>
79+
Stars
80+
</div>
81+
<div className={styles.statNumber}>
82+
{error ? '—' : formatNumber(stars)}
83+
</div>
84+
</div>
85+
<div className={styles.statRow}>
86+
<div className={styles.statLabel}>
87+
<svg width="12" height="12" viewBox="0 0 24 24" fill="var(--primary-dark)"><circle cx="12" cy="12" r="10"/></svg>
88+
Issues
89+
</div>
90+
<div className={styles.statNumber}>
91+
{error ? '—' : formatNumber(issues)}
92+
</div>
93+
</div>
94+
<div className={styles.statRow}>
95+
<div className={styles.statLabel}>
96+
<svg width="12" height="12" viewBox="0 0 24 24" fill="var(--primary-light)"><path d="M6 3v18h12V3H6zm10 16H8V5h8v14z"/></svg>
97+
PRs
98+
</div>
99+
<div className={styles.statNumber}>
100+
{error ? '—' : formatNumber(prs)}
101+
</div>
102+
</div>
103+
</div>
104+
</div>
105+
);
106+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
.widget {
2+
background: var(--bg-offset);
3+
backdrop-filter: blur(12px);
4+
border-radius: 1.2rem;
5+
border: 1px solid var(--border);
6+
padding: 0.9rem 1.2rem;
7+
width: 130px;
8+
min-width: 130px;
9+
box-sizing: border-box;
10+
transition: all 0.2s ease;
11+
box-shadow: 0 8px 20px -6px rgba(0, 0, 0, 0.1);
12+
cursor: pointer;
13+
}
14+
15+
.widget:hover {
16+
border-color: var(--primary);
17+
}
18+
19+
.statList {
20+
display: flex;
21+
flex-direction: column;
22+
gap: 0.5rem;
23+
}
24+
25+
.statRow {
26+
display: flex;
27+
align-items: center;
28+
justify-content: space-between;
29+
gap: 1rem;
30+
padding: 0.4rem 0;
31+
}
32+
33+
.statLabel {
34+
display: flex;
35+
align-items: center;
36+
gap: 0.5rem;
37+
font-weight: 500;
38+
font-size: 0.8rem;
39+
color: var(--text-light);
40+
white-space: nowrap;
41+
flex-shrink: 0;
42+
}
43+
44+
.statNumber {
45+
font-family: 'JetBrains Mono', monospace;
46+
font-size: 0.9rem;
47+
font-weight: 700;
48+
letter-spacing: -0.3px;
49+
color: var(--text);
50+
white-space: nowrap;
51+
margin-left: auto;
52+
}
53+
54+
.errorText { color: #F87171; }
55+
56+
@media (max-width: 880px) {
57+
.widget {
58+
width: 100%;
59+
min-width: unset;
60+
}
61+
}

docs/src/css/custom.css

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -66,10 +66,6 @@
6666
/* ===== Navbar ===== */
6767
.navbar {
6868
background-color: var(--bg) !important;
69-
background-image:
70-
linear-gradient(rgba(175, 120, 139, 0.06) 1px, transparent 1px),
71-
linear-gradient(90deg, rgba(175, 120, 139, 0.06) 1px, transparent 1px);
72-
background-size: 48px 48px;
7369
border-bottom: none !important;
7470
box-shadow: none !important;
7571
height: 68px !important;

docs/src/pages/index.module.css

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,24 @@
99
padding: 0;
1010
height: calc(100vh - 68px);
1111
overflow: hidden;
12+
position: relative;
1213
display: flex;
1314
align-items: center;
1415
justify-content: center;
1516
background-color: var(--bg);
16-
background-image:
17-
linear-gradient(rgba(175, 120, 139, 0.06) 1px, transparent 1px),
18-
linear-gradient(90deg, rgba(175, 120, 139, 0.06) 1px, transparent 1px);
19-
background-size: 48px 48px;
2017
font-family: 'Space Grotesk', sans-serif;
2118
color: var(--text);
2219
line-height: 1.5;
2320
}
2421

22+
/* ===== Stats widget (top-right corner) ===== */
23+
.statsCorner {
24+
position: absolute;
25+
top: 20px;
26+
right: 32px;
27+
z-index: 2;
28+
}
29+
2530
/* ===== Hero Container ===== */
2631
.hero {
2732
max-width: 1280px;

docs/src/pages/index.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type {ReactNode} from 'react';
22
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
33
import Layout from '@theme/Layout';
44
import Link from '@docusaurus/Link';
5+
import GitHubStats from '@site/src/components/GitHubStats';
56

67
import styles from './index.module.css';
78

@@ -34,6 +35,9 @@ function HamsterIcon({size = 14}: {size?: number}) {
3435
function HomepageHeader() {
3536
return (
3637
<header className={styles.heroBanner}>
38+
<div className={styles.statsCorner}>
39+
<GitHubStats />
40+
</div>
3741
<div className={styles.hero}>
3842
{/* Left: Brand + Features */}
3943
<div className={styles.heroContent}>

docs/src/theme/Navbar/index.tsx

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import NavbarItem from '@theme/NavbarItem';
55
import NavbarMobileSidebarToggle from '@theme/Navbar/MobileSidebar/Toggle';
66
import useBaseUrl from '@docusaurus/useBaseUrl';
77
import Link from '@docusaurus/Link';
8-
import GitHubStar from '../../components/GitHubStar';
98
import type {Props as NavbarItemConfig} from '@theme/NavbarItem';
109
import styles from './styles.module.css';
1110

@@ -61,9 +60,6 @@ export default function Navbar(): React.ReactElement {
6160
{centerItems.map((item, i) => <NavbarItem {...(item as NavbarItemConfig)} key={i} />)}
6261
</div>
6362
<div className={styles.navbarRight}>
64-
<div className={styles.githubStarWrapper}>
65-
<GitHubStar />
66-
</div>
6763
<ColorModeToggle />
6864
</div>
6965
</div>

docs/src/theme/Navbar/styles.module.css

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@
7373
opacity: 1;
7474
}
7575

76-
/* Right: github star */
76+
/* Right: theme toggle */
7777
.navbarRight {
7878
display: flex;
7979
align-items: center;
@@ -83,11 +83,6 @@
8383
padding-right: 24px;
8484
}
8585

86-
.githubStarWrapper {
87-
display: flex;
88-
align-items: center;
89-
}
90-
9186
/* Theme toggle */
9287
.themeToggle {
9388
display: inline-flex;

0 commit comments

Comments
 (0)